Изменение домена с rosetta-im.com на rosetta.im

This commit is contained in:
RoyceDa
2026-02-12 14:20:29 +02:00
parent e229b2d61f
commit fe5bf2bd04
114 changed files with 435 additions and 435 deletions

View File

@@ -0,0 +1,210 @@
package im.rosetta;
import im.rosetta.client.ClientManager;
import im.rosetta.client.OnlineManager;
import im.rosetta.event.EventManager;
import im.rosetta.executors.Executor0Handshake;
import im.rosetta.executors.Executor10RequestUpdate;
import im.rosetta.executors.Executor11Typeing;
import im.rosetta.executors.Executor15RequestTransport;
import im.rosetta.executors.Executor16PushNotification;
import im.rosetta.executors.Executor17GroupCreate;
import im.rosetta.executors.Executor18GroupInfo;
import im.rosetta.executors.Executor19GroupInviteInfo;
import im.rosetta.executors.Executor1UserInfo;
import im.rosetta.executors.Executor20GroupJoin;
import im.rosetta.executors.Executor21GroupLeave;
import im.rosetta.executors.Executor22GroupBan;
import im.rosetta.executors.Executor24DeviceResolve;
import im.rosetta.executors.Executor3Search;
import im.rosetta.executors.Executor4OnlineState;
import im.rosetta.executors.Executor6Message;
import im.rosetta.executors.Executor7Read;
import im.rosetta.listeners.DeviceListListener;
import im.rosetta.listeners.HandshakeCompleteListener;
import im.rosetta.listeners.OnlineStatusDisconnectListener;
import im.rosetta.listeners.OnlineStatusHandshakeCompleteListener;
import im.rosetta.listeners.ServerStopListener;
import im.rosetta.logger.Logger;
import im.rosetta.logger.enums.Color;
import im.rosetta.logger.enums.LogLevel;
import im.rosetta.packet.Packet0Handshake;
import im.rosetta.packet.Packet10RequestUpdate;
import im.rosetta.packet.Packet11Typeing;
import im.rosetta.packet.Packet15RequestTransport;
import im.rosetta.packet.Packet16PushNotification;
import im.rosetta.packet.Packet17GroupCreate;
import im.rosetta.packet.Packet18GroupInfo;
import im.rosetta.packet.Packet19GroupInviteInfo;
import im.rosetta.packet.Packet1UserInfo;
import im.rosetta.packet.Packet20GroupJoin;
import im.rosetta.packet.Packet21GroupLeave;
import im.rosetta.packet.Packet22GroupBan;
import im.rosetta.packet.Packet23DeviceList;
import im.rosetta.packet.Packet24DeviceResolve;
import im.rosetta.packet.Packet2Result;
import im.rosetta.packet.Packet3Search;
import im.rosetta.packet.Packet4OnlineSubscribe;
import im.rosetta.packet.Packet5OnlineState;
import im.rosetta.packet.Packet6Message;
import im.rosetta.packet.Packet7Read;
import im.rosetta.packet.Packet8Delivery;
import im.rosetta.packet.Packet9DeviceNew;
import io.orprotocol.Server;
import io.orprotocol.Settings;
import io.orprotocol.packet.PacketManager;
/**
* Boot отвечает за инициализацию всех пакетов и их обработчиков,
* а так же событий приложения. Этот Boot отвечает за приложение, а не за протокол.
*
* Нужен он для того, чтобы все части приложения получали одинаковые ссылки на глобальные обьекты приложения, такие как менеджер пакетов,
* менеджер событий, менеджер клиентов и так далее
*/
public class Boot {
private PacketManager packetManager;
private EventManager eventManager;
private Logger logger;
private Server server;
private ServerAdapter serverAdapter;
private ClientManager clientManager;
private OnlineManager onlineManager;
/**
* Конструктор по умолчанию, использует порт 3000 для сервера
*/
public Boot() {
this(3000);
}
/**
* Инициализатор приложения
* @param port Порт, на котором будет работать сервер. Если не указан, то будет использован порт 3000
*/
public Boot(int port) {
this.packetManager = new PacketManager();
this.eventManager = new EventManager();
this.onlineManager = new OnlineManager();
this.logger = new Logger(LogLevel.INFO);
this.serverAdapter = new ServerAdapter(this.eventManager);
this.server = new Server(new Settings(
port,
30
), packetManager, this.serverAdapter);
this.clientManager = new ClientManager(server);
}
/**
* Получить менеджер пакетов приложения
* @return PacketManager
*/
public PacketManager getPacketManager() {
return this.packetManager;
}
/**
* Получить менеджер событий приложения
* @return EventManager
*/
public EventManager getEventManager() {
return this.eventManager;
}
/**
* Получить менеджера клиентов, нужно для того чтобы отправить пакет списку клиентов например
* @return менеджер клиентов
*/
public ClientManager getClientManager() {
return this.clientManager;
}
/**
* Получить логгер приложения
* @return Logger
*/
public Logger getLogger() {
return this.logger;
}
/**
* Запуск сервера, регистрация пакетов, обработчиков, событий приложения
* @return Boot
*/
public Boot bootstrap() {
try{
this.server.start();
this.registerAllPackets();
this.registerAllExecutors();
this.registerAllEvents();
this.printBootMessage();
return this;
}catch(Exception e){
this.logger.error(Color.RED + "Booting error, stack trace:");
e.printStackTrace();
return null;
}
}
private void registerAllEvents() {
this.eventManager.registerListener(new ServerStopListener(this.logger));
this.eventManager.registerListener(new HandshakeCompleteListener());
this.eventManager.registerListener(new OnlineStatusHandshakeCompleteListener(this.onlineManager));
this.eventManager.registerListener(new OnlineStatusDisconnectListener(this.onlineManager));
this.eventManager.registerListener(new DeviceListListener(this.clientManager));
}
private void registerAllPackets() {
this.packetManager.registerPacket(0, Packet0Handshake.class);
this.packetManager.registerPacket(1, Packet1UserInfo.class);
this.packetManager.registerPacket(2, Packet2Result.class);
this.packetManager.registerPacket(3, Packet3Search.class);
this.packetManager.registerPacket(4, Packet4OnlineSubscribe.class);
this.packetManager.registerPacket(5, Packet5OnlineState.class);
this.packetManager.registerPacket(6, Packet6Message.class);
this.packetManager.registerPacket(7, Packet7Read.class);
this.packetManager.registerPacket(8, Packet8Delivery.class);
this.packetManager.registerPacket(9, Packet9DeviceNew.class);
this.packetManager.registerPacket(10, Packet10RequestUpdate.class);
this.packetManager.registerPacket(11, Packet11Typeing.class);
//RESERVED 12 PACKET AVATAR (unused)
//RESERVED 13 PACKET KERNEL UPDATE (unused)
//RESERVED 14 PACKET APP UPDATE (unused)
this.packetManager.registerPacket(15, Packet15RequestTransport.class);
this.packetManager.registerPacket(16, Packet16PushNotification.class);
this.packetManager.registerPacket(17, Packet17GroupCreate.class);
this.packetManager.registerPacket(18, Packet18GroupInfo.class);
this.packetManager.registerPacket(19, Packet19GroupInviteInfo.class);
this.packetManager.registerPacket(20, Packet20GroupJoin.class);
this.packetManager.registerPacket(21, Packet21GroupLeave.class);
this.packetManager.registerPacket(22, Packet22GroupBan.class);
this.packetManager.registerPacket(23, Packet23DeviceList.class);
this.packetManager.registerPacket(24, Packet24DeviceResolve.class);
}
private void registerAllExecutors() {
this.packetManager.registerExecutor(0, new Executor0Handshake(this.eventManager, this.clientManager, this.packetManager));
this.packetManager.registerExecutor(1, new Executor1UserInfo());
this.packetManager.registerExecutor(3, new Executor3Search(this.clientManager));
this.packetManager.registerExecutor(4, new Executor4OnlineState(this.onlineManager, this.clientManager));
this.packetManager.registerExecutor(6, new Executor6Message(this.clientManager, this.packetManager));
this.packetManager.registerExecutor(7, new Executor7Read(this.clientManager, this.packetManager));
this.packetManager.registerExecutor(10, new Executor10RequestUpdate());
this.packetManager.registerExecutor(11, new Executor11Typeing(this.clientManager, this.packetManager));
this.packetManager.registerExecutor(15, new Executor15RequestTransport());
this.packetManager.registerExecutor(16, new Executor16PushNotification());
this.packetManager.registerExecutor(17, new Executor17GroupCreate());
this.packetManager.registerExecutor(18, new Executor18GroupInfo());
this.packetManager.registerExecutor(19, new Executor19GroupInviteInfo());
this.packetManager.registerExecutor(20, new Executor20GroupJoin());
this.packetManager.registerExecutor(21, new Executor21GroupLeave());
this.packetManager.registerExecutor(22, new Executor22GroupBan());
this.packetManager.registerExecutor(24, new Executor24DeviceResolve(this.clientManager, this.eventManager));
}
private void printBootMessage() {
this.logger.log(LogLevel.INFO, Color.GREEN + "Boot successful complete");
}
}

View File

@@ -0,0 +1,47 @@
package im.rosetta;
import io.orprotocol.BaseFailures;
public enum Failures implements BaseFailures {
/**
* Ошибка аутентификации
*/
AUTHENTIFICATION_ERROR(3001),
DATA_MISSMATCH(3001),
/**
* Handshake не завершен
*/
HANDSHAKE_NOT_COMPLETED(3002),
/**
* Пользователь не состоит в группе, в которую пытается отправить сообщение
*/
USER_NOT_IN_GROUP(3005),
/**
* Неподдерживаемый протокол
*/
UNSUPPORTED_PROTOCOL(3008),
/**
* Слишком много вложений отправлено в сообщении
*/
TOO_MANY_ATTACHMENTS(3009),
/**
* Слишком много подписок на онлайн статусы
*/
TOO_MANY_ONLINE_SUBSCRIPTIONS(3010);
private final int code;
Failures(int code) {
this.code = code;
}
/**
* Получает код ошибки.
* @return Код ошибки.
*/
public int getCode() {
return code;
}
}

View File

@@ -0,0 +1,38 @@
package im.rosetta;
public class Main {
public static void main(String[] args) {
int port = resolvePort(args);
/**
* Регистрация всех пакетов и их обработчиков
*/
Boot boot = new Boot(port);
/**
* Стартуем сервер
*/
boot.bootstrap();
}
private static int resolvePort(String[] args) {
// Если порт указан аргументом — используем его.
if (args != null && args.length > 0) {
try {
return Integer.parseInt(args[0]);
} catch (NumberFormatException ignored) {
}
}
// Если порт задан в окружении — используем его.
String envPort = System.getenv("PORT");
if (envPort != null && !envPort.isBlank()) {
try {
return Integer.parseInt(envPort);
} catch (NumberFormatException ignored) {
}
}
// Значение по умолчанию.
return 3000;
}
}

View File

@@ -0,0 +1,71 @@
package im.rosetta;
import im.rosetta.event.EventManager;
import im.rosetta.event.events.ConnectEvent;
import im.rosetta.event.events.DisconnectEvent;
import im.rosetta.event.events.PacketInputEvent;
import im.rosetta.event.events.ServerErrorEvent;
import im.rosetta.event.events.ServerStartEvent;
import im.rosetta.event.events.ServerStopEvent;
import io.orprotocol.Server;
import io.orprotocol.ServerListener;
import io.orprotocol.client.Client;
import io.orprotocol.packet.Packet;
/**
* Адаптер нужный для трансляции событий протокола в события приложения (EventManager)
*/
public class ServerAdapter implements ServerListener {
private EventManager eventManager;
public ServerAdapter(EventManager eventManager) {
this.eventManager = eventManager;
}
@Override
public void onServerStart(Server server) {
this.eventManager.callEvent(new ServerStartEvent(server));
}
@Override
public void onServerStop(Server server) {
this.eventManager.callEvent(new ServerStopEvent(server));
}
@Override
public boolean onClientConnect(Server server, Client client) {
boolean cancelled = this.eventManager.callEvent(new ConnectEvent(server, client));
/**
* Если событие отменено (true), то подключение клиента будет отклонено,
* иначе (false) клиент будет успешно подключен.
* Инверсия нужна для того чтобы соответствовать логике отмены событий в ServerListener,
* который требует чтобы мы возвращали true если подключение должно быть разрешено.
*/
return !cancelled;
}
@Override
public void onClientDisconnect(Server server, Client client) {
this.eventManager.callEvent(new DisconnectEvent(server, client));
}
@Override
public void onError(Server server, Exception exception) {
this.eventManager.callEvent(new ServerErrorEvent(server, exception));
}
@Override
public boolean onPacketReceived(Server server, Client client, Packet packet) {
boolean cancelled = this.eventManager.callEvent(new PacketInputEvent(server, client, packet));
/**
* Если событие отменено (true), то пакет не будет обработан дальше,
* иначе (false) будет продолжена его обработка.
* Инверсия нужна для того чтобы соответствовать логике отмены событий в ServerListener,
* который требует чтобы мы возвращали true если пакет должен быть обработан дальше.
*/
return !cancelled;
}
}

View File

@@ -0,0 +1,112 @@
package im.rosetta.client;
import java.util.HashSet;
import java.util.List;
import im.rosetta.client.tags.ECIAuthentificate;
import io.orprotocol.ProtocolException;
import io.orprotocol.Server;
import io.orprotocol.client.Client;
import io.orprotocol.index.ClientIndexer;
import io.orprotocol.packet.Packet;
/**
* Менеджер клиентов
*/
public class ClientManager {
private Server server;
private ClientIndexer clientIndexer;
public ClientManager(Server server) {
this.server = server;
this.clientIndexer = server.getClientIndexer();
}
public Server getServer() {
return this.server;
}
public boolean isClientConnected(String publicKey) {
HashSet<Client> clients = this.clientIndexer.getClients(ECIAuthentificate.class, "publicKey", publicKey);
if(clients == null){
/**
* Нет клиентов с таким публичным ключом
*/
return false;
}
if(clients.size() <= 0){
/**
* Нет клиентов с таким публичным ключом
*/
return false;
}
/**
* Есть клиенты с таким публичным ключом
*/
return true;
}
/**
* Отправить пакет всем АВТОРИЗОВАННЫМ клиентам с публичным ключом publicKey
* @param publicKey публичный ключ получателя
* @param packet пакет для отправки
* @throws ProtocolException если произошла ошибка при отправке пакета клиенту
*/
public void sendPacketToAuthorizedPK(String publicKey, Packet packet) throws ProtocolException {
HashSet<Client> clients = this.clientIndexer.getClients(ECIAuthentificate.class, "publicKey", publicKey);
if(clients == null){
/**
* Нет клиентов с таким публичным ключом, значит отправлять некому
*/
return;
}
for(Client client : clients){
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Если клиент не авторизован, пропускаем его, он не должен получать пакеты,
* если нужно отправить пакет неавторизованному клиенту, нужно отправить его напрямую посредством client.send(packet),
* а не через этот метод
*/
continue;
}
/**
* Отправляем пакет каждому клиенту с таким публичным ключом (то есть всем его авторизованным сессиям/устройствам)
*/
client.send(packet);
}
}
/**
* Отправить пакет всем клиентам с публичными ключами из списка publicKeys
* @param publicKeys список публичных ключей получателей
* @param packet пакет для отправки
* @throws ProtocolException если произошла ошибка при отправке пакета клиенту
*/
public void sendPacketToAuthorizedPK(List<String> publicKeys, Packet packet) throws ProtocolException {
for(String publicKey : publicKeys){
this.sendPacketToAuthorizedPK(publicKey, packet);
}
}
/**
* Получить список клиентов по публичному ключу (get PublicKey clients), могут быть неавторизованные клиенты
* @param publicKey публичный ключ клиента
* @return список клиентов с таким публичным ключом, может быть пустым, если клиентов с таким публичным ключом нет
*/
public List<Client> getPKClients(String publicKey) {
HashSet<Client> clients = this.clientIndexer.getClients(ECIAuthentificate.class, "publicKey", publicKey);
if(clients == null){
/**
* Нет клиентов с таким публичным ключом
*/
return List.of();
}
return List.copyOf(clients);
}
}

View File

@@ -0,0 +1,73 @@
package im.rosetta.client;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import im.rosetta.client.tags.ECIAuthentificate;
import io.orprotocol.client.Client;
/**
* Отвечает за подписки на онлайн статус пользователей
* Каждый пользователь может подписаться на онлайн статус других пользователей
* и получать обновления об их статусе в реальном времени
*/
public class OnlineManager {
private HashMap<Client, HashSet<String>> onlineSubscriptions;
public OnlineManager() {
this.onlineSubscriptions = new HashMap<>();
}
/**
* Подписывает клиента на онлайн статус другого пользователя по его публичному ключу
* @param client клиент, который подписывается
* @param targetPublicKey публичный ключ пользователя, на которого подписываются
*/
public void subscribe(Client client, String targetPublicKey) {
this.onlineSubscriptions.computeIfAbsent(client, k -> new HashSet<>())
.add(targetPublicKey);
}
/**
* Отписывает клиента от онлайн статуса другого пользователя по его публичному ключу, например при отключении клиента
* @param client клиент, который отписывается от всех (отключается)
*/
public void unsubscribeAll(Client client) {
this.onlineSubscriptions.remove(client);
}
/**
* Получает список клиентов, которые подписаны на онлайн статус пользователя с указанным публичным ключом
* @param targetPublicKey публичный ключ пользователя, чью онлайн статус интересует
* @return список клиентов, подписанных на этот публичный ключ
*/
public List<Client> getSubscribers(String targetPublicKey) {
List<Client> subscribers = new java.util.ArrayList<>();
for (var entry : this.onlineSubscriptions.entrySet()) {
Client client = entry.getKey();
HashSet<String> subscribedKeys = entry.getValue();
if (subscribedKeys.contains(targetPublicKey)) {
subscribers.add(client);
}
}
return subscribers;
}
/**
* Получает список клиентов, которые подписаны на онлайн статус пользователя, представленного данным клиентом
* @param client клиент, представляющий пользователя, чью онлайн статус интересует
* @return список клиентов, подписанных на этого пользователя
*/
public List<Client> getSubscribers(Client client) {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
return new ArrayList<>();
}
String publicKey = eciAuthentificate.getPublicKey();
return this.getSubscribers(publicKey);
}
}

