Как сделать push-уведомления на сайте для Chrome

С 2015 года начала стремительно набирать популярность технология Push API от Chrome. Все чаще, заходя на различные новостные (и не только новостные) сайты, посетителям вылетает вот такой системный фрейм с запросом:

Системное окно Google Chrome, запрашивающее разрешение на доставку уведомлений от сайта.

Я искренне считаю этот канал доставки контента (или привлечения пользователей, называйте как хотите) одним из самых перспективных. Возможно даже, через пару лет эти пуши будут таким же обязательным атрибутом каждого уважающего себя новостного сайта, коими сегодня являются RSS и паблики в социальных сетях.

Однако сегодня эта технология еще довольно молода и руководств по её использованию мало не только в отечественном интернете, но и в зарубежном. Для примера далеко ходить не надо — даже сам “Google”, когда анонсировал Push API, выпустил до слёз скудный пресс-релиз. И только сейчас, несколько часов покопавшись в гугловских FAQ для разработчиков, можно собрать информацию, чтобы собрать худо-бедно работающие скрипты для отправки Push-уведомлений своим читателям.

Как же сделать такие Push-уведомления для своего сайта? Тут есть два пути: использовать сторонние сервисы (они уже есть и некоторые из них очень даже неплохи) или создать собственное решение. Поскольку я сторонник минимального использования сторонних сервисов на сайте, то наш путь в Городе был предрешен.

Но, ради справедливости, стоит замолвить пару словечек и о внешних решениях. Признаюсь, я не очень изучал этот рынок (причина названа чуть выше), однако нельзя не упомянуть сервис OneSignal, самая привлекательная черта которого в том, что их услуги абсолютно бесплатны — зарабатывают они на продаже данных о посетителях клиентского сайта. Так же есть сервис Jeapie, на их стороне очень грамотный маркетинг и, как правило, хорошие отзывы. Однако стоит отметить, что в свое время от их услуг отказалась Медуза — Платформа была попросту не готова к тому количеству пушей, которые приходилось отправлять для огромной аудитории Медузы.

Реализация. Получение учетных данных от Google.

В исходных данных сайт, написанный на Rails 3.2.8 и задача сделать на нём пушер уведомлений для Chrome.

Первым делом необходимо перевести сайт на HTTPS (защищённый гипертекстовый протокол), то есть сделать для своего сайта SSL-сертификат. Без него пуши работать не будут (не проверял с обычным http, но так везде пишут). О том, что такое SSL, с чем его едят, как поставить сертификат на сервер и подключить его, я писать не буду — интернет пестрит подобными статьями. Лишь порекомендую для этих целей StartSSL. Это хороший Удостоверяющий центр, с которым дружат все известные мне браузеры, с нарочито простой процедурой регистрации и верификации (нужно лишь минимально знать английский язык) для получения абсолютно бесплатного SSL-сертификата начального уровня, в который можно включить еще пять(sic!) субдоменов.

Итак, мы получили сертификат и соответствующим образом настроили свой Nginx или Apache. Теперь в адресной строке браузера рядом с адресом нашего сайта горит зелёненьким симпатичнейший замочек, надпись https://, и, если вам не жалко денег, еще название организации.

Далее идем в Google Сloud Platform, где регистрируем новый проект, назвав его, например, MySite-Push.

Создание нового проекта в Coogle Cloud Platform

Через несколько секунд, когда проект будет создан, через раздел Подключение к API Google, по ссылке Включить и настроить API переходим в раздел, где целый список методов API для всех сервисов Google. Там мы должны включить метод Google Cloud Messaging из раздела Mobile API.

метод Google Cloud Messaging из раздела Mobile API

Скорее всего, при подключении к методу, Google попросит зарегистрировать дополнительные данные. Например, попросит уточнить откуда будет вызываться API, тогда мы указываем необходимый нам тип обработчика (например, Веб-сервер).

После чего указываем IP сервера (если спросит) и генерируем закрытый ключ, по которому будет происходить авторизация запросов. Этот ключ, разумеется, нам в будущем понадобится.

