Предисловие


Приветствую Вас. Недавно передо мной стала задача — настроить Push-уведомления на сайте. С этим я столкнулся впервые и во много разобраться мне помогла эта статья. В ней же уже есть описание серверной стороны, но, в процессе изучения данной темы я обнаружил более удобный способ реализации средствами самой библиотеки Firebase. Собственно, о нем я и хотел бы вам рассказать, т.к. внятного объяснения в интернете мне не удалось найти.

Также данная статья может быть полезна программирующим на Node.js, Python и Go, поскольку библиотека есть и на этих языках.

Непосредственно к сути


В данной статье я расскажу только о серверной стороне.
(клиентскую часть Вы можете настроить используя ту самую статью)
Итак:

  • Для начала Вам нужно зайти на сайт, зарегистрировать и создать проект.
  • Далее в левом верхнем углу нажимаем на шестерню и выбираем «Настройки проекта».
  • Переходим на вкладку «Сервисные аккаунты», выбираем интересующий нас язык, нажимаем на «создание закрытого ключа» и скачиваем сгенерированный файл
Данный JSON-файл содержит в себе конфигурацию необходимую для библиотеки Firebase.
Теперь займемся сервером

Для удобства объявим в application.properties путь к скаченному файлу

fcm.service-account-file = /path/to/file.json

Добавим необходимые зависимости в pom.xml

<dependency>
    <groupId>com.google.firebase</groupId>
    <artifactId>firebase-admin</artifactId>
    <version>6.7.0</version>
</dependency>

Создадим бин возвращающий наш JSON:

@ConfigurationProperties(prefix = "fcm")
@Component
public class FcmSettings {
    private String serviceAccountFile;

    public String getServiceAccountFile() {
        return this.serviceAccountFile;
    }

    public void setServiceAccountFile(String serviceAccountFile) {
        this.serviceAccountFile = serviceAccountFile;
    }
}

И сервис, в котором и будет вся логика отправки уведомлений:

@Service
public class FcmClient {


 public FcmClient(FcmSettings settings) {
    Path p = Paths.get(settings.getServiceAccountFile());
    try (InputStream serviceAccount = Files.newInputStream(p)) {
        FirebaseOptions options = new FirebaseOptions.Builder()
               .setCredentials(GoogleCredentials.fromStream(serviceAccount))
               .build();

         FirebaseApp.initializeApp(options);
     } catch (IOException e) {
         Logger.getLogger(FcmClient.class.getName())
                 .log(Level.SEVERE, null, e);
        }
    }

 public void sendByTopic(String topic, String actionUrl,
                         String imageUrl, String title, 
                         String body, String ttlInSeconds)
         throws InterruptedException, ExecutionException {
     WebpushNotification.Builder builder = WebpushNotification.builder();
     builder.addAction(new WebpushNotification.Action(actionUrl, "Открыть"));
     builder.setImage(imageUrl);
     builder.setTitle(title);
     builder.setBody(body);
     Message message = Message.builder().setTopic(topic)
            .setWebpushConfig(WebpushConfig.builder()
                     .putHeader("ttl", ttlInSeconds)
                     .setNotification(builder.build())
                     .build())
             .build();

     String response = FirebaseMessaging.getInstance()
             .sendAsync(message)
             .get();
     System.out.println("Sent message: " + response);

   }

public void subscribeUsers(String topic, List<String> clientTokens)
         throws  FirebaseMessagingException {
     for (String token : clientTokens) {
             TopicManagementResponse response = FirebaseMessaging.getInstance()
                    .subscribeToTopic(Collections.singletonList(token), topic);
             System.out.println(response.getSuccessCount()
                     + " tokens were subscribed successfully");
        }
    }

 public void sendPersonal(String clientToken, String title, 
                          String body, String iconUrl, 
                          String clickUrl, String ttlInSeconds)
         throws ExecutionException, InterruptedException {
     WebpushNotification.Builder builder = WebpushNotification.builder();
     builder.addAction(new WebpushNotification.Action(clickUrl, "Открыть"));
     builder.setImage(iconUrl);
     builder.setTitle(title);
     builder.setBody(body);
     Message message = Message.builder().setToken(clientToken)
             .setWebpushConfig(WebpushConfig.builder()
                     .putHeader("ttl", ttlInSeconds)
                     .setNotification(builder.build())
                     .build())
             .build();

     String response = FirebaseMessaging.getInstance()
             .sendAsync(message)
             .get();
     System.out.println("Sent message: " + response);
   }

}