View File

@@ -0,0 +1,67 @@
package im.rosetta.client.tags;
import java.util.HashMap;
import java.util.Map;
import im.rosetta.packet.runtime.HandshakeStage;
import io.orprotocol.client.ECITag;
/**
* Это вложенный обьект для клиента, содержащий информацию об аутентификации.
*/
public class ECIAuthentificate implements ECITag {
public String publicKey;
public String privateKey;
public HandshakeStage handshakeStage;
public ECIAuthentificate(String publicKey, String privateKey, HandshakeStage handshakeStage) {
this.publicKey = publicKey;
this.privateKey = privateKey;
this.handshakeStage = handshakeStage;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public HandshakeStage getHandshakeStage() {
return handshakeStage;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public void setHandshakeStage(HandshakeStage handshakeStage) {
this.handshakeStage = handshakeStage;
}
/**
* Проверяет, прошел ли клиент аутентификацию. В том числе подтвердил ли устройство.
* @return true если аутентификация пройдена, иначе false.
*/
public boolean hasAuthorized() {
return this.handshakeStage == HandshakeStage.COMPLETED;
}
/**
* Создаем индекс для быстрого поиска клиентов (индексацию реализует ORProtocol)
*/
@Override
public Map<String, Object> getIndex() {
Map<String, Object> indexes = new HashMap<>();
indexes.put("publicKey", publicKey);
return indexes;
}
}

View File

@@ -0,0 +1,41 @@
package im.rosetta.client.tags;
import io.orprotocol.client.ECITag;
public class ECIDevice implements ECITag {
public String deviceId;
public String deviceName;
public String deviceOs;
public ECIDevice(String deviceId, String deviceName, String deviceOs) {
this.deviceId = deviceId;
this.deviceName = deviceName;
this.deviceOs = deviceOs;
}
public String getDeviceId() {
return deviceId;
}
public String getDeviceName() {
return deviceName;
}
public String getDeviceOs() {
return deviceOs;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public void setDeviceOs(String deviceOs) {
this.deviceOs = deviceOs;
}
}

View File

@@ -0,0 +1,42 @@
package im.rosetta.database;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
/**
* Базовый класс для сущностей с полями
* времени создания и обновления
*/
@MappedSuperclass
public class CreateUpdateEntity {
@Column(name = "createdAt", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updatedAt", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
}

View File

@@ -0,0 +1,46 @@
package im.rosetta.database;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtil {
private static final SessionFactory sessionFactory;
static {
try {
Configuration cfg = new Configuration().configure();
String host = System.getenv("DB_HOST");
String port = System.getenv("DB_PORT");
String user = System.getenv("DB_USER");
String pass = System.getenv("DB_PASSWORD");
String name = System.getenv("DB_NAME");
String url = String.format("jdbc:postgresql://%s:%s/%s", host, port, name);
cfg.setProperty("hibernate.connection.url", url);
cfg.setProperty("hibernate.connection.username", user);
cfg.setProperty("hibernate.connection.password", pass);
sessionFactory = cfg.buildSessionFactory();
} catch (Exception e) {
e.printStackTrace();
throw new ExceptionInInitializerError("Error initializing Hibernate: " + e.getMessage());
}
}
public static SessionFactory getSessionFactory() {
return sessionFactory;
}
public static Session getCurrentSession() {
return sessionFactory.getCurrentSession();
}
public static Session openSession() {
return sessionFactory.openSession();
}
public static void shutdown() {
sessionFactory.close();
}
}

View File

@@ -0,0 +1,41 @@
package im.rosetta.database;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
public class QuerySession<T> implements AutoCloseable {
private final Session session;
private final Query<T> query;
private final Transaction tx;
public QuerySession(Session session, Query<T> query) {
this.session = session;
this.query = query;
this.tx = session.beginTransaction();
}
public Query<T> getQuery() {
return query;
}
public void commit() {
if (tx != null && tx.isActive()) {
tx.commit();
}
}
@Override
public void close() {
try {
if (tx != null && tx.isActive()) {
tx.rollback();
}
} finally {
if (session != null && session.isOpen()) {
session.close();
}
}
}
}

View File

@@ -0,0 +1,377 @@
package im.rosetta.database;
import java.util.HashMap;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
/**
* Базовый репозиторий для работы с сущностями базы данных
*/
public abstract class Repository<T> {
protected Class<T> entityClass;
public Repository(Class<T> entityClass) {
this.entityClass = entityClass;
}
/**
* Сохранение сущности в базе данных
* @param entity сущность для сохранения
* @return сохраненная сущность
*/
public T save(T entity) {
return executeInTransaction(session -> {
session.persist(entity);
return entity;
});
}
/**
* Обновление сущности в базе данных
* @param entity сущность для обновления
* @return обновленная сущность
*/
public T update(T entity) {
return executeInTransaction(session -> {
session.merge(entity);
return entity;
});
}
/**
* Удаление сущности из базы данных
* @param entity сущность для удаления
*/
public void delete(T entity) {
executeInTransaction(session -> {
session.remove(entity);
return null;
});
}
/**
* Поиск сущности по значению одного поля
* @param fieldName поле
* @param value значение
* @return найденная сущность или null
*/
public T findByField(String fieldName, Object value) {
return executeInSession(session -> {
String queryString = "FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " = :value";
return session.createQuery(queryString, entityClass)
.setParameter("value", value)
.uniqueResult();
});
}
/**
* Поиск сущности по значению одного поля с использованием оператора LIKE
* @param fieldName поле
* @param value значение
* @return найденная сущность или null
*/
public T likeSearch(String fieldName, String value) {
return executeInSession(session -> {
String queryString = "FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " LIKE :value";
return session.createQuery(queryString, entityClass)
.setParameter("value", "%" + value + "%")
.uniqueResult();
});
}
/**
* Поиск сущности по значению одного поля с использованием оператора LIKE
* @param fieldName поле
* @param value значение
* @return найденная сущность или null
*/
public List<T> likeSearchAll(String fieldName, String value) {
return executeInSession(session -> {
String queryString = "FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " LIKE :value";
return session.createQuery(queryString, entityClass)
.setParameter("value", value + "%")
.list();
});
}
/**
* Поиск сущности по значению одного поля с использованием оператора LIKE и ограничения LIMIT
* @param fieldName поле
* @param value значение
* @return найденная сущность или null
*/
public List<T> likeSearchAll(String fieldName, String value, int take) {
return executeInSession(session -> {
String queryString = "FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " LIKE :value";
return session.createQuery(queryString, entityClass)
.setParameter("value", value + "%")
.setMaxResults(take)
.list();
});
}
/**
* Поиск сущности по набору полей
* @param fields карта полей и их значений
* @return найденная сущность или null
*/
public T findByField(HashMap<String, Object> fields) {
return executeInSession(session -> {
StringBuilder queryString = new StringBuilder("FROM " + entityClass.getSimpleName() + " WHERE ");
int index = 0;
for (String fieldName : fields.keySet()) {
if (index > 0) {
queryString.append(" AND ");
}
queryString.append(fieldName).append(" = :").append(fieldName);
index++;
}
var query = session.createQuery(queryString.toString(), this.entityClass);
for (var entry : fields.entrySet()) {
query.setParameter(entry.getKey(), entry.getValue());
}
return query.uniqueResult();
});
}
/**
* Удаление сущностей по значению одного поля
* @param fieldName поле
* @param value значение
*/
public void deleteByField(String fieldName, Object value) {
executeInTransaction(session -> {
String queryString = "DELETE FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " = :value";
session.createQuery(queryString, entityClass)
.setParameter("value", value)
.executeUpdate();
return null;
});
}
/**
* Поиск всех сущностей по значению одного поля
* @param fieldName поле
* @param value значение
* @return список найденных сущностей
*/
public List<T> findAllByField(String fieldName, Object value) {
return executeInSession(session -> {
String queryString = "FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " = :value";
return session.createQuery(queryString, entityClass)
.setParameter("value", value)
.list();
});
}
/**
* Поиск всех сущностей по значению набора полей
* @param fields карта полей и их значений
* @return список найденных сущностей
*/
public List<T> findAllByField(HashMap<String, Object> fields) {
return executeInSession(session -> {
StringBuilder queryString = new StringBuilder("FROM " + entityClass.getSimpleName() + " WHERE ");
int index = 0;
for (String fieldName : fields.keySet()) {
if (index > 0) {
queryString.append(" AND ");
}
queryString.append(fieldName).append(" = :").append(fieldName);
index++;
}
var query = session.createQuery(queryString.toString(), this.entityClass);
for (var entry : fields.entrySet()) {
query.setParameter(entry.getKey(), entry.getValue());
}
return query.list();
});
}
/**
* Поиск всех сущностей, тяжелый метод, лучше не выполнять без необходимости
* @return список всех сущностей
*/
public List<T> findAll() {
return executeInSession(session -> {
String queryString = "FROM " + entityClass.getSimpleName();
return session.createQuery(queryString, entityClass).list();
});
}
/**
* Подсчет всех сущностей в таблице
* @return количество сущностей
*/
public long countAll() {
return executeInSession(session -> {
String queryString = "SELECT COUNT(*) FROM " + entityClass.getSimpleName();
return session.createQuery(queryString, Long.class).uniqueResult();
});
}
/**
* Подсчет сущностей по значению одного поля
* @param fieldName поле
* @param value значение
* @return количество сущностей
*/
public long countByField(String fieldName, Object value) {
return executeInSession(session -> {
String queryString = "SELECT COUNT(*) FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " = :value";
return session.createQuery(queryString, Long.class)
.setParameter("value", value)
.uniqueResult();
});
}
/**
* Выполняет запрос с параметрами и возвращает список сущностей
* @param queryString SQL запрос
* @param parameters параметры запроса
* @param noResultType если true, то не указывать тип результата в запросе, используется для запросов типа UPDATE и DELETE
* @return список сущностей
*/
@SuppressWarnings("deprecation")
public QuerySession<T> buildQuery(String queryString, HashMap<String, Object> parameters, boolean noResultType) {
Session session = HibernateUtil.openSession();
try {
Query<T> query;
if(noResultType) {
query = session.createQuery(queryString);
} else {
query = session.createQuery(queryString, entityClass);
}
for (var entry : parameters.entrySet()) {
query.setParameter(entry.getKey(), entry.getValue());
}
return new QuerySession<>(session, query);
} catch (Exception e) {
session.close();
throw e;
}
}
/**
* Выполняет запрос с параметрами и возвращает список сущностей, тип результата указывается автоматически, используется для запросов типа SELECT
* @param queryString SQL запрос
* @param parameters параметры запроса
* @return список сущностей
*/
public QuerySession<T> buildQuery(String queryString, HashMap<String, Object> parameters) {
return buildQuery(queryString, parameters, false);
}
/**
* Подсчет сущностей по набору полей
* @param fields карта полей и их значений
* @return количество сущностей
*/
public long countByField(HashMap<String, Object> fields) {
return executeInSession(session -> {
StringBuilder queryString = new StringBuilder("SELECT COUNT(*) FROM " + entityClass.getSimpleName() + " WHERE ");
int index = 0;
for (String fieldName : fields.keySet()) {
if (index > 0) {
queryString.append(" AND ");
}
queryString.append(fieldName).append(" = :").append(fieldName);
index++;
}
var query = session.createQuery(queryString.toString(), Long.class);
for (var entry : fields.entrySet()) {
query.setParameter(entry.getKey(), entry.getValue());
}
return query.uniqueResult();
});
}
/**
* Обновление полей сущности по заданным условиям
* @param fieldsToUpdate поля для обновления
* @param whereFields условия для выбора сущностей
*/
public void update(HashMap<String, Object> fieldsToUpdate, HashMap<String, Object> whereFields) {
executeInTransaction(session -> {
StringBuilder queryString = new StringBuilder("UPDATE " + entityClass.getSimpleName() + " SET ");
int index = 0;
for (String fieldName : fieldsToUpdate.keySet()) {
if (index > 0) {
queryString.append(", ");
}
queryString.append(fieldName).append(" = :").append(fieldName);
index++;
}
queryString.append(" WHERE ");
index = 0;
for (String fieldName : whereFields.keySet()) {
if (index > 0) {
queryString.append(" AND ");
}
/**
* Добавляем префикс where_ к параметрам условия WHERE,
* чтобы избежать конфликтов имен с параметрами SET
*/
queryString.append(fieldName).append(" = :where_").append(fieldName);
index++;
}
var query = session.createQuery(queryString.toString(), this.entityClass);
for (var entry : fieldsToUpdate.entrySet()) {
query.setParameter(entry.getKey(), entry.getValue());
}
for (var entry : whereFields.entrySet()) {
/**
* Устанавливаем параметры с префиксом where_ для условий WHERE,
* чтобы избежать конфликтов имен с параметрами SET
*/
query.setParameter("where_" + entry.getKey(), entry.getValue());
}
query.executeUpdate();
return null;
});
}
protected <R> R executeInTransaction(TransactionCallback<R> callback) {
Session session = HibernateUtil.openSession();
Transaction transaction = null;
try {
transaction = session.beginTransaction();
R result = callback.execute(session);
transaction.commit();
return result;
} catch (Exception e) {
if (transaction != null) {
transaction.rollback();
}
throw new RuntimeException("Transaction failed: " + e.getMessage(), e);
} finally {
session.close();
}
}
protected <R> R executeInSession(SessionCallback<R> callback) {
try (Session session = HibernateUtil.openSession()) {
return callback.execute(session);
}
}
/**
* Функциональный интерфейс для обратного вызова транзакции.
* (Коллбэки)
*/
@FunctionalInterface
protected interface TransactionCallback<R> {
R execute(Session session);
}
@FunctionalInterface
protected interface SessionCallback<R> {
R execute(Session session);
}
}

View File

@@ -0,0 +1,27 @@
package im.rosetta.database.converters;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
@Override
public String convertToDatabaseColumn(List<String> attribute) {
if (attribute == null || attribute.isEmpty()) {
return "";
}
return String.join(",", attribute);
}
@Override
public List<String> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isBlank()) {
return new ArrayList<>();
}
return new ArrayList<>(Arrays.asList(dbData.split(",")));
}
}

View File

@@ -0,0 +1,85 @@
package im.rosetta.database.entity;
import im.rosetta.database.CreateUpdateEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* Сущность для буфера сообщений, которые не были доставлены получателю, например,
* из-за того, что он был оффлайн, а так же для синхронизации сообщений
* между устройствами одного пользователя.
* Сообщения в буфере хранятся в виде сериализованных пакетов.
* Когда получатель становится онлайн, сервер пытается доставить ему все сообщения из буфера.
*/
@Entity
@Table(name = "packet_buffer")
public class Buffer extends CreateUpdateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "source")
private String from;
@Column(name = "destination")
private String to;
@Column(name = "packetId")
private int packetId;
@Column(name = "packet", columnDefinition = "bytea")
private byte[] packet;
@Column(name = "timestamp")
private Long timestamp;
public Long getId() {
return id;
}
public String getFrom() {
return from;
}
public String getTo() {
return to;
}
public byte[] getPacket() {
return packet;
}
public Long getTimestamp() {
return timestamp;
}
public void setFrom(String from) {
this.from = from;
}
public void setTo(String to) {
this.to = to;
}
public void setPacket(byte[] packet) {
this.packet = packet;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public int getPacketId() {
return packetId;
}
public void setPacketId(int packetId) {
this.packetId = packetId;
}
}

View File

@@ -0,0 +1,82 @@
package im.rosetta.database.entity;
import im.rosetta.database.CreateUpdateEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
@Entity
@Table(name = "devices", indexes = {
@Index(name = "idx_public_key", columnList = "publicKey, deviceId", unique = true)
})
public class Device extends CreateUpdateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "publicKey", nullable = false)
private String publicKey;
@Column(name = "deviceId", nullable = false)
private String deviceId;
@Column(name = "deviceName", nullable = false)
private String deviceName;
@Column(name = "deviceOs", nullable = false)
private String deviceOs;
/**
* Время завершения сессии устройства
*/
@Column(name = "leaveTime", nullable = true, columnDefinition = "bigint default 0")
private Long leaveTime;
public Long getId() {
return id;
}
public String getPublicKey() {
return publicKey;
}
public String getDeviceId() {
return deviceId;
}
public String getDeviceName() {
return deviceName;
}
public String getDeviceOs() {
return deviceOs;
}
public Long getLeaveTime() {
return leaveTime;
}
public void setLeaveTime(Long leaveTime) {
this.leaveTime = leaveTime;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public void setDeviceOs(String deviceOs) {
this.deviceOs = deviceOs;
}
}

View File