Сгенерированный закрытый ключ для доступа к Google API

После нажатия кнопки Готово мы окажемся на странице, где нужно будет указать домен и подтвердить на него права через инструменты Search Console. Об этом тоже довольно много статей в интернете, да и сама процедура интуитивно понятна. Так что не будем здесь останавливаться.

Далее возвращаемся на главную страницу нашего проекта MySite-Push и запоминаем индикатор нашего проекта. Он нам тоже пригодится.

По сути, нам больше от Coogle Cloud Platform ничего и не надо. Ключ и идентификатор.

Реализация. Первичная настройка сайта.

Теперь в корневую папку нашего сайта нам нужно добавить файл manifest.json, в котором будет указано следующее:

{
  "name": "mysite.ru Push test",  // Название сайта
  "display": "standalone",        // Указываем, где показывать уведомления
  "gcm_sender_id": "258466066904" // Тот самый идентификатор приложения в Google Cloud Platform
}

Это самый необходимый минимум информации для манифеста. Можно погуглить и узнать, какие дополнительные параметры там можно указывать.

Ну и добавить в секцию <head></head> мета-тег со ссылкой на манифест:

<link rel="manifest" href="/manifest.json">

Теперь там же, в корневой папке, создаем файл push.js, куда вписываем вот такой код:

'use strict';
function SendPushMe() {
  if ('serviceWorker' in navigator) {
    console.log('Service Worker is supported');
    navigator.serviceWorker.register('/sw.js').then(function() {
      return navigator.serviceWorker.ready;
    }).then(function(reg) {
      console.log('Service Worker is ready :^)', reg);
      reg.pushManager.subscribe({userVisibleOnly: true}).then(function(sub) {
        console.log('endpoint:', sub.endpoint);
        $.get( "https://mysite.ru/createpushadresat?adresat=" + sub.endpoint, function( data ) {});
      });
    }).catch(function(error) {
      console.log('Service Worker error :^(', error);
    });
  }
}

Что делает этот код? При вызове метода SendPushMe() он проверяет поддерживает ли браузер подписку на пуши. Затем, если поддерживает (в лог-консоли разработчика появится сообщение “Service Worker is supported”, в противном случае там же появится сообщение об ошибке), попытается зарегистрировать Service Worker, который скоро появится по адресу /sw.js. Именно в этот момент пользователь увидит запрос от браузера на подтверждение действия. Затем, если пользователь даст согласие, воркер будет зарегистрирован, а по адресу https://mysite.ru/createpushadresat GET-запросом в переменной adresat будет передан уникальный идентификатор браузера. Он же отобразится в лог-консоли разработчика.

Добавим ссылку на этот скрипт в <head></head> нашего сайта:

<script type="text/javascript" src="/push.js"></script>

Теперь создадим Service Worker. Это будет файл sw.js в корневой папке:

'use strict';
self.addEventListener('install', function(event) {
  event.waitUntil(self.skipWaiting());
});
self.addEventListener('push', function (event) {
  event.waitUntil(
    fetch('/latest.json').then(function (response) {
      if (response.status !== 200) {
        console.log('Latest.json request error: ' + response.status);
        throw new Error();
      }
      return response.json().then(function (data) {
        if (data.error || !data.notification) {
          console.error('Latest.json Format Error.', data.error);
          throw new Error();
        }
        var title = data.notification.title;
        var body = data.notification.body;
        var icon = 'https://mysite.ru/my_beautiful_push_icon.png';
        return self.registration.showNotification(title, {
          body: body,
          icon: icon,
          data: {
            url: data.notification.url
          }
        });
      }).catch(function (err) {
        console.error('Retrieve data Error', err);
      });
    })
  );
});
self.addEventListener('notificationclick', function (event) {
  event.notification.close();
  var url = event.notification.data.url;
  event.waitUntil(clients.openWindow(url));
});

