package im.rosetta.service.dispatch; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.messaging.AndroidConfig; import com.google.firebase.messaging.ApnsConfig; import com.google.firebase.messaging.Aps; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; import im.rosetta.database.repository.UserRepository; import im.rosetta.service.dispatch.runtime.PushType; import im.rosetta.service.services.UserService; /** * Класс для отправки push-уведомлений пользователям через Firebase Cloud Messaging (FCM). */ public class FirebaseDispatcher { private UserRepository userRepository = new UserRepository(); private UserService userService = new UserService(userRepository); private final ExecutorService executor = Executors.newFixedThreadPool(10); public FirebaseDispatcher() { initializeFirebase(); } /** * Инициализация Firebase Admin SDK */ private void initializeFirebase() { if (FirebaseApp.getApps().isEmpty()) { try { String firebaseCredentialsPath = System.getenv("FIREBASE_CREDENTIALS_PATH"); if (firebaseCredentialsPath == null || firebaseCredentialsPath.isEmpty()) { throw new IllegalStateException("FIREBASE_CREDENTIALS_PATH environment variable is not set"); } FileInputStream serviceAccount = new FileInputStream(firebaseCredentialsPath); FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .build(); FirebaseApp.initializeApp(options); } catch (IOException e) { throw new RuntimeException("Failed to initialize Firebase", e); } } } private Message buildMessage(String token, HashMap data) { String type = data.get("type"); if(type == null){ throw new IllegalArgumentException("Push notification type is required in data"); } ApnsConfig.Builder apnsConfig = ApnsConfig.builder(); AndroidConfig.Builder androidConfig = AndroidConfig.builder(); Message.Builder messageBuilder = Message.builder() .setToken(token) .putAllData(data); switch(type) { case PushType.READ: /** * Тихий тип уведомления для очистки отправленных уведомлений на устройстве, * не должен отображаться пользователю, поэтому не задаем звук и ставим contentAvailable для iOS и high priority для Android */ apnsConfig.setAps(Aps.builder().setContentAvailable(true).setSound("default").build()); androidConfig.setPriority(AndroidConfig.Priority.HIGH); messageBuilder.setApnsConfig(apnsConfig.build()); messageBuilder.setAndroidConfig(androidConfig.build()); break; case PushType.PERSONAL_MESSAGE: case PushType.GROUP_MESSAGE: /** * Уведомление о новом сообщении, должно отображаться пользователю, поэтому задаем звук и high priority для Android */ String body = type == PushType.PERSONAL_MESSAGE ? "New message" : "New group message"; apnsConfig.setAps(Aps.builder().setSound("default").setMutableContent(true).build()); androidConfig.setPriority(AndroidConfig.Priority.HIGH); messageBuilder.setApnsConfig(apnsConfig.build()); messageBuilder.setNotification(Notification.builder().setTitle( data.getOrDefault("title", "Rosetta") ).setBody(body).build()); messageBuilder.setAndroidConfig(androidConfig.build()); break; case PushType.CALL: /** * Звонок для андроид используем high priority, чтобы уведомление доставлялось даже если устройство в режиме Doze, * для iOS используем VoIP уведомление, которое доставляется даже если приложение убито */ androidConfig.setPriority(AndroidConfig.Priority.HIGH); messageBuilder.setAndroidConfig(androidConfig.build()); break; } return messageBuilder.build(); } public void sendPushNotification(String publicKey, HashMap data) { executor.submit(() -> { try { List tokens = userService.getNotificationsTokens(publicKey); if (tokens == null || tokens.isEmpty()) { return; } for (String token : tokens) { try { Message message = this.buildMessage(token, data); FirebaseMessaging.getInstance().send(message); } catch (Exception e) { e.printStackTrace(); } } } catch (Exception e) { e.printStackTrace(); } }); } /** * Отправляет push-уведомление нескольким пользователям (асинхронно) * @param publicKeys список публичных ключей пользователей * @param data данные уведомления */ public void sendPushNotification(List publicKeys, HashMap data) { executor.submit(() -> { for (String publicKey : publicKeys) { sendPushNotificationSync(publicKey, data); } }); } private void sendPushNotificationSync(String publicKey, HashMap data) { try { List tokens = userService.getNotificationsTokens(publicKey); if (tokens == null || tokens.isEmpty()) { return; } for (String token : tokens) { try { Message message = this.buildMessage(token, data); FirebaseMessaging.getInstance().send(message); } catch (Exception e) { e.printStackTrace(); } } } catch (Exception e) { // Логирование ошибки } } /** * Завершить работу executor при остановке приложения */ public void shutdown() { executor.shutdown(); } }