Я: — Firebase, почему так много билдим?
Firebase: — Потому что
  1. Конструктор служит для инициализации FirebaseApp с использованием нашего JSON-файла
  2. Метод sendByTopic() производит отправку уведомлений пользователям подписанным на заданную тему.

    Поля:

    • topic — тема
    • actionUrl — ссылка, куда отправится пользователь при клике на уведомление
      Их можно добавлять несколько, но все отобразит не каждый браузер (ниже пример из Chroma)
    • imageUrl — ссылка на картинку
    • title — Оглавление уведомления
    • body — текст уведомления
    • ttlInSeconds — время актуальности уведомления
  3. Метод subscribeUsers() подписывет на тему (topic) пользователей (clientTokens).
    может выполняться асинхронно, для этого используется .subscribeToTopicAsync()

  4. Метод sendPersonal() реализует отправку персонального уведомления пользователю (clientToken)

Результат

image

Другой браузер

image
Иконок нет потому, что Ubuntu:)

Подводим итоги



По сути, библиотека Firebase собирает нам JSON примерно такого вида:

{from: "Server key"
?
     notification: {
??          title: "Привет Habr"
          actions: (1) [???
                 0: Object { 
                         action: "https://habr.com/ru/top/",
                         title: "Открыть" }
                             ]??
          length: 1??
          body: "как-то так"??
          image: "https://habrastorage.org/webt/7i/k5/77/7ik577fzskgywduy_2mfauq1gxs.png"??
          }
?priority: "normal"}

А на стороне клиента Вы уже парсите его, как нравится.

Спасибо за внимание!

Полезные ссылки:


firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/messaging/WebpushNotification

habr.com/ru/post/321924/#otpravka-uvedomleniy-s-servera

firebase.google.com/docs/web/setup

Комментарии (6)


  1. mikaakim
    02.03.2019 10:24

    Коду в этой статье бы код-ревью пройти, а потом уже в народ)


    1. Mister1Burger Автор
      02.03.2019 11:21

      У меня есть возможность редактирования статьи. Поэтому, с радостью выслушаю Ваши предложения по ее улучшению.


      1. mikaakim
        02.03.2019 11:53

        1) Где-то используете билдер в формате лесенки, где-то вызов каждый метода на каждой строке у экземпляра билдера
        2) ttlInSeconds в виде строки? замените на примитив
        3) Не проверяете существование файла настроек при создании бина
        4) Используете sout
        5) Метод с 6ю параметрами заменил бы конфиг-объектом
        6) принципы DRY


        1. Mister1Burger Автор
          02.03.2019 12:15

          Благодарю Вас. Постараюсь исправить эти недочеты.


  1. johnspade
    03.03.2019 01:32

    Используете ли очередь (MQ) для отправки уведомлений или они отправляются синхронно по событиям? Недавно задумался над этим вопросом: операция отправки push-уведомления может быть дорогой, нужно отправить сетевой запрос в Firebase, а то и сходить в БД за данными для уведомления. Стоит ли сразу озаботиться настройкой очереди для пушей, чтобы сервер не падал под нагрузкой при большом количестве уведомлений?


    1. Mister1Burger Автор
      03.03.2019 08:48

      Сложности на серверной стороне

      • Понятно, что идентификатор устройства, присылаемый пользователем, мы сохраняем в базу данных;
      • Идентификатор устройства хорошо бы привязывать к пользователю, чтобы отправлять персонализированные сообщения;
      • Стоит помнить, что пользователь у нас один, а устройств у него может быть несколько, также одним устройством могут пользоваться несколько пользователей;
      • Отправка уведомлений пользователям не самая дешевая операция и поэтому событие, инициирующее отправку уведомления, нужно ставить в очередь на отправку;
      • Только маленькие проекты с малым числом получателей могут позволить себе отправлять уведомления по событию, в течении того-же HTTP запроса;
      • Так у нас появляется система очередей на RabbitMQ, Redis и т.д.;
      • Появляются демоны/воркеры которые разбирают очередь и другие инструменты поддержки очередей;
      • Для увеличения скорости отправки можно распараллелить процесс и разнести его на несколько нод.