Этот код после его вызова создает уведомление, данные для которого берет из файла latest.json, расположенного по адресу https://mysite.ru/latest.json. Если запрос этого файла не удался, или сайт ответил невалидным json, в лог-консоли разработчика появится соответствующая запись. Если же все хорошо, будет сформирован пуш. Пуш формируется из переменных body — это короткое текстовое сообщение, title — заголовок пуша, url — ссылка, по которой перейдет пользователь при клике на уведомление и icon — красивой и восхитительной (желательно квадратной, со стороной не меньше 150px) иконкой, на которой будет показан логотип сайта. Причем конкретно в этом скрипте icon — статичный параметр, а все остальные динамично обновляются из latest.json.

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

Ну и, наконец, создаем latest.json, из которого и будет браться информация для уведомлений. Думаю, не трудно догадаться, что лежать он будет в корневой папке. А вот и его содержимое:

{"notification":{"url":"https://mysite.ru/posts/1","title":"Крутейшая новость","body":"Мы сделали офигительную подписку для Chrome"},"_id":319}

Полагаю, объяснять что есть что в этом json’е нет смысла, всё и так понятно.


По идее, уже сейчас наш сайт, а вернее его фронтенд, практически полностью готов. Осталось лишь научиться говорить Google, что у нас появились новые обновления, чтобы тот, в свою очередь, сообщал об этом браузерам наших подписчиков. Ну и, разумеется, динамически обновлять наш latest.json. Однако, это уже в следующей части.

P.S. Кстати, уже сейчас можно вызвать SendPushMe() и посмотреть, что произойдет. Для этого, например, можно создать ссылку, по клику которой будет этот метод и вызываться:

<a href="#"  onclick="SendPushMe();return false">Вызвать SendPushMe</a>

И, если все сделано правильно, за кликом по такой ссылке высветится системное окошко с запросом:

Но не спешите пока давать разрешение. Нажмите на крестик и пока закройте окошко.

Если вы уже нажали, то откройте настройки сайта (нажав на зеленый замочек в адресной строке, если кто не знает) и в пункте “Оповещения” верните Глобальный параметр по умолчанию (“Спрашивать”).

Теперь откройте консоль разработчика F12 -> Console. Далее нажмите снова ссылку “Вызвать SendPushMe”.

В консоли должны последовательно появиться две строки, содержащие “Service Worker is supported” и “Service Worker is ready” соответственно. А сам браузер должен вновь запросить разрешение. И вот теперь можно дать своё согласие.

В итоге в вашей консоли разработчика должны появиться три таких записи:

Лог подписки на Push в браузере Chrome

Строка, содержащая Endpoint, очень важна нам. Она содержит уникальный идентификатор браузера-подписчика. Причем идентификатор этот уникален не просто для самого браузера, а для пары “Браузер — Сервер”. То есть, если вы вдруг попытаетесь отправить пуш по этому идентификатору от имени какого-то другого сайта, у вас просто ничего не получится.

Если помните, скрипт push.js отправляет GET-запрос. Именно Endpoint и передаётся серверу с этим запросом. Чуть позже мы научим сайт запоминать их, чтобы мы могли отправлять по этим идентификаторам пуши.

Что мы теперь имеем? У нас есть полноценный подписчик на уведомления от сайта и мы теперь умеем вычленять идентификаторы подписавшихся. Теперь сам Бог велел попробовать послать себе самому уведомление.

Для этого нужно сказать Google о новом уведомлении, а он, в свою очередь, сообщит об этом нашим подписчикам (или одному единственному, как сейчас у нас).

Google уведомляется специальным POST запросом на https://gcm-http.googleapis.com/gcm/send вот в такой форме:

Content-Type:application/json
Authorization:key=A...A //Закрытый ключ нашего приложения,
который мы получили в прошлой части.
{
  "to" : "bk3RNwTe3H0:CI2k_...", // Уникальный идентификатор подписчика.
  "data" : {
    "title": "Portugal vs. Denmark", // Данные для уведомления
    "text": "5 to 1"
  },
}