@@ -0,0 +1,67 @@
package im.rosetta.database.entity;
import java.util.ArrayList;
import java.util.List;
import im.rosetta.database.CreateUpdateEntity;
import im.rosetta.database.converters.StringListConverter;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* Сущность для групповых чатов.
*/
@Entity
@Table(name = "groups")
public class Group extends CreateUpdateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "groupId")
private String groupId;
@Convert(converter = StringListConverter.class)
@Column(name = "membersPublicKeys", nullable = false)
private List<String> membersPublicKeys = new ArrayList<>();
@Convert(converter = StringListConverter.class)
@Column(name = "bannedPublicKeys", nullable = false)
private List<String> bannedPublicKeys = new ArrayList<>();
public Long getId() {
return id;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public List<String> getMembersPublicKeys() {
return membersPublicKeys;
}
public void setMembersPublicKeys(List<String> membersPublicKeys) {
this.membersPublicKeys = membersPublicKeys;
}
public List<String> getBannedPublicKeys() {
return bannedPublicKeys;
}
public void setBannedPublicKeys(List<String> bannedPublicKeys) {
this.bannedPublicKeys = bannedPublicKeys;
}
}

View File

@@ -0,0 +1,100 @@
package im.rosetta.database.entity;
import im.rosetta.database.CreateUpdateEntity;
import im.rosetta.database.converters.StringListConverter;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "users", indexes = {
@Index(name = "idx_users_publickey", columnList = "publicKey", unique = true)
})
public class User extends CreateUpdateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "title")
private String title;
@Column(name = "verified", nullable = false)
private int verified;
@Column(name = "privateKey", nullable = false)
private String privateKey;
@Column(name = "publicKey", nullable = false, unique = true)
private String publicKey;
@Convert(converter = StringListConverter.class)
@Column(name = "notificationsTokens", nullable = false)
private List<String> notificationsTokens = new ArrayList<>();
public Long getId() {
return id;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public int getVerified() {
return verified;
}
public void setVerified(int verified) {
this.verified = verified;
}
public List<String> getNotificationsTokens() {
return notificationsTokens;
}
public void setNotificationsTokens(List<String> notificationsTokens) {
this.notificationsTokens = notificationsTokens;
}
}

View File

@@ -0,0 +1,12 @@
package im.rosetta.database.repository;
import im.rosetta.database.Repository;
import im.rosetta.database.entity.Buffer;
public class BufferRepository extends Repository<Buffer> {
public BufferRepository() {
super(Buffer.class);
}
}

View File

@@ -0,0 +1,47 @@
package im.rosetta.database.repository;
import java.util.List;
import im.rosetta.database.Repository;
import im.rosetta.database.entity.Device;
import im.rosetta.database.entity.User;
public class DeviceRepository extends Repository<Device> {
public DeviceRepository() {
super(Device.class);
}
/**
* Найти все устройства пользователя
* @param user пользователь
* @return список устройств
*/
public List<Device> findAll(User user) {
return this.findAllByField("publicKey", user.getPublicKey());
}
/**
* Считает количество устройств пользователя
* @param user пользователь
* @return количество устройств
*/
public long countUserDevices(User user) {
return this.countByField("publicKey", user.getPublicKey());
}
/**
* Обновляет время последней активности устройства
* @param deviceId ID устройства
*/
public void updateDeviceLeaveTime(String deviceId) {
Device device = this.findByField("deviceId", deviceId);
if(device == null) {
return;
}
device.setLeaveTime(System.currentTimeMillis());
this.update(device);
}
}

View File

@@ -0,0 +1,118 @@
package im.rosetta.database.repository;
import java.util.ArrayList;
import java.util.List;
import im.rosetta.database.Repository;
import im.rosetta.database.entity.Group;
public class GroupRepository extends Repository<Group> {
public GroupRepository() {
super(Group.class);
}
/**
* Найти участников группы по groupId
* @param groupId ID группы
* @return список публичных ключей участников группы
*/
public List<String> findGroupMembers(String groupId) {
Group group = this.findByField("groupId", groupId);
if(group == null) {
return new ArrayList<>();
}
return group.getMembersPublicKeys();
}
/**
* Создать группу с заданным id и создателем, который будет единственным участником группы
* @param groupId ID группы
* @param creatorPublicKey публичный ключ создателя группы, который будет единственным участником группы при создании
*/
public void createGroup(String groupId, String creatorPublicKey) {
Group group = new Group();
group.setGroupId(groupId);
List<String> membersPublicKeys = new ArrayList<>();
membersPublicKeys.add(creatorPublicKey);
group.setMembersPublicKeys(membersPublicKeys);
this.save(group);
}
/**
* Получить группу по id
* @param groupId ID группы
* @return группа с заданным id, или null, если группа не найдена
*/
public Group getGroup(String groupId) {
return this.findByField("groupId", groupId);
}
/**
* Удалить группу по id
* @param groupId ID группы, которую нужно удалить
*/
public void removeGroup(String groupId) {
Group group = this.findByField("groupId", groupId);
if(group != null) {
this.delete(group);
}
}
/**
* Добавить участника в группу
* @param groupId ID группы, в которую нужно добавить участника
* @param memberPublicKey публичный ключ участника, которого нужно добавить в группу
*/
public void addMemberToGroup(String groupId, String memberPublicKey) {
Group group = this.findByField("groupId", groupId);
if(group != null) {
List<String> membersPublicKeys = group.getMembersPublicKeys();
if(!membersPublicKeys.contains(memberPublicKey)) {
membersPublicKeys.add(memberPublicKey);
group.setMembersPublicKeys(membersPublicKeys);
this.update(group);
}
}
}
/**
* Удалить участника из группы
* @param groupId ID группы, из которой нужно удалить участника
* @param memberPublicKey публичный ключ участника, которого нужно удалить из группы
*/
public void removeMemberFromGroup(String groupId, String memberPublicKey) {
Group group = this.findByField("groupId", groupId);
if(group != null) {
List<String> membersPublicKeys = group.getMembersPublicKeys();
if(membersPublicKeys.contains(memberPublicKey)) {
membersPublicKeys.remove(memberPublicKey);
group.setMembersPublicKeys(membersPublicKeys);
this.update(group);
}
}
}
/**
* Забанить участника в группе, добавив его публичный ключ в список забаненных публичных ключей группы
* @param groupId ID группы, в которой нужно забанить участника
* @param memberPublicKey публичный ключ участника, которого нужно забанить в группе
*/
public void banMemberInGroup(String groupId, String memberPublicKey) {
Group group = this.findByField("groupId", groupId);
if(group != null) {
List<String> bannedPublicKeys = group.getBannedPublicKeys();
List<String> membersPublicKeys = group.getMembersPublicKeys();
if(membersPublicKeys.contains(memberPublicKey)) {
membersPublicKeys.remove(memberPublicKey);
group.setMembersPublicKeys(membersPublicKeys);
}
if(!bannedPublicKeys.contains(memberPublicKey)) {
bannedPublicKeys.add(memberPublicKey);
group.setBannedPublicKeys(bannedPublicKeys);
}
this.update(group);
}
}
}

View File

@@ -0,0 +1,13 @@
package im.rosetta.database.repository;
import im.rosetta.database.Repository;
import im.rosetta.database.entity.User;
public class UserRepository extends Repository<User> {
public UserRepository() {
super(User.class);
}
}

View File

@@ -0,0 +1,17 @@
package im.rosetta.event;
public interface Cancelable {
/**
* Отменено ли событие
* @return true, если событие отменено
*/
boolean isCanceled();
/**
* Установить отмену события
* @param canceled true, если событие должно быть отменено
*/
void setCanceled(boolean canceled);
}

View File

@@ -0,0 +1,18 @@
package im.rosetta.event;
public class Event {
private String name;
public Event() {}
public String getEventName() {
if(this.name == null) {
this.name = this.getClass().getSimpleName();
}
return this.name;
}
}

View File

