Предисловие
Приветствую Вас. Недавно передо мной стала задача — настроить 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: — Потому что
- Конструктор служит для инициализации FirebaseApp с использованием нашего JSON-файла
- Метод sendByTopic() производит отправку уведомлений пользователям подписанным на заданную тему.
Поля:
- topic — тема
- actionUrl — ссылка, куда отправится пользователь при клике на уведомление
Их можно добавлять несколько, но все отобразит не каждый браузер (ниже пример из Chroma)
- imageUrl — ссылка на картинку
- title — Оглавление уведомления
- body — текст уведомления
- ttlInSeconds — время актуальности уведомления
- Метод subscribeUsers() подписывет на тему (topic) пользователей (clientTokens).
может выполняться асинхронно, для этого используется .subscribeToTopicAsync()
- Метод sendPersonal() реализует отправку персонального уведомления пользователю (clientToken)
Результат
Другой браузер
Иконок нет потому, что 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)
johnspade
03.03.2019 01:32Используете ли очередь (MQ) для отправки уведомлений или они отправляются синхронно по событиям? Недавно задумался над этим вопросом: операция отправки push-уведомления может быть дорогой, нужно отправить сетевой запрос в Firebase, а то и сходить в БД за данными для уведомления. Стоит ли сразу озаботиться настройкой очереди для пушей, чтобы сервер не падал под нагрузкой при большом количестве уведомлений?
Mister1Burger Автор
03.03.2019 08:48Сложности на серверной стороне
- Понятно, что идентификатор устройства, присылаемый пользователем, мы сохраняем в базу данных;
- Идентификатор устройства хорошо бы привязывать к пользователю, чтобы отправлять персонализированные сообщения;
- Стоит помнить, что пользователь у нас один, а устройств у него может быть несколько, также одним устройством могут пользоваться несколько пользователей;
- Отправка уведомлений пользователям не самая дешевая операция и поэтому событие, инициирующее отправку уведомления, нужно ставить в очередь на отправку;
- Только маленькие проекты с малым числом получателей могут позволить себе отправлять уведомления по событию, в течении того-же HTTP запроса;
- Так у нас появляется система очередей на RabbitMQ, Redis и т.д.;
- Появляются демоны/воркеры которые разбирают очередь и другие инструменты поддержки очередей;
- Для увеличения скорости отправки можно распараллелить процесс и разнести его на несколько нод.
mikaakim
Коду в этой статье бы код-ревью пройти, а потом уже в народ)
Mister1Burger Автор
У меня есть возможность редактирования статьи. Поэтому, с радостью выслушаю Ваши предложения по ее улучшению.
mikaakim
1) Где-то используете билдер в формате лесенки, где-то вызов каждый метода на каждой строке у экземпляра билдера
2) ttlInSeconds в виде строки? замените на примитив
3) Не проверяете существование файла настроек при создании бина
4) Используете sout
5) Метод с 6ю параметрами заменил бы конфиг-объектом
6) принципы DRY
Mister1Burger Автор
Благодарю Вас. Постараюсь исправить эти недочеты.