Как можно догадаться, этот запрос уведомляет одного конкретного подписчика, причем данные для уведомления передает прямо в запросе. Однако, у нас содержимое уведомления берется из latest.json. Да и отправлять каждому подписчику персональный запрос слишком жирно. Хотя, если вам нужно отправлять индивидуальные и персональные уведомления, этот вариант как-раз вам подойдет. Однако мы будем отправлять запросы пакетно, для чего используем вот такую схему:

Content-Type:application/json
Authorization:key=A...A //Закрытый ключ нашего приложения,
который мы получили в прошлой части.
{
  "registration_ids" : {
    [...] // Массив идентификаторов подписчиков.
  },
}

В таком варианте, поскольку запрос не содержит данных для самого уведомления, браузер будет обращаться к Service Worker, от которого и будет получать информацию.

Давайте через терминал (SSH-консоль) сформируем и отправим запрос:

curl --header "Authorization: key=AI...3A" \
--header Content-Type:"application/json" \
https://gcm-http.googleapis.com/gcm/send \
-d "{\"registration_ids\":[\"cym...W3\"]}"

И УРА!

SSH-консоль с отправленным POST-запросом и появившийся от него Push

На этом скриншоте видно и сам запрос и его итог — пришедшее спустя секунду уведомление.

Да, мы это сделали — самая главная и, кажется, малопонятная часть позади. Дальнейшая работа в этом направлении будет зависеть от того, на чём написан ваш сайт. И, если ваш сайт написан на Rails, то нам с вами всё еще по пути. Если же нет, то не торопитесь прощаться: в следующей заключительной части я еще вернусь к тем вопросам по теме Веб-уведомлений, решение которых будет полезно знать веб-разработчикам любой специализации.


Итак, Push-уведомления Chrome для сайта на Ruby On Rails

Сначала скажу, что пример этого кода валиден для сайта на Rails 3.2.8!

По сути, нам нужно сделать:

  1. Функционал, чтобы наш сайт запоминал идентификаторы подписчиков;
  2. Модель для создания push-уведомлений;
  3. Динамически обновляемый latest.json;
  4. Ну и добавить в контроллер пару строк кода, который будет при создании нового Push’а отправлять POST-запрос на сервер Google.

Если вы забыли, напомню, что в первой части мы сразу добавили в push.js строчку для формирования GET-запроса к нам на сайт, который передаёт идентификатор браузера каждого вновь подписавшегося человека.

Теперь сделаем так, чтобы наш сайт понимал, что это за запрос и сохранял данные. Первым делом создадим модель и контроллер для Pushsubscriber:

rails g model pushsubscriber browserkey:string
rake db:migrate
rails g controller pushsubscribers create delete

Далее добавим в routes.rb нужный нам путь:

match '/createpushadresat', to: 'pushsubscribers#create', via: :get

Теперь наш сайт переадресует входящие GET-запросы в контроллер pushsubscribers, где он будет обрабатываться методом create. Настроим же его:

def create
     @newsubscriber = Pushsubscriber.new(:browserkey => URI(params[:adresat].to_s).path.split('/').last)
     if @newsubscriber.save?
         render :text => 'ok'
     else
         render :text => 'fail'
     end
end

Сразу скажу, этот код лишь для того, чтобы показать, в каком направлении нужно разрабатывать. Он практически ничего не проверяет и не использует ни валидаторы, ни регекспы — не используйте его в таком виде. Проверяется лишь, чтобы входящий параметр содержал данные в виде ссылки, а затем из этой ссылки вычленяется последний после слэша участок и сохраняется в базе. Именно в таком виде push.js передает Endpoint. И именно в последней секции (за слэшем) endpoint’а содержится идентификатор браузера.

Итак, люди начали подписываться на наши обновления и теперь наша База данных пополняется идентификаторами. Пора начать рассылать им уведомления:

rails g scaffold notification title:string bodytext:string url:string succescount:integer
rake db:migrate