@@ -0,0 +1,13 @@
package im.rosetta.event;
public class EventException extends Exception {
public EventException(String message) {
super(message);
}
public EventException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,12 @@
package im.rosetta.event;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventHandler {
}

View File

@@ -0,0 +1,119 @@
package im.rosetta.event;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Менеджер событий
*/
public class EventManager {
private Set<Listener> listeners;
public EventManager() {
this.listeners = new HashSet<>();
}
/**
* Регистрация слушателя событий
* @param listener Слушатель событий
*/
public void registerListener(Listener listener) {
this.listeners.add(listener);
}
/**
* Удаление слушателя событий
* @param listener Слушатель событий
*/
public void unregisterListener(Listener listener) {
this.listeners.remove(listener);
}
/**
* Получить все зарегистрированные слушатели событий
* @return Множество слушателей событий
*/
public Set<Listener> getListeners() {
return this.listeners;
}
/**
* Вызывает событие и обрабатывает исключения
* @param event Событие для вызова
* @return true, если событие отменено
*/
public boolean callEvent(Event event) {
try {
return fireEvent(event);
} catch (EventException e) {
e.printStackTrace();
return false;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* Запускает событие, уведомляя всех зарегистрированных слушателей, возвращает true если событие отменено
* @param event Событие для запуска
* @return true, если событие отменено
* @throws EventException Ошибка при обработке события
* @throws Exception Общая ошибка при вызове метода
*/
private boolean fireEvent(Event event) throws EventException {
for(Listener listener : this.listeners) {
/**
* Получаем все методы в Listener с аннотацией @EventHandler
*/
List<Method> methods = getMethodsWithAnnotation(listener.getClass(), EventHandler.class);
for(Method method : methods) {
/**
* Проверяем, что метод принимает один параметр того же типа, что и событие
*/
if(method.getParameterCount() == 1
&& method.getParameterTypes()[0].isAssignableFrom(event.getClass())) {
/**
* Если параметры совпадают и они одного типа - вызываем событие
*/
try{
method.setAccessible(true);
method.invoke(listener, event);
} catch (Exception e) {
throw new EventException("Error while invoking event handler method: " + method.getName(), e);
}
/**
* Если событие отменяемое - проверяем его статус
*/
if(event instanceof Cancelable) {
Cancelable cancelableEvent = (Cancelable) event;
if(cancelableEvent.isCanceled()) {
return true;
}
}
continue;
}
}
}
return false;
}
private List<Method> getMethodsWithAnnotation(Class<?> clazz, Class<? extends Annotation> annotation) {
List<Method> methods = new ArrayList<>();
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(annotation)) {
methods.add(method);
}
}
return methods;
}
}

View File

@@ -0,0 +1,25 @@
package im.rosetta.event;
/**
* Приоритет события
* Указывает на то, в каком порядке обработаются два одинаковых события
*/
public enum EventPriority {
LOW(0),
MEDIUM(1),
HIGH(2);
private final int priority;
EventPriority(int priority) {
this.priority = priority;
}
/**
* Получить приоритет
* @return Приоритет события
*/
public int getPriority() {
return priority;
}
}

View File

@@ -0,0 +1,10 @@
package im.rosetta.event;
/**
* Слушатель событий
*/
public interface Listener {
/**
* Пустой интерфейс для обозначения слушателя событий
*/
}

View File

@@ -0,0 +1,41 @@
package im.rosetta.event.events;
import im.rosetta.event.Cancelable;
import im.rosetta.event.Event;
import io.orprotocol.Server;
import io.orprotocol.client.Client;
/**
* Событие подключения клиента к серверу.
*/
public class ConnectEvent extends Event implements Cancelable {
private boolean canceled = false;
private Server server;
private Client client;
public ConnectEvent(Server server, Client client) {
this.server = server;
this.client = client;
}
@Override
public boolean isCanceled() {
return this.canceled;
}
@Override
public void setCanceled(boolean canceled) {
this.canceled = canceled;
}
public Server getServer() {
return this.server;
}
public Client getClient() {
return this.client;
}
}

View File

@@ -0,0 +1,29 @@
package im.rosetta.event.events;
import im.rosetta.event.Event;
import io.orprotocol.Server;
import io.orprotocol.client.Client;
/**
* Событие отключения клиента от сервера.
*/
public class DisconnectEvent extends Event {
private Server server;
private Client client;
public DisconnectEvent(Server server, Client client) {
this.server = server;
this.client = client;
}
public Server getServer() {
return this.server;
}
public Client getClient() {
return this.client;
}
}

View File

@@ -0,0 +1,48 @@
package im.rosetta.event.events;
import im.rosetta.event.Cancelable;
import im.rosetta.event.Event;
import io.orprotocol.Server;
import io.orprotocol.client.Client;
import io.orprotocol.packet.Packet;
/**
* Событие входящего пакета от клиента.
*/
public class PacketInputEvent extends Event implements Cancelable {
private boolean canceled = false;
private Server server;
private Client client;
private Packet packet;
public PacketInputEvent(Server server, Client client, Packet packet) {
this.server = server;
this.client = client;
this.packet = packet;
}
@Override
public boolean isCanceled() {
return this.canceled;
}
@Override
public void setCanceled(boolean canceled) {
this.canceled = canceled;
}
public Server getServer() {
return this.server;
}
public Client getClient() {
return this.client;
}
public Packet getPacket() {
return this.packet;
}
}

View File

@@ -0,0 +1,28 @@
package im.rosetta.event.events;
import im.rosetta.event.Event;
import io.orprotocol.Server;
/**
* Событие ошибки сервера.
*/
public class ServerErrorEvent extends Event {
private Exception exception;
private Server server;
public ServerErrorEvent(Server server, Exception exception) {
this.server = server;
this.exception = exception;
}
public Exception getException() {
return this.exception;
}
public Server getServer() {
return this.server;
}
}

View File

@@ -0,0 +1,22 @@
package im.rosetta.event.events;
import im.rosetta.event.Event;
import io.orprotocol.Server;
/**
* Событие запуска сервера.
*/
public class ServerStartEvent extends Event {
private Server server;
public ServerStartEvent(Server server) {
this.server = server;
}
public Server getServer() {
return this.server;
}
}

View File

@@ -0,0 +1,22 @@
package im.rosetta.event.events;
import im.rosetta.event.Event;
import io.orprotocol.Server;
/**
* Событие остановки сервера.
*/
public class ServerStopEvent extends Event {
private Server server;
public ServerStopEvent(Server server) {
this.server = server;
}
public Server getServer() {
return this.server;
}
}

View File

@@ -0,0 +1,58 @@
package im.rosetta.event.events.handshake;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.client.tags.ECIDevice;
import im.rosetta.event.Cancelable;
import im.rosetta.event.Event;
import io.orprotocol.client.Client;
/**
* Базовое событие хэндшейка
*/
public class BaseHandshakeEvent extends Event implements Cancelable {
private String publicKey;
private String privateKey;
private ECIDevice device;
private ECIAuthentificate eciAuthentificate;
private Client client;
private boolean canceled = false;
public BaseHandshakeEvent(String publicKey, String privateKey, ECIDevice device, ECIAuthentificate eciAuthentificate, Client client) {
this.publicKey = publicKey;
this.privateKey = privateKey;
this.device = device;
this.eciAuthentificate = eciAuthentificate;
this.client = client;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public ECIDevice getDevice() {
return device;
}
public ECIAuthentificate getEciAuthentificate() {
return eciAuthentificate;
}
public Client getClient() {
return client;
}
public void setCanceled(boolean canceled) {
this.canceled = canceled;
}
public boolean isCanceled() {
return canceled;
}
}

View File

@@ -0,0 +1,16 @@
package im.rosetta.event.events.handshake;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.client.tags.ECIDevice;
import io.orprotocol.client.Client;
public class HandshakeCompletedEvent extends BaseHandshakeEvent {
public HandshakeCompletedEvent(String publicKey, String privateKey, ECIDevice device, ECIAuthentificate eciAuthentificate, Client client) {
super(publicKey, privateKey, device, eciAuthentificate, client);
}
}

View File

@@ -0,0 +1,18 @@
package im.rosetta.event.events.handshake;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.client.tags.ECIDevice;
import io.orprotocol.client.Client;
/**
* Вызывается когда устройство клиента нуждается в подтверждении
* пользователем с другого устрйоства для завершения хэндшейка.
*/
public class HandshakeDeviceConfirmEvent extends BaseHandshakeEvent {
public HandshakeDeviceConfirmEvent(String publicKey, String privateKey, ECIDevice device, ECIAuthentificate eciAuthentificate, Client client) {
super(publicKey, privateKey, device, eciAuthentificate, client);
}
}

View File

@@ -0,0 +1,15 @@
package im.rosetta.event.events.handshake;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.client.tags.ECIDevice;
import io.orprotocol.client.Client;
public class HandshakeFailedEvent extends BaseHandshakeEvent {
public HandshakeFailedEvent(String publicKey, String privateKey, ECIDevice eciDevice, ECIAuthentificate eciAuthentificate,
Client client) {
super(publicKey, privateKey, eciDevice, eciAuthentificate, client);
}
}

View File

@@ -0,0 +1,12 @@
package im.rosetta.exception;
/**
* Выбрасывается когда файл конфигурации не найден
*/
public class ConfigurationException extends Exception {
public ConfigurationException(String message){
super(message);
}
}

View File

@@ -0,0 +1,9 @@
package im.rosetta.exception;
public class UnauthorizedExeception extends Exception {
public UnauthorizedExeception(String message) {
super(message);
}
}

View File

@@ -0,0 +1,231 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.client.tags.ECIDevice;
import im.rosetta.database.entity.Device;
import im.rosetta.database.entity.User;
import im.rosetta.database.repository.BufferRepository;
import im.rosetta.database.repository.DeviceRepository;
import im.rosetta.database.repository.UserRepository;
import im.rosetta.event.EventManager;
import im.rosetta.event.events.handshake.HandshakeCompletedEvent;
import im.rosetta.event.events.handshake.HandshakeDeviceConfirmEvent;
import im.rosetta.event.events.handshake.HandshakeFailedEvent;
import im.rosetta.packet.Packet0Handshake;
import im.rosetta.packet.Packet9DeviceNew;
import im.rosetta.packet.runtime.HandshakeStage;
import im.rosetta.service.services.BufferService;
import im.rosetta.service.services.DeviceService;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.lock.Lock;
import io.orprotocol.packet.PacketExecutor;
import io.orprotocol.packet.PacketManager;
public class Executor0Handshake extends PacketExecutor<Packet0Handshake> {
private final UserRepository userRepository = new UserRepository();
private final DeviceRepository deviceRepository = new DeviceRepository();
private final DeviceService deviceService = new DeviceService(deviceRepository);
private final EventManager eventManager;
private final ClientManager clientManager;
private final BufferRepository bufferRepository = new BufferRepository();
private final BufferService bufferService;
public Executor0Handshake(EventManager eventManager, ClientManager clientManager, PacketManager packetManager) {
this.eventManager = eventManager;
this.clientManager = clientManager;
this.bufferService = new BufferService(bufferRepository, packetManager);
}
@Override
@Lock(lockFor = "publicKey")
public void onPacketReceived(Packet0Handshake handshake, Client client) throws ProtocolException {
String publicKey = handshake.getPublicKey();
String privateKey = handshake.getPrivateKey();
String deviceId = handshake.getDeviceId();
String deviceName = handshake.getDeviceName();
String deviceOs = handshake.getDeviceOs();
int protocolVersion = handshake.getProtocolVersion();
/**
* Получаем информацию об аутентификации клиента
* используя возможности ECI тэгов.
*/
ECIAuthentificate authentificate = client.getTag(ECIAuthentificate.class);
if(authentificate != null && authentificate.hasAuthorized()) {
/**
* Клиент уже авторизован, повторный хэндшейк не допускается
*/
return;
}
/**
* Проверяем корректность версии протокола
*/
if(protocolVersion != 1) {
client.disconnect(Failures.UNSUPPORTED_PROTOCOL);
return;
}
/**
* Создаем минимальную информацию об устройстве клиента
*/
ECIDevice device = new ECIDevice(deviceId, deviceName, deviceOs);
client.addTag(ECIDevice.class, device);
/**
* Проверяем есть ли такой пользователь
*/
User user = userRepository.findByField("publicKey", publicKey);
if(user == null) {
/**
* Пользователь не найден, создаем нового
*/
user = new User();
user.setPrivateKey(privateKey);
user.setPublicKey(publicKey);
user.setUsername("");
user.setTitle(publicKey.substring(0, 7));
/**
* Новый пользователь не верифицирован
*/
user.setVerified(0);
userRepository.save(user);
/**
* Ставим метку аутентификации на клиента
*/
ECIAuthentificate eciTag = new ECIAuthentificate
(publicKey, privateKey, HandshakeStage.COMPLETED);
client.addTag(ECIAuthentificate.class, eciTag);
/**
* Вызываем событие завершения хэндшейка
*/
boolean cancelled = this.eventManager.callEvent(
new HandshakeCompletedEvent(publicKey, privateKey, device, eciTag, client)
);
if(cancelled) {
/**
* Событие было отменено, не даем завершить хэндшейк
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
/**
* Отправляем клиенту подтверждение успешного хэндшейка
*/
handshake.setHandshakeStage(HandshakeStage.COMPLETED);
handshake.setHeartbeatInterval(this.settings.heartbeatInterval);
client.send(handshake);
return;
}
/**
* Пользователь найден, проверяем приватный ключ
*/
if(!user.getPrivateKey().equals(privateKey)){
/**
* Приватный ключ не совпадает, отключаем клиента
*/
eventManager.callEvent(new HandshakeFailedEvent(publicKey, privateKey, device, authentificate, client));
client.disconnect(Failures.AUTHENTIFICATION_ERROR);
return;
}
long userDevicesCount = deviceRepository.countUserDevices(user);
/**
* Проверяем верифицировано ли устройство
*/
if(userDevicesCount > 0 && !deviceService.isDeviceVerifiedByUser(deviceId, user)) {
/**
* Устройство не верифицировано, нужно отправить клиента
* на подтверждение устройства
*/
handshake.setHandshakeStage(HandshakeStage.NEED_DEVICE_VERIFICATION);
handshake.setHeartbeatInterval(this.settings.heartbeatInterval);
/**
* Ставим метку аутентификации на клиента
*/
ECIAuthentificate eciTag = new ECIAuthentificate
(publicKey, privateKey, HandshakeStage.NEED_DEVICE_VERIFICATION);
client.addTag(ECIAuthentificate.class, eciTag);
/**
* Вызываем событие подтверждения устройства
*/
this.eventManager.callEvent(
new HandshakeDeviceConfirmEvent(publicKey, privateKey, device, authentificate, client)
);
/**
* Отправляем клиенту информацию о необходимости
* подтверждения устройства
*/
client.send(handshake);
/**
* Уведомляем все авторизованные устройства пользователя о том, что нужно подтвердить новое устройство
*/
Packet9DeviceNew newDevicePacket = new Packet9DeviceNew();
newDevicePacket.setDeviceId(deviceId);
newDevicePacket.setDeviceName(deviceName);
newDevicePacket.setDeviceOs(deviceOs);
newDevicePacket.setIpAddress(client.getSocket().getRemoteSocketAddress().getAddress().getHostAddress());
clientManager.sendPacketToAuthorizedPK(publicKey, newDevicePacket);
/**
* Сбрасываем клиенту все старые подтверждения устройств, чтобы исключить спам запросами
*/
this.bufferService.deletePacketsFromBuffer(publicKey, newDevicePacket, 0);
/**
* Кладем пакет в очередь на все устройства пользователя,
* чтобы если в момент отправки этого пакета какое-то устройство было не онлайн,
* то когда оно зайдет в сеть, то получит этот пакет и сможет отреагировать на него,
* показав пользователю уведомление о том, что нужно подтвердить новое устройство
*/
this.bufferService.pushPacketToBuffer("server", publicKey, newDevicePacket);
return;
}
if(userDevicesCount == 0) {
/**
* Это первое устройство пользователя, сохраняем его
* как верифицированное
*/
Device newDevice = new Device();
newDevice.setDeviceId(deviceId);
newDevice.setDeviceName(deviceName);
newDevice.setDeviceOs(deviceOs);
newDevice.setPublicKey(publicKey);
newDevice.setLeaveTime(System.currentTimeMillis());
deviceRepository.save(newDevice);
}
/**
* Ставим метку аутентификации на клиента
*/
ECIAuthentificate eciTag = new ECIAuthentificate
(publicKey, privateKey, HandshakeStage.COMPLETED);
client.addTag(ECIAuthentificate.class, eciTag);
/**
* Вызываем событие завершения хэндшейка
*/
boolean cancelled = this.eventManager.callEvent(
new HandshakeCompletedEvent(publicKey, privateKey, device, eciTag, client)
);
if(cancelled) {
/**
* Событие было отменено, не даем завершить хэндшейк
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
/**
* Отправляем клиенту подтверждение успешного хэндшейка
*/
handshake.setHandshakeStage(HandshakeStage.COMPLETED);
handshake.setHeartbeatInterval(this.settings.heartbeatInterval);
client.send(handshake);
}
}

View File

@@ -0,0 +1,43 @@
package im.rosetta.executors;
import java.util.Arrays;
import java.util.List;
import im.rosetta.packet.Packet10RequestUpdate;
import im.rosetta.util.RandomUtil;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
/**
* Исполнитель по своей логике идентичен Executor15RequestTransport,
* но код продублирован специально, чтобы не размазывать
* его например в Dispatcher. Так читать удобнее
*/
public class Executor10RequestUpdate extends PacketExecutor<Packet10RequestUpdate> {
@Override
public void onPacketReceived(Packet10RequestUpdate packet, Client client) throws Exception, ProtocolException {
/**
* Обратите внимание этот пакет в отличии от Packet15RequestTransport
* не требует авторизации. Это сделано на те случаи когда приложение
* обновить нужно, а авторизоваться не получается (например если
* авторизация сломалась)
*/
/**
* Если пользователь авторизован, выбираем случайный сервер обновлений и
* заполняем им пакет
*
* TODO: Логика проверки на доступность (health)
*/
List<String> cdnServers = Arrays.asList(System.getenv("SDU_SERVERS").split(","));
String selectedServer = cdnServers.get(RandomUtil.randomBetween(0, cdnServers.size() - 1));
packet.setServer(selectedServer);
/**
* Сервер выбран, отправляем готовый пакет клиенту
*/
client.send(packet);
}
}

View File

@@ -0,0 +1,73 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet11Typeing;
import im.rosetta.service.dispatch.MessageDispatcher;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
import io.orprotocol.packet.PacketManager;
public class Executor11Typeing extends PacketExecutor<Packet11Typeing> {
private final MessageDispatcher messageDispatcher;
public Executor11Typeing(ClientManager clientManager, PacketManager packetManager) {
this.messageDispatcher = new MessageDispatcher(clientManager, packetManager);
}
@Override
public void onPacketReceived(Packet11Typeing packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
String fromPublicKey = packet.getFromPublicKey();
String toPublicKey = packet.getToPublicKey();
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Если пользователь не авторизован он не может отправлять пакет печати
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
if(!eciAuthentificate.getPublicKey().equals(fromPublicKey)){
/**
* Если клиент пытается отправить сообщение от отправителя,
* которым он не является
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
if(fromPublicKey.equals(toPublicKey)){
/**
* Отправка пакета печати самому себе, не кикаем пользователя, так как это ни на что не
* влияет, просто ничего не делаем
*/
return;
}
/**
* Удаляем приватный ключ чтобы не показать его оппоненту
*/
packet.setPrivateKey("");
if(toPublicKey.startsWith("#group:")){
/**
* Пакет печати отправляется в группу, отправляем всем участникам
*/
this.messageDispatcher.sendGroup(packet, client, eciAuthentificate);
}else{
/**
* Пакет печати отправляется обычному оппоненту (пользователь),
* отправляем его, при этом выключаем буфферизацию, потому что пакет печати действителен
* только "здесь и сейчас", его не нужно видеть офлайн пользователям
*/
this.messageDispatcher.sendPeer(packet, client, false);
}
}
}

View File

@@ -0,0 +1,42 @@
package im.rosetta.executors;
import java.util.Arrays;
import java.util.List;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet15RequestTransport;
import im.rosetta.util.RandomUtil;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor15RequestTransport extends PacketExecutor<Packet15RequestTransport> {
@Override
public void onPacketReceived(Packet15RequestTransport packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Пользователь не авторизован, но запросил транспортный сервер - это неправильное поведение
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
/**
* Если пользователь авторизован, выбираем случайный транспортный сервер и
* заполняем им пакет
*
* TODO: Логика проверки на доступность (health)
*/
List<String> cdnServers = Arrays.asList(System.getenv("CDN_SERVERS").split(","));
String selectedServer = cdnServers.get(RandomUtil.randomBetween(0, cdnServers.size() - 1));
packet.setServer(selectedServer);
/**
* Сервер выбран, отправляем готовый пакет клиенту
*/
client.send(packet);
}
}

View File

@@ -0,0 +1,47 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.User;
import im.rosetta.database.repository.UserRepository;
import im.rosetta.packet.Packet16PushNotification;
import im.rosetta.service.services.UserService;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor16PushNotification extends PacketExecutor<Packet16PushNotification> {
private final UserRepository userRepository = new UserRepository();
private final UserService userService = new UserService(userRepository);
@Override
public void onPacketReceived(Packet16PushNotification packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Клиент не авторизован, нельзя подписывать его на уведомления
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String notificationToken = packet.getNotificationToken();
if(notificationToken.isEmpty()){
/**
* Клиент прислал пустой токен, отписывать его от уведомлений не нужно, а подписывать бессмысленно, просто игнорируем этот пакет
*/
return;
}
User user = userService.fromClient(client);
switch (packet.getAction()) {
case SUBSCRIBE:
userService.subscribeToPushNotifications(user, notificationToken);
break;
case UNSUBSCRIBE:
userService.unsubscribeFromPushNotifications(user, notificationToken);
break;
}
}
}

View File

@@ -0,0 +1,36 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.repository.GroupRepository;
import im.rosetta.packet.Packet17GroupCreate;
import im.rosetta.util.RandomUtil;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor17GroupCreate extends PacketExecutor<Packet17GroupCreate> {
private final GroupRepository groupRepository = new GroupRepository();
@Override
public void onPacketReceived(Packet17GroupCreate packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Клиент не авторизован, он не может создавать группы
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String groupId = RandomUtil.randomString(16);
this.groupRepository.createGroup(groupId, eciAuthentificate.getPublicKey());
/**
* Отправляем клиенту ид созданной группы
*/
packet.setGroupId(groupId);
client.send(packet);
}
}

View File

@@ -0,0 +1,58 @@
package im.rosetta.executors;
import java.util.ArrayList;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.Group;
import im.rosetta.database.repository.GroupRepository;
import im.rosetta.packet.Packet18GroupInfo;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor18GroupInfo extends PacketExecutor<Packet18GroupInfo> {
private final GroupRepository groupRepository = new GroupRepository();
@Override
public void onPacketReceived(Packet18GroupInfo packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Клиент не авторизован, он не может запрашивать информацию о группах
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String groupId = packet.getGroupId();
Group group = this.groupRepository.getGroup(groupId);
if(group == null || group.getMembersPublicKeys().size() <= 0) {
/**
* Если сервер возвращает пустой список участников,
* значит группы не существует, потому что
* пустая группа быть не может, так как они автоматически
* удаляются при выходе последнего участника
*/
packet.setMembersPKs(new ArrayList<>());
client.send(packet);
return;
}
if(!group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey())){
/**
* Клиент не является участником группы, значит его может быть
* исключили, возвращаем пустую информацию как будто группы нет.
*/
packet.setMembersPKs(new ArrayList<>());
client.send(packet);
return;
}
/**
* Отправляем клиенту список участников группы
*/
packet.setMembersPKs(group.getMembersPublicKeys());
client.send(packet);
}
}

View File

@@ -0,0 +1,57 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.Group;
import im.rosetta.database.repository.GroupRepository;
import im.rosetta.packet.Packet19GroupInviteInfo;
import im.rosetta.packet.runtime.NetworkGroupStatus;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor19GroupInviteInfo extends PacketExecutor<Packet19GroupInviteInfo> {
private final GroupRepository groupRepository = new GroupRepository();
@Override
public void onPacketReceived(Packet19GroupInviteInfo packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Клиент не авторизован, он не может запрашивать информацию о приглашениях
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String groupId = packet.getGroupId();
Group group = this.groupRepository.getGroup(groupId);
if(group == null){
/**
* Группы не существует, возвращаем клиенту статус INVALID
*/
packet.setStatus(NetworkGroupStatus.INVALID);
client.send(packet);
return;
}
if(group.getBannedPublicKeys().contains(eciAuthentificate.getPublicKey())){
/**
* Клиент забанен в группе, возвращаем клиенту статус BANNED
*/
packet.setStatus(NetworkGroupStatus.BANNED);
client.send(packet);
return;
}
/**
* Отправляем клиенту информацию о количестве участников и статусе, является ли
* пользователь участником группы или нет
*/
int membersCount = group.getMembersPublicKeys().size();
boolean isMember = group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey());
packet.setMembersCount(membersCount);
packet.setStatus(isMember ? NetworkGroupStatus.JOINED : NetworkGroupStatus.NOT_JOINED);
client.send(packet);
}
}

View File

@@ -0,0 +1,180 @@
package im.rosetta.executors;
import java.util.Arrays;
import java.util.HashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.User;
import im.rosetta.database.repository.UserRepository;
import im.rosetta.packet.Packet1UserInfo;
import im.rosetta.packet.Packet2Result;
import im.rosetta.packet.runtime.ResultCode;
import im.rosetta.service.services.UserService;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor1UserInfo extends PacketExecutor<Packet1UserInfo> {
private final UserRepository userRepository = new UserRepository();
private final UserService userService = new UserService(userRepository);
private final HashSet<String> blockedUsernames = new HashSet<>(Arrays.asList(
"user",
"admin",
"rosettasupport",
"rosettaupdates",
"freddie871",
"updates",
"deleted",
"safety",
"secure",
"rosettasafe"
));
@Override
public void onPacketReceived(Packet1UserInfo packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
String username = packet.getUsername();
String title = packet.getTitle();
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Только для авторизованных пользователей, а этот пользователь - не авторизован
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
User user = userService.fromClient(client);
if(user == null){
/**
* Пользователь с таким ключем не найден в базе,
* такого не может быть, но лучше чтобы была дополнительная проверка
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
ResultCode usernameResult = tryChangeUsername(user, username);
if(usernameResult == ResultCode.USERNAME_TAKEN){
/**
* Это имя пользователя уже занято, отправляем клиенту ошибку
*/
Packet2Result result = new Packet2Result();
result.setResultCode(ResultCode.USERNAME_TAKEN);
client.send(result);
return;
}
if(usernameResult != ResultCode.SUCCESS){
/**
* Не удалось сменить username, отправляем клиенту ошибку
*/
Packet2Result result = new Packet2Result();
result.setResultCode(ResultCode.INVALID);
client.send(result);
return;
}
ResultCode titleResult = tryChangeTitle(user, title);
if(titleResult != ResultCode.SUCCESS){
/**
* Не удалось сменить title, отправляем клиенту ошибку
*/
Packet2Result result = new Packet2Result();
result.setResultCode(ResultCode.INVALID);
client.send(result);
return;
}
/**
* Отправляем клиенту успешный результат
*/
Packet2Result result = new Packet2Result();
result.setResultCode(ResultCode.SUCCESS);
client.send(result);
}
/**
* Пробует сменить username
* @param user пользователь
* @param username имя пользователя для смены
* @return вернет false если смена прошла неудачно или true если username
* не нуждается в изменении или изменен
*/
public ResultCode tryChangeUsername(User user, String username){
String targetRegexp = "^[a-z][a-z0-9_]{4,15}$";
Pattern pattern = Pattern.compile(targetRegexp);
Matcher matcher = pattern.matcher(username);
if(user.getUsername().equalsIgnoreCase(username)){
/**
* Пользователь не меняет имя, значит операция прошла успешно,
* по крайней мере нам не нужно возвращать клиенту код ошибки
*/
return ResultCode.SUCCESS;
}
if(!matcher.matches()){
/**
* Не подходит по регулярному выражению
*/
return ResultCode.INVALID;
}
if(blockedUsernames.contains(username)){
/**
* Это имя пользователя не доступно для смены
*/
return ResultCode.INVALID;
}
if(userService.isUsernameTaken(username)){
/**
* Такое имя пользователя уже занято
*/
return ResultCode.USERNAME_TAKEN;
}
/**
* Меняем имя пользователя
*/
user.setUsername(username);
userRepository.update(user);
return ResultCode.SUCCESS;
}
/**
* Пробует сменить заголовок пользователя (title)
* @param user пользователь
* @param username имя пользователя для смены
* @return вернет false если смена прошла неудачно или true если title
* не нуждается в изменении или изменен
*/
public ResultCode tryChangeTitle(User user, String title) {
String targetRegexp = "^[a-zA-Z0-9а-яА-Я _-]{1,22}$";
Pattern pattern = Pattern.compile(targetRegexp);
Matcher matcher = pattern.matcher(title);
if(user.getTitle().equalsIgnoreCase(title)){
/**
* Пользователь не меняет имя, значит операция прошла успешно,
* по крайней мере нам не нужно возвращать клиенту код ошибки
*/
return ResultCode.SUCCESS;
}
if(!matcher.matches()){
/**
* Не подходит по регулярному выражению
*/
return ResultCode.INVALID;
}
/**
* Меняем имя пользователя
*/
user.setTitle(title);
userRepository.update(user);
return ResultCode.SUCCESS;
}
}

View File

@@ -0,0 +1,65 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.Group;
import im.rosetta.database.repository.GroupRepository;
import im.rosetta.packet.Packet20GroupJoin;
import im.rosetta.packet.runtime.NetworkGroupStatus;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor20GroupJoin extends PacketExecutor<Packet20GroupJoin> {
private final GroupRepository groupRepository = new GroupRepository();
@Override
public void onPacketReceived(Packet20GroupJoin packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Клиент не авторизован, он не может вступать в группы
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String groupId = packet.getGroupId();
Group group = this.groupRepository.getGroup(groupId);
if(group == null){
/**
* Группы не существует, возвращаем клиенту статус INVALID
*/
packet.setStatus(NetworkGroupStatus.INVALID);
client.send(packet);
return;
}
if(group.getBannedPublicKeys().contains(eciAuthentificate.getPublicKey())){
/**
* Клиент забанен в группе, возвращаем клиенту статус BANNED
*/
packet.setStatus(NetworkGroupStatus.BANNED);
client.send(packet);
return;
}
if(group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey())){
/**
* Клиент уже является участником группы, возвращаем клиенту статус JOINED
*/
packet.setStatus(NetworkGroupStatus.JOINED);
client.send(packet);
return;
}
/**
* Добавляем клиента в группу и возвращаем клиенту статус JOINED
*/
this.groupRepository.addMemberToGroup(groupId, eciAuthentificate.getPublicKey());
packet.setStatus(NetworkGroupStatus.JOINED);
client.send(packet);
}
}

View File

@@ -0,0 +1,66 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.Group;
import im.rosetta.database.repository.GroupRepository;
import im.rosetta.packet.Packet21GroupLeave;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
/**
* Обработчик пакета выхода из группы
* Отправляет клиенту в ответ такой же пакет, если он успешно покинул группу или не состоял в ней изначально
* чтобы клиентское приложение могло корректно обновить интерфейс, например, удалить группу из списка групп пользователя
* Если клиент является единственным участником группы, то при выходе группа удаляется целиком
*/
public class Executor21GroupLeave extends PacketExecutor<Packet21GroupLeave> {
private final GroupRepository groupRepository = new GroupRepository();
@Override
public void onPacketReceived(Packet21GroupLeave packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Клиент не авторизован, он не может покидать группы
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String groupId = packet.getGroupId();
Group group = this.groupRepository.getGroup(groupId);
if(group == null){
/**
* Группы не существует, просто возвращаем клиенту тот же пакет,
* как будто мы успешно покинули группу, потому что по факту мы уже не состоим в ней
*/
client.send(packet);
return;
}
if(!group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey())){
/**
* Клиент не является участником группы, просто возвращаем клиенту тот же пакет,
* как будто мы успешно покинули группу, потому что по факту мы уже не состоим в ней
*/
client.send(packet);
return;
}
if(group.getMembersPublicKeys().size() <= 1){
/**
* Клиент является единственным участником группы, удаляем группу целиком
*/
this.groupRepository.removeGroup(groupId);
client.send(packet);
return;
}
/**
* Удаляем клиента из группы
*/
this.groupRepository.removeMemberFromGroup(groupId, eciAuthentificate.getPublicKey());
client.send(packet);
}
}

