Como enviar mensagens com bibliotecas push da Web

Um dos pontos problemáticos ao trabalhar com push da Web é que o acionamento de uma mensagem push é extremamente "fiel". Para acionar uma mensagem push, um aplicativo precisa fazer uma solicitação POST a um serviço de push seguindo o protocolo de push da Web. Para usar push em todos os navegadores, você precisa usar o VAPID (também conhecido como chaves de servidor de aplicativos), que basicamente exige a configuração de um cabeçalho com um valor que comprova que seu aplicativo pode enviar mensagens para um usuário. Para enviar dados com uma mensagem push, eles precisam ser criptografados e cabeçalhos específicos precisam ser adicionados para que o navegador possa descriptografar a mensagem corretamente.

O principal problema com o acionamento de push é que, se você encontrar um problema, será difícil diagnosticá-lo. Isso está melhorando com o tempo e o suporte mais amplo aos navegadores, mas está longe de ser fácil. Por esse motivo, é altamente recomendável usar uma biblioteca para lidar com a criptografia, formatação e acionamento da sua mensagem push.

Se você realmente quiser saber mais sobre o que as bibliotecas estão fazendo, vamos abordar isso na próxima seção. Por enquanto, vamos analisar o gerenciamento de assinaturas e o uso de uma biblioteca push da Web existente para fazer as solicitações de push.

Nesta seção, usaremos a biblioteca de nós de push da Web. Outros idiomas terão diferenças, mas não serão muito diferentes. Estamos analisando o Node, já que é JavaScript e deve ser o mais acessível para os leitores.

Vamos passar pelas seguintes etapas:

  1. Envie uma assinatura para nosso back-end e salve-a.
  2. Recuperar assinaturas salvas e acionar uma mensagem push.

Salvando assinaturas

Salvar e consultar PushSubscriptions de um banco de dados varia de acordo com a linguagem do lado do servidor e a escolha do banco de dados, mas pode ser útil ver um exemplo de como isso pode ser feito.

Na página da Web de demonstração, o PushSubscription é enviado ao nosso back-end com uma solicitação POST simples:

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription),
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error('Bad status code from server.');
      }

      return response.json();
    })
    .then(function (responseData) {
      if (!(responseData.data && responseData.data.success)) {
        throw new Error('Bad response from server.');
      }
    });
}

O servidor Express na nossa demonstração tem um listener de solicitações correspondente para o endpoint /api/save-subscription/:

app.post('/api/save-subscription/', function (req, res) {

Nesse caminho, validamos a assinatura apenas para garantir que a solicitação esteja correta e não esteja cheia de lixo:

const isValidSaveRequest = (req, res) => {
  // Check the request body has at least an endpoint.
  if (!req.body || !req.body.endpoint) {
    // Not a valid subscription.
    res.status(400);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'no-endpoint',
          message: 'Subscription must have an endpoint.',
        },
      }),
    );
    return false;
  }
  return true;
};

Se a assinatura for válida, precisaremos salvá-la e retornar uma resposta JSON apropriada:

return saveSubscriptionToDatabase(req.body)
  .then(function (subscriptionId) {
    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({data: {success: true}}));
  })
  .catch(function (err) {
    res.status(500);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'unable-to-save-subscription',
          message:
            'The subscription was received but we were unable to save it to our database.',
        },
      }),
    );
  });

Esta demonstração usa o nedb para armazenar as assinaturas. É um banco de dados simples baseado em arquivos, mas você pode usar qualquer um. Estamos usando esse recurso apenas porque ele não requer configuração. Para produção, é melhor usar algo mais confiável. Eu costumo usar o bom e velho MySQL.

function saveSubscriptionToDatabase(subscription) {
  return new Promise(function (resolve, reject) {
    db.insert(subscription, function (err, newDoc) {
      if (err) {
        reject(err);
        return;
      }

      resolve(newDoc._id);
    });
  });
}

Envio de mensagens push

Quando se trata de enviar uma mensagem push, precisamos de algum evento para acionar o processo de envio de uma mensagem aos usuários. Uma abordagem comum é criar uma página de administrador para configurar e acionar a mensagem push. No entanto, você pode criar um programa para ser executado localmente ou qualquer outra abordagem que permita acessar a lista de PushSubscriptions e executar o código para acionar a mensagem push.

Nossa demonstração tem uma página "como administrador" que permite acionar um push. Como é apenas uma demonstração, é uma página pública.

Vou passar por cada etapa para fazer a demonstração funcionar. São etapas simples para que todos possam acompanhar, incluindo qualquer pessoa nova no Node.