Эта команда Скаффолдом создаст нам минимально работающую модель notification c полями title, bodytext, url и successcount. Первые три — соответственно заголовок, текст и ссылка будущего оповещения, а successcount — количество успешно переданных пушей, которое нам будет любезно сообщать Google. Так же будут созданы контроллер с вьюхами для этой модели. Разумеется там еще нужно будет вписать сгенерированные вьюхи в общий дизайн сайта, а контроллер “огородить” before_filter’ами, чтобы “пушить” могли только люди с соответствующим доступом. Но это вы уже будете решать индивидуально. А сейчас мы лишь чуть-чуть (на самом деле совсем не чуть-чуть) исправим метод create в notifications_controller.rb:

require 'open-uri'
require 'multi_json'
require 'uri'
def create
   @notification = Notification.new(params[:notification])
   if @notification.save
      @adresats = Pushsubscriber.all.collect(&:browserkey)
      @keys = '{"registration_ids":'[email protected]_json+'}'.as_json
      uri = URI.parse("https://android.googleapis.com/gcm/send")
      http = Net::HTTP.new(uri.host,uri.port)
      http.use_ssl = true
      req = Net::HTTP::Post.new(uri.path)
      req['Authorization'] = 'key=A...A'       # Тут вписывается закрытый ключ
      req['Content-Type'] = 'application/json'
      res = http.request(req, @keys)
      parsed_json = ActiveSupport::JSON.decode(res.body)
      @notification.update_attribute(:success, parsed_json['success'].to_i)
   end
   redirect_to notifications_path
end

Этот код, если создано и сохранено новое уведомление (@notification), формирует и передает POST-запрос для Google (точно так же, как мы делали это выше), в котором в формате json по спецификации Google передаются ВСЕ идентификаторы наших подписчиков. Затем нам должен ответить Google своим json’ом. Из которого отпарсивается секция success, из которой сохраняется число — количество успешно переданных пушей. Так же там передается секция failure, в которой, соответственно, хранится число по той или иной причине недоставленных пушей. Вообще, можно самому посмотреть какие данные передает Google, может что-то еще решите сохранить.

И, как и в прошлый раз, данный код не учитывает, что Google может не ответить, или ответить сообщением об ошибке, а не валидным для парсинга json’ом. Учтите в своей разработке подобные случаи.

Ну а теперь пробуем создать на своем сайте пуш через форму Notification.new (созданную Скаффолдом) и… ВУАЛЯ! Система работает — нам пришло уведомление!

Правда содержимое для этого уведомления все еще берется из статичного latest.json. Осталось последнее — заставить этот файл обновляться динамически. Как это сделать? Очень просто, ведь у нас уже есть модель Notification, а в latest.json должно содержаться именно наше последнее Уведомление (то есть Notification.all.last). Для этого удаляем наш статический latest.json из корневой папки сайта и добавляем в routes.rb следующий маршрут:

match '/latest.json', to: 'notification#latestpush', via: :get

То есть теперь latest.json будет формироваться методом latestpush в контроллере notification. Создадим этот метод:

def latestpush
   @push = Pushnotification.all.last
   render json: '{"notification":{"url":"'[email protected]_s+'","title":"'[email protected]_s+'","body":"'[email protected]_s+'"},"_id":'[email protected]_s+'}'.as_json
end

Теперь создадим еще одно уведомление через форму Notification.new. И, наконец, мы видим в углу монитора пуш именно с тем содержимым, которое только что вбили в форму.

Казалось бы, это все? Все, да не совсем. Во-первых, мы сделали подписку, но не сделали отписку — будьте честны и предоставьте посетителям сайта такую возможность. Во-вторых, подписку-то мы сделали, но она пока малоинформативна и показывается всем, а не только пользователям Chrome, для которых подписка и делается. Ну и в-третьих, мы сделали на своем сайте очень интересную игрушку, у которой есть свои правила использования. Вот об этом всем мы и поговорим в следующей, третьей и, наконец-то, заключительной части нашего повествования о Chrome push API.