View File

@@ -0,0 +1,78 @@
package im.rosetta.executors;
import java.util.List;
import im.rosetta.Failures;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.Group;
import im.rosetta.database.repository.GroupRepository;
import im.rosetta.packet.Packet18GroupInfo;
import im.rosetta.packet.Packet22GroupBan;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor22GroupBan extends PacketExecutor<Packet22GroupBan> {
private final GroupRepository groupRepository = new GroupRepository();
@Override
public void onPacketReceived(Packet22GroupBan packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Клиент не авторизован, он не может банить участников групп
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String groupId = packet.getGroupId();
String publicKeyToBan = packet.getPublicKey();
Group group = this.groupRepository.getGroup(groupId);
if(group == null){
/**
* Группы не существует, но так как клиент вызывает бан участника, предполагается что он админ,
* а значит точно должен знать что группы не существует, значит это какое-то атипичное поведение
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
if(!group.getMembersPublicKeys().get(0).equals(eciAuthentificate.getPublicKey())){
/**
* Администратор группы - первый участник в списке участников,
* если публичный ключ клиента не совпадает с публичным ключом первого участника,
* значит он не админ и не может банить участников
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
if(!group.getMembersPublicKeys().contains(publicKeyToBan)){
/**
* Пользователя которого пытаются забанить нет в группе
*/
return;
}
/**
* Баним пользователя в группе - удаляем его из участников и добавляем в бан
*/
this.groupRepository.banMemberInGroup(groupId, publicKeyToBan);
/**
* Удаляем пользователя из списка участников (сверху мы уже удаляем его в базе,
* а здесь просто удаляем из объекта, чтобы отправить
* клиенту обновленную информацию о группе)
*/
List<String> membersPKs = group.getMembersPublicKeys();
membersPKs.remove(publicKeyToBan);
group.setBannedPublicKeys(membersPKs);
/**
* Отправляем клиенту новый Packet18GroupInfo, чтобы он обновил информацию о группе,
* например, удалил участника из списка участников
*/
Packet18GroupInfo groupInfoPacket = new Packet18GroupInfo();
groupInfoPacket.setGroupId(groupId);
groupInfoPacket.setMembersPKs(group.getMembersPublicKeys());
client.send(groupInfoPacket);
}
}

View File

@@ -0,0 +1,127 @@
package im.rosetta.executors;
import java.util.List;
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.client.tags.ECIDevice;
import im.rosetta.database.entity.Device;
import im.rosetta.database.repository.DeviceRepository;
import im.rosetta.event.EventManager;
import im.rosetta.event.events.handshake.HandshakeCompletedEvent;
import im.rosetta.packet.Packet0Handshake;
import im.rosetta.packet.Packet24DeviceResolve;
import im.rosetta.packet.runtime.DeviceSolution;
import im.rosetta.packet.runtime.HandshakeStage;
import im.rosetta.service.dispatch.DeviceDispatcher;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor24DeviceResolve extends PacketExecutor<Packet24DeviceResolve> {
private final ClientManager clientManager;
private final EventManager eventManager;
private final DeviceRepository deviceRepository = new DeviceRepository();
private final DeviceDispatcher deviceDispatcher;
public Executor24DeviceResolve(ClientManager clientManager, EventManager eventManager) {
this.clientManager = clientManager;
this.eventManager = eventManager;
this.deviceDispatcher = new DeviceDispatcher(clientManager);
}
@Override
public void onPacketReceived(Packet24DeviceResolve packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if (eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Если клиент не прошел аутентификацию, то он не может разрешать устройства
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
String deviceId = packet.getDeviceId();
DeviceSolution solution = packet.getSolution();
/**
* Получаем всех клиентов с таким publicKey, и сравниваем внутри них deviceId,
* если находим совпадение - разрешаем это устройство
*/
List<Client> clients = this.clientManager.getPKClients(eciAuthentificate.getPublicKey());
for(Client c : clients){
ECIDevice deviceTag = c.getTag(ECIDevice.class);
if(deviceTag != null && deviceTag.getDeviceId().equals(deviceId)){
/**
* Нашли клиента с таким deviceId, разрешаем или отклоняем его в зависимости от решения которое
* пришло в пакете
*/
if(solution == DeviceSolution.ACCEPT){
/**
* Разрешено, запоминаем устройство, инициируем событие успешного хэндшейка, и отправляем успешный хэндшейк этому устройству,
* чтобы клиент понял, что устройство разрешено и мог продолжать работу
*/
Device device = new Device();
device.setDeviceId(deviceId);
device.setPublicKey(eciAuthentificate.getPublicKey());
device.setDeviceOs(deviceTag.getDeviceOs());
device.setDeviceName(deviceTag.getDeviceName());
/**
* TODO: Здесь можно реализовать отключение синхронизации,
* например если у пользователя отключена синхронизация, то при разрешении нового устройства
* можно устанавливать leaveTime как текущее время, тогда сообщения новому устройству не загрузятся.
* Если установить leaveTime в 0, то синхронизируются все сообщения которые есть на сервере
*/
device.setLeaveTime(0L);
this.deviceRepository.save(device);
/**
* Устанавливаем пользователю успешный хэндшейк
*/
ECIAuthentificate authTag = c.getTag(ECIAuthentificate.class);
authTag.setHandshakeStage(HandshakeStage.COMPLETED);
c.reindexTag(ECIAuthentificate.class, authTag);
/**
* Отправляем этому устройству пакет с успешным хэндшейком, чтобы клиент понял,
* что устройство разрешено и мог продолжать работу
*/
Packet0Handshake handshake = new Packet0Handshake();
handshake.setHandshakeStage(HandshakeStage.COMPLETED);
handshake.setDeviceId("");
handshake.setDeviceName("");
handshake.setDeviceOs("");
handshake.setHeartbeatInterval(this.getSettings().heartbeatInterval);
handshake.setPrivateKey("");
handshake.setPublicKey("");
c.send(handshake);
/**
* Инициируем событие успешного хэндшейка, чтобы другие части сервера могли отреагировать на это,
* например отправить синхронизацию сообщений этому устройству
*/
this.eventManager.callEvent(new HandshakeCompletedEvent(deviceId, deviceId, deviceTag, eciAuthentificate, client));
break;
}
if(solution == DeviceSolution.DECLINE){
/**
* Отклонено, отправляем отклонение
*/
c.send(packet);
/**
* И удаляем теги аутентификации и устройства, так как клиент в момент отклонения
* должен поймать разлогин
*/
c.clearTags();
/**
* Отправяем всем устройствам этого пользователя информацию о том, что устройство было отключено (чтобы клиент мог скрыть уведомление
* о присоединении нового устройства)
*/
this.deviceDispatcher.sendDevices(eciAuthentificate.getPublicKey());
break;
}
}
}
}
}

View File

@@ -0,0 +1,72 @@
package im.rosetta.executors;
import java.util.ArrayList;
import java.util.List;
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.database.entity.User;
import im.rosetta.database.repository.UserRepository;
import im.rosetta.packet.Packet3Search;
import im.rosetta.packet.runtime.NetworkStatus;
import im.rosetta.packet.runtime.SearchInfo;
import im.rosetta.service.services.UserService;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor3Search extends PacketExecutor<Packet3Search> {
private final UserRepository userRepository = new UserRepository();
private final UserService userService = new UserService(userRepository);
private final ClientManager clientManager;
public Executor3Search(ClientManager clientManager) {
this.clientManager = clientManager;
}
@Override
public void onPacketReceived(Packet3Search packet, Client client) throws Exception, ProtocolException {
String search = packet.getSearch();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Клиент не авторизован, не разрешаем ему выполнять поиск
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
if(search.trim().equals("")){
/**
* Пустой поисковой запрос
*/
return;
}
List<User> usersFindedList = userService.searchUsers(search, 7);
Packet3Search response = new Packet3Search();
response.setSearch("");
response.setPrivateKey("");
List<SearchInfo> searchInfos = new ArrayList<>();
for(User user : usersFindedList){
SearchInfo searchInfo = new SearchInfo(
user.getUsername(),
user.getTitle(),
user.getPublicKey(),
user.getVerified(),
NetworkStatus.fromBoolean(this.clientManager.isClientConnected(user.getPublicKey()))
);
searchInfos.add(searchInfo);
}
response.setSearchInfos(searchInfos);
client.send(response);
}
}

View File

@@ -0,0 +1,74 @@
package im.rosetta.executors;
import java.util.ArrayList;
import java.util.List;
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.OnlineManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet4OnlineSubscribe;
import im.rosetta.packet.Packet5OnlineState;
import im.rosetta.packet.runtime.NetworkStatus;
import im.rosetta.packet.runtime.PKNetworkStatus;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
public class Executor4OnlineState extends PacketExecutor<Packet4OnlineSubscribe> {
private final OnlineManager onlineManager;
private final ClientManager clientManager;
public Executor4OnlineState(OnlineManager onlineManager, ClientManager clientManager) {
this.onlineManager = onlineManager;
this.clientManager = clientManager;
}
@Override
public void onPacketReceived(Packet4OnlineSubscribe packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Клиент не авторизован
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
/**
* Устанавливаем подписку на онлайн статус указанных публичных ключей
*/
List<String> publicKeys = packet.getPublicKeys();
if(publicKeys == null || publicKeys.isEmpty()) {
/**
* Пустой список, ничего не делаем
*/
return;
}
if(publicKeys.size() > 20) {
/**
* Слишком много подписок за один раз
*/
client.disconnect(Failures.TOO_MANY_ONLINE_SUBSCRIPTIONS);
return;
}
for (String targetPublicKey : publicKeys) {
this.onlineManager.subscribe(client, targetPublicKey);
}
/**
* Сразу же формируем и отправляем клиенту онлайн статус для указанных публичных ключей, чтобы клиент не ждал обновления статуса,
* а получил его сразу после подписки
*/
Packet5OnlineState onlineStates = new Packet5OnlineState();
List<PKNetworkStatus> onlineStatuses = new ArrayList<>();
for (String targetPublicKey : publicKeys) {
boolean isOnline = this.clientManager.isClientConnected(targetPublicKey);
PKNetworkStatus networkStatus = new PKNetworkStatus(targetPublicKey, NetworkStatus.fromBoolean(isOnline));
onlineStatuses.add(networkStatus);
}
onlineStates.setPkNetworkStatuses(onlineStatuses);
client.send(onlineStates);
}
}

View File

@@ -0,0 +1,126 @@
package im.rosetta.executors;
import java.util.List;
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet6Message;
import im.rosetta.packet.Packet8Delivery;
import im.rosetta.packet.runtime.Attachment;
import im.rosetta.service.dispatch.MessageDispatcher;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
import io.orprotocol.packet.PacketManager;
/**
* Обработчик пакета сообщений
*/
public class Executor6Message extends PacketExecutor<Packet6Message> {
private final MessageDispatcher messageDispatcher;
public Executor6Message(ClientManager clientManager, PacketManager packetManager) {
this.messageDispatcher = new MessageDispatcher(clientManager, packetManager);
}
@Override
public void onPacketReceived(Packet6Message packet, Client client) throws Exception, ProtocolException {
String fromPublicKey = packet.getFromPublicKey();
String toPublicKey = packet.getToPublicKey();
String messageId = packet.getMessageId();
List<Attachment> attachments = packet.getAttachments();
int attachmentsCount = attachments.size();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){
/**
* Если пользователь не авторизован
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
if(!eciAuthentificate.getPublicKey().equals(fromPublicKey)){
/**
* Если клиент пытается отправить сообщение от отправителя,
* которым он не является
*/
client.disconnect(Failures.DATA_MISSMATCH);
return;
}
if(fromPublicKey.equals(toPublicKey)){
/**
* Самому себе отправить сообщение нельзя, но это более-менее
* нормальное поведение, хотя на клиентах должно быть отработано,
* не кикаем пользователя
*/
return;
}
long currentTimestampSec = (System.currentTimeMillis() / 1000);
long messageTimestampSec = (packet.getTimestamp() / 1000);
/**
* Максимальный возраст сообщения в секундах, который сервер примет, чтобы
* клиент не мог подделать дату отправки и отправлять
* сообщения из "прошлого"
*/
long maxPaddingSec = 30;
if(attachmentsCount > 0){
/**
* Так как у нас есть вложения, то клиенту нужно какое-то время на их загрузку,
* разрешаем клиенту превысить maxPaddingSec и даем ему 30 секунд
* на отправку одного вложения (этого более чем достаточно, так как клиент
* вообще не должен отправлять сообщение пока все вложения не будут загружены
* на сервер)
*/
maxPaddingSec = maxPaddingSec * attachmentsCount;
}
if(currentTimestampSec - messageTimestampSec > maxPaddingSec){
/**
* Если сообщение было отправлено из "прошлого", то есть на момент
* прихода на сервер сообщению уже больше секунд чем допускает
* maxPaddingSec, то отклоняем его
*/
return;
}
if(attachmentsCount > 10){
/**
* Слишком много отправляемых вложений, так нельзя
*/
client.disconnect(Failures.TOO_MANY_ATTACHMENTS);
return;
}
/**
* Обновляем системную метку времени в соотвествии с сервером,
* так как у клиентов могут быть например неправильно настроены часы
* или разные часовые пояса
*/
packet.setTimestamp(System.currentTimeMillis());
packet.setPrivateKey("");
if(toPublicKey.startsWith("#group:")){
/**
* Это групповое сообщение, отправляем его всем участникам группы, кроме отправителя
*/
this.messageDispatcher.sendGroup(packet, client, eciAuthentificate);
}else{
/**
* Это личное сообщение, отправляем его получателю
*/
this.messageDispatcher.sendPeer(packet, client);
}
/**
* Сообщение успешно отправлено, отправялем клиенту пакет успешной доставки
*/
Packet8Delivery deliveryPacket = new Packet8Delivery();
deliveryPacket.setMessageId(messageId);
deliveryPacket.setToPublicKey(toPublicKey);
client.send(deliveryPacket);
}
}