Quando discutimos a inscrição de um usuário, abordamos a adição de um applicationServerKey às opções subscribe(). Precisamos dessa chave privada no back-end.

Na demonstração, esses valores são adicionados ao nosso app Node da seguinte forma (um código entediante, que eu sei, mas só quero que você saiba que não há mágica:

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

Em seguida, precisamos instalar o módulo web-push do nosso servidor Node:

npm install web-push --save

Em seguida, no script do Node, precisamos do módulo web-push da seguinte maneira:

const webpush = require('web-push');

Agora, podemos começar a usar o módulo web-push. Primeiro, precisamos informar ao módulo web-push sobre as chaves do servidor de aplicativos. Elas também são conhecidas como chaves VAPID, porque esse é o nome da especificação.

const vapidKeys = {
  publicKey:
    'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
};

webpush.setVapidDetails(
  'mailto:web-push-book@gauntface.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey,
);

Observe que também incluímos uma string "mailto:". Essa string precisa ser um URL ou um endereço de e-mail mailto. Na verdade, essa informação será enviada ao serviço de push da Web como parte da solicitação para acionar um push. Isso é feito para que, se um serviço de push da Web precisar entrar em contato com o remetente, ele tenha algumas informações que permitam isso.

Com isso, o módulo web-push estará pronto para uso. A próxima etapa é acionar uma mensagem push.

A demonstração usa o painel de adm. de simulação para acionar mensagens push.

Captura de tela da página "Administrador".

Clicar no botão "Trigger Push Message" gera uma solicitação POST para /api/trigger-push-msg/, que é o sinal para nosso back-end enviar mensagens push. Por isso, criamos a rota no Express para este endpoint:

app.post('/api/trigger-push-msg/', function (req, res) {

Quando essa solicitação é recebida, pegamos as assinaturas do banco de dados e, para cada uma, acionamos uma mensagem push.

return getSubscriptionsFromDatabase().then(function (subscriptions) {
  let promiseChain = Promise.resolve();

  for (let i = 0; i < subscriptions.length; i++) {
    const subscription = subscriptions[i];
    promiseChain = promiseChain.then(() => {
      return triggerPushMsg(subscription, dataToSend);
    });
  }

  return promiseChain;
});

A função triggerPushMsg() pode usar a biblioteca de push da Web para enviar uma mensagem à assinatura fornecida.

const triggerPushMsg = function (subscription, dataToSend) {
  return webpush.sendNotification(subscription, dataToSend).catch((err) => {
    if (err.statusCode === 404 || err.statusCode === 410) {
      console.log('Subscription has expired or is no longer valid: ', err);
      return deleteSubscriptionFromDatabase(subscription._id);
    } else {
      throw err;
    }
  });
};

A chamada para webpush.sendNotification() vai retornar uma promessa. Se a mensagem foi enviada com sucesso, a promessa será resolvida e não será necessário fazer nada. Se a promessa for rejeitada, será necessário examinar o erro, já que ele informará se o PushSubscription ainda é válido ou não.

Para determinar o tipo de erro de um serviço de push, analise o código de status. As mensagens de erro variam entre os serviços de push, e alguns são mais úteis que outros.

Neste exemplo, ela verifica os códigos de status 404 e 410, que são os códigos de status HTTP para "Not Found" e "Gone". Se recebermos uma dessas mensagens, isso significa que a assinatura expirou ou não é mais válida. Nesses casos, precisamos remover as assinaturas do nosso banco de dados.

No caso de algum outro erro, apenas throw err, o que fará com que a promessa retornada por triggerPushMsg() seja rejeitada.

Vamos abordar alguns dos outros códigos de status na próxima seção quando analisarmos o protocolo de push da Web com mais detalhes.

Depois de passar pelas assinaturas, precisamos retornar uma resposta JSON.

.then(() => {
res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({ data: { success: true } }));
})
.catch(function(err) {
res.status(500);
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({
    error: {
    id: 'unable-to-send-messages',
    message: `We were unable to send messages to all subscriptions : ` +
        `'${err.message}'`
    }
}));
});

Passamos pelas principais etapas de implementação:

  1. Crie uma API para enviar assinaturas da nossa página da Web para o back-end e salvá-las em um banco de dados.
  2. Crie uma API para acionar o envio de mensagens push (neste caso, uma API chamada do painel de administração simulado).
  3. Recupere todas as assinaturas do nosso back-end e envie uma mensagem para cada uma delas com uma das bibliotecas de push da Web.

Independentemente do back-end (Node, PHP, Python etc.), as etapas para implementar o push serão as mesmas.

A seguir, o que exatamente essas bibliotecas de push da Web estão fazendo por nós?

A seguir

Laboratórios de códigos