From b07f76ba1e7695e28e4ad19a3dc7a8698649ee6c Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Wed, 1 Apr 2026 16:06:35 +0200 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20call-pushes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 4 + .gitignore | 3 + pom.xml | 6 ++ .../executors/Executor26SignalPeer.java | 32 +++++-- .../dispatch/push/dispatchers/FCM.java | 3 +- .../dispatch/push/dispatchers/VoIPApns.java | 83 ++++++++++++++++++- src/main/java/io/orprotocol/Server.java | 1 - 7 files changed, 120 insertions(+), 12 deletions(-) diff --git a/.env b/.env index c21b809..4e0c135 100644 --- a/.env +++ b/.env @@ -14,6 +14,10 @@ SDU_SERVERS=http://10.211.55.2:7777 #Firebase Credentials FIREBASE_CREDENTIALS_PATH=serviceAccount.json +#Apple APNS +APNS_KEY_PATH=voip.p12 +APNS_P12_PASSWORD=rosetta1 +IOS_BUNDLE_ID=com.rosetta.dev #Каждые сколько дней будет очищаться буфер (максимальная дистанция синхронизации сообщений) BUFFER_CLEANUP_DAYS=7 diff --git a/.gitignore b/.gitignore index 47e61c3..ab2eaf7 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ target .settings .project .classpath +serviceAccount.json +build/*.p12 +*.p12 build/.env build/.env* diff --git a/pom.xml b/pom.xml index 4605919..a0a3469 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,12 @@ jakarta.persistence-api 3.1.0 + + + com.eatthepath + pushy + 0.15.4 + diff --git a/src/main/java/im/rosetta/executors/Executor26SignalPeer.java b/src/main/java/im/rosetta/executors/Executor26SignalPeer.java index 9ea8147..aea72be 100644 --- a/src/main/java/im/rosetta/executors/Executor26SignalPeer.java +++ b/src/main/java/im/rosetta/executors/Executor26SignalPeer.java @@ -1,10 +1,15 @@ package im.rosetta.executors; +import java.util.HashMap; +import java.util.UUID; + import im.rosetta.Failures; import im.rosetta.client.ClientManager; import im.rosetta.client.tags.ECIAuthentificate; import im.rosetta.packet.Packet26SignalPeer; import im.rosetta.packet.runtime.NetworkSignalType; +import im.rosetta.service.dispatch.push.PushNotifyDispatcher; +import im.rosetta.service.dispatch.runtime.PushType; import im.rosetta.service.services.ForwardUnitService; import io.g365sfu.Room; import io.orprotocol.ProtocolException; @@ -18,6 +23,7 @@ public class Executor26SignalPeer extends PacketExecutor { private ClientManager clientManager; private ForwardUnitService fus; + private PushNotifyDispatcher pushNotifyDispatcher = new PushNotifyDispatcher(); public Executor26SignalPeer(ClientManager clientManager, ForwardUnitService fus) { this.clientManager = clientManager; @@ -26,6 +32,8 @@ public class Executor26SignalPeer extends PacketExecutor { @Override public void onPacketReceived(Packet26SignalPeer packet, Client client) throws Exception, ProtocolException { + String src = packet.getSrc(); + String dst = packet.getDst(); ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); if (eciAuthentificate == null || !eciAuthentificate.hasAuthorized()) { /** @@ -35,6 +43,14 @@ public class Executor26SignalPeer extends PacketExecutor { client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); return; } + if(!src.equals(eciAuthentificate.getPublicKey())) { + /** + * Если src в пакете не совпадает с авторизованным PK клиента, то это может означать, что клиент пытается + * отправить сигнал от другого пользователя, отключаем его от сервера. + */ + client.disconnect(Failures.DATA_MISSMATCH); + return; + } NetworkSignalType type = packet.getSignalType(); if(type == NetworkSignalType.CALL) { /** @@ -50,6 +66,14 @@ public class Executor26SignalPeer extends PacketExecutor { this.clientManager.sendPacketToAuthorizedPK(packet.getSrc(), responsePacket); return; } + /** + * Получатель сигнала не занят, отправляем ему пуш уведомление о входящем звонке и сигнал CALL для инициализации звонка + */ + pushNotifyDispatcher.sendPush(dst, new HashMap<>(){{ + put("type", PushType.CALL); + put("dialog", src); + put("callId", UUID.randomUUID().toString()); + }}); } if(type == NetworkSignalType.CREATE_ROOM){ /** @@ -66,14 +90,8 @@ public class Executor26SignalPeer extends PacketExecutor { this.clientManager.sendPacketToAuthorizedPK(packet.getDst(), packet); return; } - /** - * TODO: Проверка на существование получателя - */ + this.clientManager.sendPacketToAuthorizedPK(packet.getDst(), packet); - /** - * TODO: Высокоприоритетный пуш для сигналов звонков, чтобы мобильные устройства могли показать - * интерфейс входящего звонка, даже если приложение находится в фоне - */ } } diff --git a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java index 036e27a..a181e88 100644 --- a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java +++ b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/FCM.java @@ -82,8 +82,7 @@ public class FCM extends Pusher { break; case PushType.CALL: /** - * Звонок для андроид используем high priority, чтобы уведомление доставлялось даже если устройство в режиме Doze, - * для iOS используем VoIP уведомление, которое доставляется даже если приложение убито + * Это только для Android, для iOS используется VoIP APNs с отдельным сертификатом */ androidConfig.setPriority(AndroidConfig.Priority.HIGH); messageBuilder.setAndroidConfig(androidConfig.build()); diff --git a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java index 2a37a94..cabe6f2 100644 --- a/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java +++ b/src/main/java/im/rosetta/service/dispatch/push/dispatchers/VoIPApns.java @@ -1,15 +1,94 @@ package im.rosetta.service.dispatch.push.dispatchers; +import java.io.File; import java.util.HashMap; +import java.util.concurrent.ExecutionException; + +import com.eatthepath.pushy.apns.ApnsClient; +import com.eatthepath.pushy.apns.ApnsClientBuilder; +import com.eatthepath.pushy.apns.PushNotificationResponse; +import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification; +import com.eatthepath.pushy.apns.util.TokenUtil; import im.rosetta.service.dispatch.push.Pusher; +import im.rosetta.service.dispatch.runtime.PushType; public class VoIPApns extends Pusher { + private ApnsClient client; + private String topic; + + public VoIPApns(){ + this.initializeApns(); + } + + private void initializeApns() { + try { + String p12Path = System.getenv("APNS_KEY_PATH"); + String p12Password = System.getenv("APNS_P12_PASSWORD"); + String bundleId = System.getenv("IOS_BUNDLE_ID"); + + if (p12Path == null || bundleId == null) { + throw new IllegalStateException("APNS_P12_PATH and IOS_BUNDLE_ID must be set"); + } + + this.topic = bundleId + ".voip"; + + this.client = new ApnsClientBuilder() + .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST) + .setClientCredentials(new File(p12Path), p12Password) + .build(); + } catch (Exception e) { + throw new RuntimeException("Failed to init VoIP APNs client", e); + } + } + @Override public void sendPush(String token, HashMap data) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'sendPush'"); + if(data.get("type") != PushType.CALL) { + /** + * Для VoIP APNs отправляем уведомления только о входящих звонках + */ + return; + } + try { + String normalizedToken = TokenUtil.sanitizeTokenString(token); + + String payload = """ + { + "aps": { "content-available": 1 }, + "type": "CALL", + "callId": "%s", + "from": "%s" + } + """.formatted( + escape(data.getOrDefault("callId", "")), + escape(data.getOrDefault("dialog", "")) + ); + + SimpleApnsPushNotification push = new SimpleApnsPushNotification( + normalizedToken, + topic, + payload, + null, // invalidation time + com.eatthepath.pushy.apns.DeliveryPriority.IMMEDIATE, + com.eatthepath.pushy.apns.PushType.VOIP // apns-push-type: voip + ); + + PushNotificationResponse response = client.sendNotification(push).get(); + + if (!response.isAccepted()) { + System.err.println("VoIP push rejected: " + response.getRejectionReason()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + private String escape(String v) { + return v == null ? "" : v.replace("\\", "\\\\").replace("\"", "\\\""); } } diff --git a/src/main/java/io/orprotocol/Server.java b/src/main/java/io/orprotocol/Server.java index f446c08..dfa705a 100644 --- a/src/main/java/io/orprotocol/Server.java +++ b/src/main/java/io/orprotocol/Server.java @@ -150,7 +150,6 @@ public class Server extends WebSocketServer { } } catch (Exception e) { client.disconnect(ServerFailures.BAD_PACKET); - e.printStackTrace(); } }