View File

@@ -0,0 +1,56 @@
package im.rosetta.executors;
import im.rosetta.Failures;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.packet.Packet7Read;
import im.rosetta.service.dispatch.MessageDispatcher;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
import io.orprotocol.packet.PacketExecutor;
import io.orprotocol.packet.PacketManager;
public class Executor7Read extends PacketExecutor<Packet7Read> {
private final MessageDispatcher messageDispatcher;
public Executor7Read(ClientManager clientManager, PacketManager packetManager) {
this.messageDispatcher = new MessageDispatcher(clientManager, packetManager);
}
@Override
public void onPacketReceived(Packet7Read packet, Client client) throws Exception, ProtocolException {
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
String fromPublicKey = packet.getFromPublicKey();
String toPublicKey = packet.getToPublicKey();
if(fromPublicKey.equals(toPublicKey)){
/**
* Ничего не делаем если назначение пакета такое же как и отправитель,
* такое поведение может быть при заходе в Saved Messages и должно быть правильно обработано на клиенте
*/
return;
}
if (eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Если клиент не прошел аутентификацию, то он не может читать сообщения
*/
client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED);
return;
}
packet.setPrivateKey("");
if(toPublicKey.startsWith("#group:")){
/**
* Это групповое чтение, отправляем его всем участникам группы, кроме отправителя
*/
this.messageDispatcher.sendGroup(packet, client, eciAuthentificate);
}else{
/**
* Это личное сообщение, отправляем его получателю
*/
this.messageDispatcher.sendPeer(packet, client);
}
}
}

View File

@@ -0,0 +1,5 @@
package im.rosetta.executors.base;
public class ExecutorBaseDialog {
}

View File

@@ -0,0 +1,59 @@
package im.rosetta.listeners;
import im.rosetta.client.ClientManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.event.EventHandler;
import im.rosetta.event.Listener;
import im.rosetta.event.events.DisconnectEvent;
import im.rosetta.event.events.handshake.HandshakeCompletedEvent;
import im.rosetta.event.events.handshake.HandshakeDeviceConfirmEvent;
import im.rosetta.service.dispatch.DeviceDispatcher;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
public class DeviceListListener implements Listener {
private final DeviceDispatcher deviceDispatcher;
public DeviceListListener(ClientManager clientManager) {
this.deviceDispatcher = new DeviceDispatcher(clientManager);
}
@EventHandler
public void onHandshakeComplete(HandshakeCompletedEvent event) throws ProtocolException {
Client client = event.getClient();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate != null){
/**
* Когда клиент прошел аутентификацию, отправляем ему список устройств
*/
this.deviceDispatcher.sendDevices(eciAuthentificate.getPublicKey());
}
}
@EventHandler
public void onDeviceConfirm(HandshakeDeviceConfirmEvent event) throws ProtocolException {
Client client = event.getClient();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate != null){
/**
* Когда к аккаунту присоединяется новое устройство отправляем всем клиентам с этим публичным ключом обновленный список устройств
*/
this.deviceDispatcher.sendDevices(eciAuthentificate.getPublicKey());
}
}
@EventHandler
public void onDisconnect(DisconnectEvent event) throws ProtocolException {
Client client = event.getClient();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate != null){
/**
* Когда устройство отключается от аккаунта, отправляем всем клиентам с этим публичным ключом обновленный список устройств
*/
this.deviceDispatcher.sendDevices(eciAuthentificate.getPublicKey());
}
}
}

View File

@@ -0,0 +1,12 @@
package im.rosetta.listeners;
import im.rosetta.event.Listener;
import im.rosetta.event.events.handshake.HandshakeCompletedEvent;
public class HandshakeCompleteListener implements Listener {
public void onHandshakeComplete(HandshakeCompletedEvent event) {
//TODO: Обработка завершения Handshake и синхронизация недоставленных сообщений (переписок)
}
}

View File

@@ -0,0 +1,61 @@
package im.rosetta.listeners;
import java.util.ArrayList;
import java.util.List;
import im.rosetta.client.OnlineManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.event.EventHandler;
import im.rosetta.event.Listener;
import im.rosetta.event.events.DisconnectEvent;
import im.rosetta.packet.Packet5OnlineState;
import im.rosetta.packet.runtime.NetworkStatus;
import im.rosetta.packet.runtime.PKNetworkStatus;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
/**
* Слушатель отключения клиента
* Нужен для того чтобы обновлять онлайн статус пользоватеей, и уведомлять всех
* подписчиков об изменении статуса
*/
public class OnlineStatusDisconnectListener implements Listener {
private OnlineManager onlineManager;
public OnlineStatusDisconnectListener(OnlineManager onlineManager) {
this.onlineManager = onlineManager;
}
@EventHandler
public void onClientDisconnect(DisconnectEvent event) throws ProtocolException {
Client client = event.getClient();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Клиент не авторизован, ничего не делаем
*/
return;
}
List<Client> subscribers = this.onlineManager.getSubscribers(client);
/**
* Уведомляем всех подписчиков на его онлайн статус, что он отключился (ушел в оффлайн)
*/
for (Client subscriber : subscribers) {
Packet5OnlineState packet = new Packet5OnlineState();
List<PKNetworkStatus> statuses = new ArrayList<>();
statuses.add(new PKNetworkStatus(
eciAuthentificate.getPublicKey(),
NetworkStatus.OFFLINE
));
packet.setPkNetworkStatuses(statuses);
subscriber.send(packet);
}
/**
* Удаляем все подписки этого клиента, так как он отключился
*/
this.onlineManager.unsubscribeAll(client);
}
}

View File

@@ -0,0 +1,59 @@
package im.rosetta.listeners;
import java.util.ArrayList;
import java.util.List;
import im.rosetta.client.OnlineManager;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.event.EventHandler;
import im.rosetta.event.Listener;
import im.rosetta.event.events.handshake.HandshakeCompletedEvent;
import im.rosetta.packet.Packet5OnlineState;
import im.rosetta.packet.runtime.NetworkStatus;
import im.rosetta.packet.runtime.PKNetworkStatus;
import io.orprotocol.ProtocolException;
import io.orprotocol.client.Client;
/**
* Слушатель завершения рукопожатия (хэндшейкапа) клиента
* Нужен для того чтобы обновлять онлайн статус пользоватеей, и уведомлять всех
* подписчиков об изменении статуса
*/
public class OnlineStatusHandshakeCompleteListener implements Listener {
private final OnlineManager onlineManager;
public OnlineStatusHandshakeCompleteListener(OnlineManager onlineManager) {
this.onlineManager = onlineManager;
}
@EventHandler
public void onHandshakeComplete(HandshakeCompletedEvent event) throws ProtocolException {
Client client = event.getClient();
ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class);
if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) {
/**
* Клиент не авторизован, ничего не делаем, однако такое
* не должно происходить, так как событие хэндшейкапа
* должно означать что клиент авторизован
*/
return;
}
List<Client> subscribers = this.onlineManager.getSubscribers(client);
/**
* Уведомляем всех подписчиков на его онлайн статус, что он подключился (стал онлайн)
*/
for (Client subscriber : subscribers) {
Packet5OnlineState packet = new Packet5OnlineState();
List<PKNetworkStatus> statuses = new ArrayList<>();
statuses.add(new PKNetworkStatus(
eciAuthentificate.getPublicKey(),
NetworkStatus.ONLINE
));
packet.setPkNetworkStatuses(statuses);
subscriber.send(packet);
}
}
}

View File

@@ -0,0 +1,55 @@
package im.rosetta.listeners;
import im.rosetta.client.tags.ECIAuthentificate;
import im.rosetta.client.tags.ECIDevice;
import im.rosetta.database.repository.DeviceRepository;
import im.rosetta.event.EventHandler;
import im.rosetta.event.Listener;
import im.rosetta.event.events.ServerStopEvent;
import im.rosetta.logger.Logger;
import im.rosetta.logger.enums.Color;
import io.orprotocol.Server;
import io.orprotocol.client.Client;
/**
* При остановке сервера нам нужно обновить всем клиентам время последней активности устройства
* чтобы корректно потом отработать загрузку сообщений для пользователя
*/
public class ServerStopListener implements Listener {
private final DeviceRepository deviceRepository = new DeviceRepository();
private Logger logger;
public ServerStopListener(Logger logger) {
this.logger = logger;
}
@EventHandler
public void onServerStop(ServerStopEvent event) {
Server server = event.getServer();
this.logger.info(Color.RED + "Сервер останавливается, обновляем время последней активности устройств клиентов...");
for(Client client : server.getClients()){
ECIAuthentificate eciAuth = client.getTag(ECIAuthentificate.class);
if(eciAuth == null || !eciAuth.hasAuthorized()){
/**
* Если клиент не авторизован, пропускаем его, таким клиентам не нужно
* обновлять время активности устройства
*/
continue;
}
ECIDevice eciDevice = client.getTag(ECIDevice.class);
if(eciDevice == null){
/**
* Если у клиента нет тега устройства, пропускаем его
* такого быть не должно, но на всякий случай
*/
continue;
}
deviceRepository.updateDeviceLeaveTime(eciDevice.getDeviceId());
}
this.logger.info(Color.RED + "Время последней активности устройств клиентов обновлено.");
}
}

View File

@@ -0,0 +1,74 @@
package im.rosetta.logger;
import java.time.Instant;
import im.rosetta.logger.enums.Color;
import im.rosetta.logger.enums.LogLevel;
public class Logger {
private long startTime = 0;
private LogLevel logLevel;
public Logger(LogLevel logLevel) {
this.logLevel = logLevel;
startTime = (System.currentTimeMillis() / 1000);
}
/**
* Логирование сообщения с указанным уровнем логирования
* @param logLevel уровень логирования
* @param message сообщение для логирования
*/
public void log(LogLevel logLevel, String message) {
if (!this.logLevel.allows(logLevel)) {
return;
}
long currentTimeMs = System.currentTimeMillis();
long currentTime = currentTimeMs / 1000;
long elapsedTime = currentTime - startTime;
String currentDateISO = Instant.ofEpochMilli(currentTimeMs).toString();
System.out.println(getColorForLogLevel(logLevel) + "["+ logLevel.toString() +"]" + Color.RESET + "[" + currentDateISO + "]" + Color.CYAN + "[+" + elapsedTime + "] " + Color.WHITE + message + Color.RESET);
}
/**
* Логирование информационного сообщения
* @param message сообщение для логирования
*/
public void info(String message) {
this.log(LogLevel.INFO, message);
}
/**
* Логирование предупреждающего сообщения
* @param message сообщение для логирования
*/
public void warn(String message) {
this.log(LogLevel.WARN, message);
}
/**
* Логирование сообщения об ошибке
* @param message сообщение для логирования
*/
public void error(String message) {
this.log(LogLevel.ERROR, message);
}
/**
* Логирование отладочного сообщения
* @param message сообщение для логирования
*/
public void debug(String message) {
this.log(LogLevel.DEBUG, message);
}
private String getColorForLogLevel(LogLevel logLevel) {
return switch (logLevel) {
case INFO -> Color.BLUE;
case WARN -> Color.YELLOW;
case ERROR -> Color.RED;
case DEBUG -> Color.PURPLE;
};
}
}

View File

@@ -0,0 +1,13 @@
package im.rosetta.logger.enums;
public final class Color {
public static final String RESET = "\u001B[0m";
public static final String BLACK = "\u001B[30m";
public static final String RED = "\u001B[31m";
public static final String GREEN = "\u001B[32m";
public static final String YELLOW = "\u001B[33m";
public static final String BLUE = "\u001B[34m";
public static final String PURPLE = "\u001B[35m";
public static final String CYAN = "\u001B[36m";
public static final String WHITE = "\u001B[37m";
}

View File

@@ -0,0 +1,12 @@
package im.rosetta.logger.enums;
public enum LogLevel {
INFO,
WARN,
ERROR,
DEBUG;
public boolean allows(LogLevel other) {
return this.ordinal() <= other.ordinal();
}
}

View File

@@ -0,0 +1,156 @@
package im.rosetta.packet;
import im.rosetta.packet.runtime.HandshakeStage;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Пакет хэндшейка между клиентом и сервером.
* Используется для установления соединения и подтверждения от сервера что клиент
* тот за кого себя выдает.
*
* Протокол таблица:
* 0 - packetId (int16)
* 1 - privateKey (string)
* 2 - publicKey (string)
* 3 - protocolVersion (int8)
* 4 - heartbeatInterval (int8)
* 5 - deviceId (string)
* 6 - deviceName (string)
* 7 - deviceOs (string)
* 8 - handshakeStage (int8)
*/
public class Packet0Handshake extends Packet {
/**
* Публичный и приватный ключи клиента
*/
private String publicKey;
/**
* Приватный ключ клиента
* Это не совсем приватный ключ, а лишь необратимо зашифрованная его версия
* для идентификации клиента на сервере.
*/
private String privateKey;
/**
* Версия протокола клиента
*/
private int protocolVersion = 1;
/**
* Интервал отправки heartbeat пакетов в секундах
*/
private int heartbeatInterval = 15;
/**
* Минимальная информация об устройстве клиента
*/
private String deviceId;
private String deviceName;
private String deviceOs;
/**
* Стадия рукопожатия
* 0 - COMPLETED
* 1 - NEED_DEVICE_VERIFICATION
*/
private HandshakeStage handshakeStage = HandshakeStage.COMPLETED;
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.privateKey);
stream.writeString(this.publicKey);
stream.writeInt8(this.protocolVersion);
stream.writeInt8(this.heartbeatInterval);
stream.writeString(this.deviceId);
stream.writeString(this.deviceName);
stream.writeString(this.deviceOs);
stream.writeInt8(this.handshakeStage.getCode());
return stream;
}
@Override
public void read(Stream stream) {
this.privateKey = stream.readString();
this.publicKey = stream.readString();
this.protocolVersion = stream.readInt8();
this.heartbeatInterval = stream.readInt8();
String deviceId = stream.readString();
String deviceName = stream.readString();
String deviceOs = stream.readString();
this.deviceId = deviceId;
this.deviceName = deviceName;
this.deviceOs = deviceOs;
this.handshakeStage = HandshakeStage.fromCode(
stream.readInt8()
);
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public int getProtocolVersion() {
return protocolVersion;
}
public int getHeartbeatInterval() {
return heartbeatInterval;
}
public String getDeviceId() {
return deviceId;
}
public String getDeviceName() {
return deviceName;
}
public String getDeviceOs() {
return deviceOs;
}
public HandshakeStage getHandshakeStage() {
return handshakeStage;
}
public void setHandshakeStage(HandshakeStage handshakeStage) {
this.handshakeStage = handshakeStage;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public void setDeviceOs(String deviceOs) {
this.deviceOs = deviceOs;
}
public void setHeartbeatInterval(int heartbeatInterval) {
this.heartbeatInterval = heartbeatInterval;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
}

View File

@@ -0,0 +1,14 @@
package im.rosetta.packet;
import im.rosetta.packet.base.PacketBaseServer;
/**
* Получает сервер обновления
*/
public class Packet10RequestUpdate extends PacketBaseServer {
/**
* Пустой пакет, так как он наследник PacketBaseServer
* который всегда имеет одну структуру.
* Смотреть PacketBaseServer для реализации
*/
}

View File

@@ -0,0 +1,29 @@
package im.rosetta.packet;
import im.rosetta.packet.base.PacketBaseDialog;
import io.orprotocol.Stream;
/**
* Пакет отвечающий за индикацию печати в диалогах
*/
public class Packet11Typeing extends PacketBaseDialog {
@Override
public void read(Stream stream) {
this.privateKey = stream.readString();
this.fromPublicKey = stream.readString();
this.toPublicKey = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.privateKey);
stream.writeString(this.fromPublicKey);
stream.writeString(this.toPublicKey);
return stream;
}
}

View File

@@ -0,0 +1,16 @@
package im.rosetta.packet;
import im.rosetta.packet.base.PacketBaseServer;
/**
* Пакет отправляется клиентом для запроса транспортного сервера, строка в этот момент клиентом
* не заполняется, а уже обратно сервер заполняет строку и записывает туда транспортный сервер
* чтобы клиент мог отправлять вложения на него
*/
public class Packet15RequestTransport extends PacketBaseServer {
/**
* Пустой пакет, так как он наследник PacketBaseServer
* который всегда имеет одну структуру.
* Смотреть PacketBaseServer для реализации
*/
}

View File

@@ -0,0 +1,63 @@
package im.rosetta.packet;
import im.rosetta.packet.runtime.NetworkNotificationAction;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Packet16PushNotification бросается клиентом для подписки на пуш уведомления
*/
public class Packet16PushNotification extends Packet {
private String notificationToken;
private NetworkNotificationAction action;
@Override
public void read(Stream stream) {
this.notificationToken = stream.readString();
this.action = NetworkNotificationAction.fromCode(stream.readInt8());
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(notificationToken);
stream.writeInt8(action.getCode());
return stream;
}
/**
* Получить токен пуш уведомлений, который нужно подписать или отписать в зависимости от action
* @return токен пуш уведомлений
*/
public String getNotificationToken() {
return notificationToken;
}
/**
* Получить действие, которое нужно выполнить с токеном пуш уведомлений. SUBSCRIBE - подписать этот токен на пуш уведомления, UNSUBSCRIBE - отписать этот токен от пуш уведомлений
* @return действие, которое нужно выполнить с токеном пуш уведомлений
*/
public NetworkNotificationAction getAction() {
return action;
}
/**
* Устанавливает токен пуш уведомлений, который нужно подписать или отписать в зависимости от action
* @param notificationToken токен пуш уведомлений
*/
public void setNotificationToken(String notificationToken) {
this.notificationToken = notificationToken;
}
/**
* Устанавливает действие, которое нужно выполнить с токеном пуш уведомлений. SUBSCRIBE - подписать этот токен на пуш уведомления, UNSUBSCRIBE - отписать этот токен от пуш уведомлений
* @param action действие, которое нужно выполнить с токеном пуш уведомлений
*/
public void setAction(NetworkNotificationAction action) {
this.action = action;
}
}

View File

@@ -0,0 +1,39 @@
package im.rosetta.packet;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet17GroupCreate extends Packet {
private String groupId;
@Override
public void read(Stream stream) {
this.groupId = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.groupId);
return stream;
}
/**
* Получить id группы, которую нужно создать
* @return id группы, которую нужно создать
*/
public String getGroupId() {
return this.groupId;
}
/**
* Установить id группы, которую нужно создать
* @param groupId id группы, которую нужно создать
*/
public void setGroupId(String groupId) {
this.groupId = groupId;
}
}

View File

@@ -0,0 +1,67 @@
package im.rosetta.packet;
import java.util.List;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet18GroupInfo extends Packet {
private String groupId;
private List<String> membersPKs;
@Override
public void read(Stream stream) {
this.groupId = stream.readString();
int membersCount = stream.readInt16();
this.membersPKs = new java.util.ArrayList<>();
for(int i = 0; i < membersCount; i++) {
this.membersPKs.add(stream.readString());
}
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.groupId);
stream.writeInt16(this.membersPKs.size());
for(String memberPK : this.membersPKs) {
stream.writeString(memberPK);
}
return stream;
}
/**
* Получить id группы
* @return id группы
*/
public String getGroupId() {
return this.groupId;
}
/**
* Установить id группы
* @param groupId id группы
*/
public void setGroupId(String groupId) {
this.groupId = groupId;
}
/**
* Получить публичные ключи участников группы
* @return список публичных ключей участников группы
*/
public List<String> getMembersPKs() {
return this.membersPKs;
}
/**
* Установить публичные ключи участников группы
* @param membersPKs список публичных ключей участников группы
*/
public void setMembersPKs(List<String> membersPKs) {
this.membersPKs = membersPKs;
}
}

View File

@@ -0,0 +1,82 @@
package im.rosetta.packet;
import im.rosetta.packet.runtime.NetworkGroupStatus;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Пакет который бросается клиентом для определения статуса приглашения в группу
*/
public class Packet19GroupInviteInfo extends Packet {
private String groupId;
private int membersCount;
private NetworkGroupStatus status;
@Override
public void read(Stream stream) {
this.groupId = stream.readString();
this.membersCount = stream.readInt16();
this.status = NetworkGroupStatus.fromCode(stream.readInt8());
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.groupId);
stream.writeInt16(this.membersCount);
stream.writeInt8(this.status.getCode());
return stream;
}
/**
* Получить id группы
* @return id группы
*/
public String getGroupId() {
return this.groupId;
}
/**
* Установить id группы
* @param groupId id группы
*/
public void setGroupId(String groupId) {
this.groupId = groupId;
}
/**
* Получить количество участников в группе
* @return количество участников в группе
*/
public int getMembersCount() {
return this.membersCount;
}
/**
* Установить количество участников в группе
* @param membersCount количество участников в группе
*/
public void setMembersCount(int membersCount) {
this.membersCount = membersCount;
}
/**
* Получить статус приглашения в группу
* @return статус приглашения в группу
*/
public NetworkGroupStatus getStatus() {
return this.status;
}
/**
* Установить статус приглашения в группу
* @param status статус приглашения в группу
*/
public void setStatus(NetworkGroupStatus status) {
this.status = status;
}
}

View File

@@ -0,0 +1,84 @@
package im.rosetta.packet;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet1UserInfo extends Packet {
@Deprecated(since = "1.1", forRemoval = true)
private String privateKey;
private String username;
private String title;
@Override
public void read(Stream stream) {
this.username = stream.readString();
this.title = stream.readString();
this.privateKey = stream.readString();
}
@Override
public Stream write() {
Stream steram = new Stream();
steram.writeInt16(this.packetId);
steram.writeString(this.username);
steram.writeString(this.title);
steram.writeString(this.privateKey);
return steram;
}
/**
* Получает приватный ключ пользователя
* @return приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public String getPrivateKey() {
return this.privateKey;
}
/**
* Устанавливает приватный ключ пользователя
* @param privateKey приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
/**
* Возвращает имя пользователя
* @return имя пользователя
*/
public String getUsername() {
return this.username;
}
/**
* Возвращает заголовок (титул) пользователя
* @return заголовок пользователя
*/
public String getTitle() {
return this.title;
}
/**
* Устанавливает имя пользователя
* @param username имя пользователя
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Устанавливает заголовок (титул) пользователя
* @param title заголовок пользователя
*/
public void setTitle(String title) {
this.title = title;
}
}

View File

@@ -0,0 +1,65 @@
package im.rosetta.packet;
import im.rosetta.packet.runtime.NetworkGroupStatus;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Вызывается клиентом для вступления в группу.
* Сервер модифицирует этот пакет, устанавливая статус группы, и отправляет его обратно
* клиенту
*/
public class Packet20GroupJoin extends Packet {
private String groupId;
private NetworkGroupStatus status;
@Override
public void read(Stream stream) {
this.groupId = stream.readString();
this.status = NetworkGroupStatus.fromCode(stream.readInt8());
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.groupId);
stream.writeInt8(this.status.getCode());
return stream;
}
/**
* Получить id группы
* @return id группы
*/
public String getGroupId() {
return groupId;
}
/**
* Установить id группы
* @param groupId id группы
*/
public void setGroupId(String groupId) {
this.groupId = groupId;
}
/**
* Получить статус группы
* @return статус группы
*/
public NetworkGroupStatus getStatus() {
return status;
}
/**
* Установить статус группы
* @param status статус группы
*/
public void setStatus(NetworkGroupStatus status) {
this.status = status;
}
}

View File

@@ -0,0 +1,42 @@
package im.rosetta.packet;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Вызывается клиентом для выхода из группы. Содержит id группы, которую нужно покинуть
*/
public class Packet21GroupLeave extends Packet {
private String groupId;
@Override
public void read(Stream stream) {
this.groupId = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.groupId);
return stream;
}
/**
* Получить id группы, которую нужно покинуть
* @return id группы, которую нужно покинуть
*/
public String getGroupId() {
return this.groupId;
}
/**
* Установить id группы, которую нужно покинуть
* @param groupId id группы, которую нужно покинуть
*/
public void setGroupId(String groupId) {
this.groupId = groupId;
}
}

View File

@@ -0,0 +1,58 @@
package im.rosetta.packet;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet22GroupBan extends Packet {
private String groupId;
private String publicKey;
@Override
public void read(Stream stream) {
this.groupId = stream.readString();
this.publicKey = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.groupId);
stream.writeString(this.publicKey);
return stream;
}
/**
* Получить id группы, в которой нужно забанить пользователя
* @return id группы
*/
public String getGroupId() {
return groupId;
}
/**
* Установить id группы, в которой нужно забанить пользователя
* @param groupId id группы
*/
public void setGroupId(String groupId) {
this.groupId = groupId;
}
/**
* Получить публичный ключ пользователя, которого нужно забанить в группе
* @return публичный ключ пользователя
*/
public String getPublicKey() {
return publicKey;
}
/**
* Установить публичный ключ пользователя, которого нужно забанить в группе
* @param publicKey публичный ключ
*/
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
}

View File

@@ -0,0 +1,62 @@
package im.rosetta.packet;
import java.util.List;
import im.rosetta.packet.runtime.DeviceSolution;
import im.rosetta.packet.runtime.NetworkDevice;
import im.rosetta.packet.runtime.NetworkStatus;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Пакет, который содержит список устройств, с которых был произведен вход в систему.
* Этот пакет может быть отправлен сервером в ответ на запрос клиента о получении списка устройств,
* или может быть отправлен сервером при обнаружении нового входа в систему с нового устройства, чтобы уведомить клиента о новом устройстве.
*/
public class Packet23DeviceList extends Packet {
private List<NetworkDevice> devices;
@Override
public void read(Stream stream) {
int deviceCount = stream.readInt16();
this.devices = new java.util.ArrayList<>();
for(int i = 0; i < deviceCount; i++) {
NetworkDevice netDevice = new NetworkDevice();
netDevice.setDeviceId(stream.readString());
netDevice.setDeviceName(stream.readString());
netDevice.setDeviceOs(stream.readString());
/**
* TODO: Использовать boolean для обозначения статуса сети, а не int8.
*/
netDevice.setNetworkStatus(NetworkStatus.fromCode(stream.readInt8()));
netDevice.setDeviceSolution(DeviceSolution.fromCode(stream.readInt8()));
this.devices.add(netDevice);
}
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeInt16(this.devices.size());
for(NetworkDevice device : this.devices) {
stream.writeString(device.getDeviceId());
stream.writeString(device.getDeviceName());
stream.writeString(device.getDeviceOs());
stream.writeInt8(device.getNetworkStatus().getCode());
stream.writeInt8(device.getDeviceSolution().getCode());
}
return stream;
}
public List<NetworkDevice> getDevices() {
return devices;
}
public void setDevices(List<NetworkDevice> devices) {
this.devices = devices;
}
}

View File

@@ -0,0 +1,48 @@
package im.rosetta.packet;
import im.rosetta.packet.runtime.DeviceSolution;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Пакет для решения по запросу на добавление устройства
* Принимается от клиента, который получил запрос на добавление устройства, и отправляется серверу для обработки решения
*/
public class Packet24DeviceResolve extends Packet {
private String deviceId;
private DeviceSolution solution;
@Override
public void read(Stream stream) {
this.deviceId = stream.readString();
this.solution = DeviceSolution.fromCode(stream.readInt8());
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.deviceId);
stream.writeInt8(this.solution.getCode());
return stream;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public DeviceSolution getSolution() {
return solution;
}
public void setSolution(DeviceSolution solution) {
this.solution = solution;
}
}

View File

@@ -0,0 +1,42 @@
package im.rosetta.packet;
import im.rosetta.packet.runtime.ResultCode;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet2Result extends Packet {
private ResultCode resultCode;
@Override
public void read(Stream stream) {
this.resultCode = ResultCode.fromCode(stream.readInt16());
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeInt16(this.resultCode.getCode());
return stream;
}
/**
* Получает код результата операции
* @return код результата
*/
public ResultCode getResultCode() {
return this.resultCode;
}
/**
* Устанавливает код результата операции
* @param resultCode код результата
*/
public void setResultCode(ResultCode resultCode) {
this.resultCode = resultCode;
}
}

View File

@@ -0,0 +1,98 @@
package im.rosetta.packet;
import java.util.ArrayList;
import java.util.List;
import im.rosetta.packet.runtime.SearchInfo;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet3Search extends Packet {
@Deprecated(since = "1.1", forRemoval = true)
private String privateKey;
private String search;
private List<SearchInfo> searchInfo;
@Override
public void read(Stream stream) {
this.privateKey = stream.readString();
this.search = stream.readString();
int searchInfoCount = stream.readInt16();
this.searchInfo = new ArrayList<>();
for (int i = 0; i < searchInfoCount; i++) {
SearchInfo info = new SearchInfo();
info.readFromStream(stream);
this.searchInfo.add(info);
}
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.privateKey);
stream.writeString(this.search);
stream.writeInt16(this.searchInfo.size());
for (SearchInfo info : this.searchInfo) {
info.writeToStream(stream);
}
return stream;
}
/**
* Получает приватный ключ пользователя
* @return приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public String getPrivateKey() {
return this.privateKey;
}
/**
* Устанавливает приватный ключ пользователя
* @param privateKey приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
/**
* Получает строку поиска
* @return строка поиска
*/
public String getSearch() {
return this.search;
}
/**
* Устанавливает строку поиска
* @param search строка поиска
*/
public void setSearch(String search) {
this.search = search;
}
/**
* Получает результаты поиска
* @return список результатов
*/
public List<SearchInfo> getSearchInfos() {
return this.searchInfo;
}
/**
* Устанавливает результаты поиска
* @param searchInfo
*/
public void setSearchInfos(List<SearchInfo> searchInfos) {
this.searchInfo = searchInfos;
}
}

View File

@@ -0,0 +1,73 @@
package im.rosetta.packet;
import java.util.List;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet4OnlineSubscribe extends Packet {
private String privateKey;
private List<String> publicKeys;
@Override
public void read(Stream stream) {
this.privateKey = stream.readString();
int publicKeysCount = stream.readInt16();
this.publicKeys = new java.util.ArrayList<>();
for (int i = 0; i < publicKeysCount; i++) {
this.publicKeys.add(stream.readString());
}
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.privateKey);
stream.writeInt16(this.publicKeys.size());
for (String publicKey : this.publicKeys) {
stream.writeString(publicKey);
}
return stream;
}
/**
* Получает приватный ключ пользователя
* @return приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public String getPrivateKey() {
return this.privateKey;
}
/**
* Устанавливает приватный ключ пользователя
* @param privateKey приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
/**
* Получает список публичных ключей для подписки на онлайн статус
* @return список публичных ключей
*/
public List<String> getPublicKeys() {
return this.publicKeys;
}
/**
* Устанавливает список публичных ключей для подписки на онлайн статус
* @param publicKeys список публичных ключей
*/
public void setPublicKeys(List<String> publicKeys) {
this.publicKeys = publicKeys;
}
}

View File

@@ -0,0 +1,47 @@
package im.rosetta.packet;
import java.util.List;
import im.rosetta.packet.runtime.NetworkStatus;
import im.rosetta.packet.runtime.PKNetworkStatus;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class Packet5OnlineState extends Packet {
private List<PKNetworkStatus> pkNetworkStatuses;
@Override
public void read(Stream stream) {
int count = stream.readInt8();
this.pkNetworkStatuses = new java.util.ArrayList<>();
for (int i = 0; i < count; i++) {
String publicKey = stream.readString();
boolean online = stream.readBoolean();
PKNetworkStatus status = new PKNetworkStatus(publicKey, NetworkStatus.fromBoolean(online));
this.pkNetworkStatuses.add(status);
}
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeInt8(this.pkNetworkStatuses.size());
for (PKNetworkStatus status : this.pkNetworkStatuses) {
stream.writeString(status.getPublicKey());
stream.writeBoolean(status.getNetworkStatus().toBoolean());
}
return stream;
}
public List<PKNetworkStatus> getPkNetworkStatuses() {
return pkNetworkStatuses;
}
public void setPkNetworkStatuses(List<PKNetworkStatus> pkNetworkStatuses) {
this.pkNetworkStatuses = pkNetworkStatuses;
}
}

View File

@@ -0,0 +1,193 @@
package im.rosetta.packet;
import java.util.List;
import im.rosetta.packet.base.PacketBaseDialog;
import im.rosetta.packet.runtime.Attachment;
import im.rosetta.packet.runtime.AttachmentType;
import io.orprotocol.Stream;
/**
* Пакет для отправки сообщений между пользователями. Содержит зашифрованное текстовое содержимое и массив вложений с
* их метаданными. Временная метка используется для сортировки сообщений и отображения времени отправки. Идентификатор
* сообщения нужен для редактирования и удаления сообщений. Ключ chacha используется для шифрования текста сообщения,
* а aesChachaKey нужен для последующей синхронизации своих же сообщений на других устройствах.
* Сам ключ ChaCha20 во избежания обемена ключами зашифрован ECC.
*/
public class Packet6Message extends PacketBaseDialog {
/**
* Текстовое содержимое сообщения, зашифровано ChaCha20, зашифровано ECC
*/
private String content;
/**
* Ключ chacha для шифрования сообщения, зашифрован ECC
*/
private String chachaKey;
/**
* Временная метка сообщения в миллисекундах
*/
private long timestamp;
/**
* Идентификатор сообщения, нужен для редактирования и удаления сообщений
*/
private String messageId;
/**
* Массив вложений в сообщении
*/
private List<Attachment> attachments;
/**
* Закодированный с помощью AES ключ chacha, нужен
* для последующей синхронизации своих же сообщений
*/
private String aesChachaKey;
@Override
public void read(Stream stream) {
this.fromPublicKey = stream.readString();
this.toPublicKey = stream.readString();
this.content = stream.readString();
this.chachaKey = stream.readString();
this.timestamp = stream.readInt64();
this.privateKey = stream.readString();
this.messageId = stream.readString();
int attachmentsCount = stream.readInt8();
this.attachments = new java.util.ArrayList<>();
for (int i = 0; i < attachmentsCount; i++) {
String id = stream.readString();
String preview = stream.readString();
String blob = stream.readString();
AttachmentType type = AttachmentType.fromCode(stream.readInt8());
this.attachments.add(new Attachment(id, blob, type, preview));
}
this.aesChachaKey = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.fromPublicKey);
stream.writeString(this.toPublicKey);
stream.writeString(this.content);
stream.writeString(this.chachaKey);
stream.writeInt64(this.timestamp);
stream.writeString(this.privateKey);
stream.writeString(this.messageId);
stream.writeInt8(this.attachments.size());
for (Attachment attachment : this.attachments) {
stream.writeString(attachment.getId());
stream.writeString(attachment.getPreview());
stream.writeString(attachment.getBlob());
stream.writeInt8((byte) attachment.getType().getCode());
}
stream.writeString(this.aesChachaKey);
return stream;
}
/**
* Получить текстовое содержимое сообщения, зашифровано ChaCha20, зашифровано ECC
* @return текстовое содержимое сообщения
*/
public String getContent() {
return content;
}
/**
* Получить ключ chacha для шифрования сообщения, зашифрован ECC
* @return ключ chacha
*/
public String getChachaKey() {
return chachaKey;
}
/**
* Получить временную метку сообщения в миллисекундах
* @return временная метка сообщения в мсиллисекундах
*/
public long getTimestamp() {
return timestamp;
}
/**
* Получить идентификатор сообщения
* @return идентификатор сообщения
*/
public String getMessageId() {
return messageId;
}
/**
* Получить массив вложений в сообщении
* @return массив вложений в сообщении
*/
public List<Attachment> getAttachments() {
return attachments;
}
/**
* Получить закодированный с помощью AES ключ chacha
* @return ключ chacha
*/
public String getAesChachaKey() {
return aesChachaKey;
}
/**
* Устанавливает текстовое содержимое сообщения
* @param content текстовое содержимое сообщения
*/
public void setContent(String content) {
this.content = content;
}
/**
* Устанавливает ключ chacha для шифрования сообщения
* @param chachaKey
*/
public void setChachaKey(String chachaKey) {
this.chachaKey = chachaKey;
}
/**
* Устанавливает временную метку сообщения в миллисекундах
* @param timestamp временная метка сообщения в миллисекундах
*/
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
/**
* Устанавливает приватный ключ пользователя
* @param privateKey приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
/**
* Устанавливает идентификатор сообщения
* @param messageId идентификатор сообщения
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* Устанавливает массив вложений в сообщении
* @param attachments массив вложений в сообщении
*/
public void setAttachments(List<Attachment> attachments) {
this.attachments = attachments;
}
/**
* Устанавливает закодированный с помощью AES ключ chacha
* @param aesChachaKey ключ chacha
*/
public void setAesChachaKey(String aesChachaKey) {
this.aesChachaKey = aesChachaKey;
}
}

View File

@@ -0,0 +1,28 @@
package im.rosetta.packet;
import im.rosetta.packet.base.PacketBaseDialog;
import io.orprotocol.Stream;
/**
* Пакет для отметки сообщения как прочитанного
*/
public class Packet7Read extends PacketBaseDialog {
@Override
public void read(Stream stream) {
this.privateKey = stream.readString();
this.fromPublicKey = stream.readString();
this.toPublicKey = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.privateKey);
stream.writeString(this.fromPublicKey);
stream.writeString(this.toPublicKey);
return stream;
}
}

View File

@@ -0,0 +1,61 @@
package im.rosetta.packet;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Пакет обозначающий доставку сообщения получателю. Отправляется после успешной доставки сообщения получателю
*/
public class Packet8Delivery extends Packet {
private String messageId;
private String toPublicKey;
@Override
public void read(Stream stream) {
this.toPublicKey = stream.readString();
this.messageId = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.toPublicKey);
stream.writeString(this.messageId);
return stream;
}
/**
* Получить идентификатор доставленного сообщения
* @return идентификатор доставленного сообщения
*/
public String getMessageId() {
return messageId;
}
/**
* Получить публичный ключ получателя доставленного сообщения
* @return публичный ключ получателя доставленного сообщения
*/
public String getToPublicKey() {
return toPublicKey;
}
/**
* Установить идентификатор доставленного сообщения
* @param messageId идентификатор доставленного сообщения
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* Установить публичный ключ получателя доставленного сообщения
* @param toPublicKey публичный ключ получателя доставленного сообщения
*/
public void setToPublicKey(String toPublicKey) {
this.toPublicKey = toPublicKey;
}
}

View File

@@ -0,0 +1,72 @@
package im.rosetta.packet;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Пакет для уведомления о новом устройстве, авторизовавшемся с учетной записью пользователя
* Этот пакет может быть отправлен сервером всем авторизованным устройствам пользователя,
* чтобы уведомить их о том, что с учетной записью было авторизовано новое устройство, и предоставить информацию об этом устройстве (например, IP-адрес, тип устройства, операционная система и т.д.)
* Клиенты могут использовать эту информацию для отображения уведомления пользователю,
* а также для обеспечения безопасности учетной записи (например, если пользователь не узнает устройство, он может предпринять меры
* для защиты своей учетной записи, например, заблокировать вход для нового устройства)
*/
public class Packet9DeviceNew extends Packet {
private String ipAddress;
private String deviceId;
private String deviceName;
private String deviceOs;
@Override
public void read(Stream stream) {
this.ipAddress = stream.readString();
this.deviceId = stream.readString();
this.deviceName = stream.readString();
this.deviceOs = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.ipAddress);
stream.writeString(this.deviceId);
stream.writeString(this.deviceName);
stream.writeString(this.deviceOs);
return stream;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getDeviceOs() {
return deviceOs;
}
public void setDeviceOs(String deviceOs) {
this.deviceOs = deviceOs;
}
}

View File

@@ -0,0 +1,96 @@
package im.rosetta.packet.base;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
/**
* Базовый пакет для диалогов между пользователями
*
* ВОПРОС: Почему мы должны отправлять fromPublicKey с клиента, если сервер
* может получить fromPublicKey из хэндшейка?
* ОТВЕТ: Клиенты (оппоненты) должны понимать, от кого им приходит например пакет сообщения
* или печати с сервера (from), чтобы производить с отправителем какие-либо действия (например показать
* имя печатающего или отрендерить аватарку отправтеля сообщения), если бы поле fromPublicKey заполнял
* сервер - это бы выглядело не логично. Это не влияет на безопасность, так как каждый Exectuor
* верифицирует поле fromPublicKey сравнивая его с публичным ключом фактического отправителя.
*/
public class PacketBaseDialog extends Packet {
/**
* Публичный ключ отправителя
*/
public String fromPublicKey;
/**
* Публичный ключ получателя, может начинаться с #group: для групповых сообщений
*/
public String toPublicKey;
/**
* Приватный ключ отправителя
*/
public String privateKey;
/**
* Заглушка
*/
@Override
public void read(Stream stream) {}
/**
* Заглушка
*/
@Override
public Stream write() {return null;}
/**
* Получить публичный ключ отправителя
* @return публичный ключ отправителя
*/
public String getFromPublicKey() {
return fromPublicKey;
}
/**
* Получить публичный ключ получателя
* @return публичный ключ получателя
*/
public String getToPublicKey() {
return toPublicKey;
}
/**
* Получает приватный ключ пользователя
* @return приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public String getPrivateKey() {
return this.privateKey;
}
/**
* Устанавливает приватный ключ пользователя
* @param privateKey приватный ключ
* @deprecated с версии сервера 1.1 использование приватных ключей
* в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей.
*/
@Deprecated(since = "1.1", forRemoval = true)
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
/**
* Устанавливает публичный ключ отправителя
* @param fromPublicKey публичный ключ отправителя
*/
public void setFromPublicKey(String fromPublicKey) {
this.fromPublicKey = fromPublicKey;
}
/**
* Устанавливает публичный ключ получателя
* @param toPublicKey публичный ключ получателя
*/
public void setToPublicKey(String toPublicKey) {
this.toPublicKey = toPublicKey;
}
}

View File

@@ -0,0 +1,37 @@
package im.rosetta.packet.base;
import io.orprotocol.Stream;
import io.orprotocol.packet.Packet;
public class PacketBaseServer extends Packet {
private String server;
@Override
public void read(Stream stream) {
this.server = stream.readString();
}
@Override
public Stream write() {
Stream stream = new Stream();
stream.writeInt16(this.packetId);
stream.writeString(this.server);
return stream;
}
/**
* Получить сервер
* @return сервер
*/
public String getServer() {
return server;
}
/**
* Установить сервер
* @param server сервер
*/
public void setServer(String server) {
this.server = server;
}
}

View File

@@ -0,0 +1,52 @@
package im.rosetta.packet.runtime;
/**
* Вложение в сообщении
*/
public class Attachment {
private String id;
private String blob;
private AttachmentType type;
private String preview;
public Attachment(String id, String blob, AttachmentType type, String preview) {
this.id = id;
this.blob = blob;
this.type = type;
this.preview = preview;
}
/**
* Получить идентификатор вложения
* @return
*/
public String getId() {
return id;
}
/**
* Получить данные вложения в виде строки
* @return
*/
public String getBlob() {
return blob;
}
/**
* Получить тип вложения
* @return
*/
public AttachmentType getType() {
return type;
}
/**
* Получить превью вложения (например, для изображений)
* @return
*/
public String getPreview() {
return preview;
}
}

View File

@@ -0,0 +1,31 @@
package im.rosetta.packet.runtime;
/**
* Тип вложения в сообщении
*/
public enum AttachmentType {
IMAGE(0),
MESSAGES(1),
FILE(2),
AVATAR(3);
private final int code;
AttachmentType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static AttachmentType fromCode(int code) {
for (AttachmentType type : AttachmentType.values()) {
if (type.getCode() == code) {
return type;
}
}
throw new IllegalArgumentException("Invalid AttachmentType code: " + code);
}
}

View File

@@ -0,0 +1,33 @@
package im.rosetta.packet.runtime;
/**
* Решение по запросу на добавление устройства
*/
public enum DeviceSolution {
/**
* Принять запрос на добавление устройства
*/
ACCEPT(0),
/**
* Отклонить запрос на добавление устройства
*/
DECLINE(1);
private int code;
private DeviceSolution(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static DeviceSolution fromCode(int code) {
for (DeviceSolution solution : DeviceSolution.values()) {
if (solution.getCode() == code) {
return solution;
}
}
throw new IllegalArgumentException("Unknown DeviceSolution value: " + code);
}
}

View File

@@ -0,0 +1,36 @@
package im.rosetta.packet.runtime;
/**
* Этапы хэндшейка между клиентом и сервером.
*/
public enum HandshakeStage {
/**
* Успешный хэндшейк
* Такой пользователь может авторизованно взаимодействовать с сервером
*/
COMPLETED(0),
/**
* Необходима верификация устройства
* Такой пользователь должен подтвердить устройство (например, через код на другом устройстве)
*/
NEED_DEVICE_VERIFICATION(1);
private final int code;
HandshakeStage(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static HandshakeStage fromCode(int code) {
for (HandshakeStage stage : HandshakeStage.values()) {
if (stage.getCode() == code) {
return stage;
}
}
throw new IllegalArgumentException("Invalid HandshakeStage code: " + code);
}
}

View File

@@ -0,0 +1,68 @@
package im.rosetta.packet.runtime;
/**
* Обозначает подключенное к аккаунту устройство, с которого
* был произведен вход в систему.
*/
public class NetworkDevice {
private NetworkStatus networkStatus;
private String deviceName;
private String deviceOs;
private String deviceId;
private DeviceSolution deviceSolution;
public NetworkDevice() {
}
public NetworkDevice(NetworkStatus networkStatus, String deviceName, String deviceOs, String deviceId,
DeviceSolution deviceSolution) {
this.networkStatus = networkStatus;
this.deviceName = deviceName;
this.deviceOs = deviceOs;
this.deviceId = deviceId;
this.deviceSolution = deviceSolution;
}
public NetworkStatus getNetworkStatus() {
return networkStatus;
}
public void setNetworkStatus(NetworkStatus networkStatus) {
this.networkStatus = networkStatus;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getDeviceOs() {
return deviceOs;
}
public void setDeviceOs(String deviceOs) {
this.deviceOs = deviceOs;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public DeviceSolution getDeviceSolution() {
return deviceSolution;
}
public void setDeviceSolution(DeviceSolution deviceSolution) {
this.deviceSolution = deviceSolution;
}
}

View File

@@ -0,0 +1,42 @@
package im.rosetta.packet.runtime;
/**
* Используется в Packet19GroupInviteInfo для указания статуса пользователя в группе.
*/
public enum NetworkGroupStatus {
/**
* Пользователь уже в группе
*/
JOINED(0),
/**
* Пользователь не может вступить в группу, так как она не существует или произошла ошибка
*/
INVALID(1),
/**
* Пользователь не вступил в группу, но может вступить, так как она существует и он не был из нее исключен
*/
NOT_JOINED(2),
/**
* Пользователь исключен из группы и не может вступить в нее, так как он был из нее исключен администратором
*/
BANNED(3);
private final int code;
NetworkGroupStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static NetworkGroupStatus fromCode(int code) {
for (NetworkGroupStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("Unknown NetworkGroupStatus code: " + code);
}
}

View File

@@ -0,0 +1,34 @@
package im.rosetta.packet.runtime;
/**
* Используется в Packet16PushNotification для указания действия, которое нужно выполнить с токеном пуш уведомлений.
*/
public enum NetworkNotificationAction {
/**
* Подписать этот токен на пуш уведомления. Если токен уже был подписан, то ничего не произойдет.
*/
SUBSCRIBE(0),
/**
* Отписать этот токен от пуш уведомлений. Если токен не был подписан, то ничего не произойдет.
*/
UNSUBSCRIBE(1);
private final int code;
NetworkNotificationAction(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static NetworkNotificationAction fromCode(int code) {
for (NetworkNotificationAction action : values()) {
if (action.code == code) {
return action;
}
}
throw new IllegalArgumentException("Unknown NetworkNotificationAction code: " + code);
}
}

View File

@@ -0,0 +1,39 @@
package im.rosetta.packet.runtime;
/**
* Статус пользователя в сети
*/
public enum NetworkStatus {
ONLINE(0),
OFFLINE(1);
private final int code;
NetworkStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static NetworkStatus fromCode(int code) {
for (NetworkStatus status : NetworkStatus.values()) {
if (status.getCode() == code) {
return status;
}
}
throw new IllegalArgumentException("Invalid NetworkStatus code: " + code);
}
public static NetworkStatus fromBoolean(boolean status) {
if(status){
return NetworkStatus.ONLINE;
}
return NetworkStatus.OFFLINE;
}
public boolean toBoolean() {
return this.code == 0;
}
}

View File

@@ -0,0 +1,35 @@
package im.rosetta.packet.runtime;
/**
* Сущность для обозначения статуса сети
* пользователя по публичному ключу
*/
public class PKNetworkStatus {
public String publicKey;
public NetworkStatus networkStatus;
public PKNetworkStatus() {}
public PKNetworkStatus(String publicKey, NetworkStatus networkStatus) {
this.publicKey = publicKey;
this.networkStatus = networkStatus;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public NetworkStatus getNetworkStatus() {
return networkStatus;
}
public void setNetworkStatus(NetworkStatus networkStatus) {
this.networkStatus = networkStatus;
}
}

View File

@@ -0,0 +1,27 @@
package im.rosetta.packet.runtime;
public enum ResultCode {
SUCCESS(0),
ERROR(1),
INVALID(2),
USERNAME_TAKEN(3);
private final int code;
ResultCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static ResultCode fromCode(int code) {
for (ResultCode rc : ResultCode.values()) {
if (rc.getCode() == code) {
return rc;
}
}
throw new IllegalArgumentException("Invalid ResultCode code: " + code);
}
}

Some files were not shown because too many files have changed in this diff Show More