0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PHPとlevelDBでPWAのpush通知を実装

Last updated at Posted at 2021-06-03

はじめに

pwaでiOSのpush通知ができるようになるのはいつ頃でしょうか?
日本は特にiOSのシェアが大きいので、pwaがイニシアチブを取れるかどうかはこれにかかっていますね。

さて、web-push-phpもメジャーバージョンがあがって、php7.1以降にも対応するようになり、sendOneNotification()が廃止されたりしているようなので、あらためてこれを使ってpush通知ができるようになるまでをまとめてみます。

ついでに購読者管理を最近私的トレンドのlevelDBでやってみます。

動作確認要件

  • CentOS 7
  • PHP 7.4.0
  • web-push-php 4.0
  • levelDB 1.12

事前準備

sudo yum --enablerepo=epel install leveldb-devel

cd /usr/local/src/
git clone https://github.com/reeze/php-leveldb.git

cd php-leveldb/
phpize
./configure --prefix=/home/{you}/.phpenv/versions/7.4.0/lib/php/leveldb --with-libdir=lib64 --with-leveldb=/usr/include/leveldb --with-php-config=/home/{you}/.phpenv/versions/7.4.0/bin/php-config
make
make install

vi ~/.phpenv/versions/7.4.0/etc/php.ini
	extension=/home/{you}/.phpenv/versions/7.4.0/lib/php/extensions/no-debug-non-zts-20190902/leveldb.so

~/.phpenv/versions/7.4.0/etc/init.d/php-fpm restart

mkdir -p ~/projects/pwa/{public_html,logs}
cd ~/projects/pwa/

composer require minishlink/web-push
vi composer.json
    "minishlink/web-push": "^4.0"

composer update

npm install web-push
./node_modules/.bin/web-push generate-vapid-keys --json > pairs.txt

vi ~/projects/pwa/public_html/vapidPublicKey
    {上記pair.txt内の"publicKey"の値}
~/projects/pwa/public_html/index.html
<html>
<head>
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="PWA sandbox">
  <link rel="apple-touch-icon" href="/img/icon-192.png" sizes="192x192">
  <link rel="apple-touch-icon" href="/img/icon-256.png" sizes="256x256">
  <script async src="https://cdn.jsdelivr.net/npm/pwacompat@2.0.6/pwacompat.min.js"
    integrity="sha384-GOaSLecPIMCJksN83HLuYf9FToOiQ2Df0+0ntv7ey8zjUHESXhthwvq9hXAZTifA"
    crossorigin="anonymous"></script>
  <link rel="manifest" href="./manifest.json">
  <script src="common.js" defer></script>
</head>
<body>
  <h1>PWA sandbox</h1>
  <div>test</div>
</body>
</html>
~/projects/pwa/public_html/manifest.json
{
  "name": "PWA sandbox",
  "short_name": "PWA",
  "background_color": "#fff",
  "icons": [{
      "src": "/img/icon-256.png",
      "sizes": "256x256",
      "type": "image/png"
    },{
      "src": "/img/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }],
  "start_url": "/?utm_source=homescreen",
  "display": "standalone"
}
~/projects/pwa/public_html/service-worker.js
var CACHE_NAME = 'v1-0';
self.addEventListener('install', function(event) {
  console.log('[ServiceWorker] Install');
  event.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll([
        '{プリキャッシュしたい画面パス}'
      ]);
    })
  );
});

self.addEventListener('activate', function(event) {  
  console.log('[ServiceWorker] Activate');
  event.waitUntil(
    caches.keys().then(function(cache) {
      cache.map(function(name) {
        if(CACHE_NAME !== name) caches.delete(name);
      })
    })
  );
});

self.addEventListener('fetch', function(event) {
  console.log('[ServiceWorker] Fetch');
  event.respondWith(
    caches.match(event.request).then(function(res) {
      if(res) return res;
      return fetch(event.request);
    })
  );
});

self.addEventListener('push', function (event) {
    console.log('[ServiceWorker] Push');
    const title = 'test information';
    const options = {
        body: event.data.text(),
        tag: title,
        icon: '/img/icon-256.png',
        badge: '/img/icon-256.png'
    };
    event.waitUntil(self.registration.showNotification(title, options));
});
~/projects/pwa/public_html/common.js
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

navigator.serviceWorker.register('service-worker.js');
navigator.serviceWorker.ready
.then(function(registration) {
  return registration.pushManager.getSubscription()
  .then(async function(subscription) {
    if (subscription) {
      return subscription;
    }

    const response = await fetch('./vapidPublicKey');
    const vapidPublicKey = await response.text();
    const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);

    return registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: convertedVapidKey
    });
  });
}).then(function(subscription) {
  const key = subscription.getKey('p256dh');
  const token = subscription.getKey('auth');
  subscription['userPublicKey'] = btoa(String.fromCharCode.apply(null, new Uint8Array(key)));
  subscription['userAuthToken'] = btoa(String.fromCharCode.apply(null, new Uint8Array(token)));
  fetch('/register.php', {
    headers: {'Content-type': 'application/json'},
    method: "post",
    body: JSON.stringify(subscription)
  });
});
~/projects/pwa/common.php
<?php
define('APP_DIR', '/home/{you}/projects/pwa/');
define('LEVEL_DB', 'subscribers');
define('VAPID_SUBJECT', 'mailto:{you}@{your domain}');
define('PUBLIC_KEY', '「事前準備」の最後に取得したpair.txt内の"publicKey"の値');
define('PRIVATE_KEY', '「事前準備」の最後に取得したpair.txt内の"privateKey"の値');

define('LEVEL_DB_BASE_OPTIONS', serialize([
    'create_if_missing' => true,
    'error_if_exists' => false,
    'paranoid_checks' => false,
    'block_cache_size' => 8 * (2 << 20),
    'write_buffer_size' => 4<<20,
    'block_size' => 4096,
    'max_open_files' => 1000,
    'block_restart_interval' => 16,
    'compression' => 1,
    'comparator' => NULL,
]));
define('LEVEL_DB_READ_OPTIONS', serialize([
    'verify_check_sum' => false,
    'fill_cache' => true,
    'snapshot' => null
]));
define('LEVEL_DB_WRITE_OPTIONS', serialize([
    'sync' => false,
]));
~/projects/pwa/sendpush.php
<?php
require_once 'common.php';
require_once 'vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

if (trim($argv[1] ?? '') === '') {
    echo "need to input message";
    exit(1);
}
$message = $argv[1];

try {
    $db = new LevelDB(APP_DIR . LEVEL_DB, unserialize(LEVEL_DB_BASE_OPTIONS), unserialize(LEVEL_DB_READ_OPTIONS), unserialize(LEVEL_DB_WRITE_OPTIONS));
    $it = new LevelDBIterator($db);
    foreach($it as $endpoint => $pairs) {
        $pairs = unserialize($pairs);
        send_push($message, $endpoint, $pairs['public_key'] ?? null, $pairs['auth_token'] ?? null);
    }

} catch (Exception $e) {
    error_log($e->getMessage());
}

function send_push(String $message, String $endpoint, ?String $public_key, ?String $auth_token) {
    $subscription = Subscription::create([
        'endpoint' => $endpoint,
        'publicKey' => $public_key,
        'authToken' => $auth_token,
    //    'contentEncoding' => 'aes128gcm',
    ]);

    $auth = [
        'VAPID' => [
            'subject' => VAPID_SUBJECT,
            'publicKey' => PUBLIC_KEY,
            'privateKey' => PRIVATE_KEY,
        ]
    ];

    $webPush = new WebPush($auth);
    $report = $webPush->sendNotification(
        $subscription,
        $message,
        false,
        [],
        $auth
    );
    $webPush->flush();
}
~/projects/pwa/public_html/register.php
<?php
require_once '../common.php';
header("Content-Type: application/json; charset=utf-8");

$reqs = json_decode(file_get_contents('php://input'));
$endpoint = $reqs->endpoint ?? null;
$public_key = $reqs->keys->p256dh ?? null;
$auth_token = $reqs->keys->auth ?? null;

$is = false;
if ($endpoint && $public_key && $auth_token) {
    try {
        $db = new LevelDB(APP_DIR . LEVEL_DB, unserialize(LEVEL_DB_BASE_OPTIONS), unserialize(LEVEL_DB_READ_OPTIONS), unserialize(LEVEL_DB_WRITE_OPTIONS));
        $db->put($endpoint, serialize(compact('public_key', 'auth_token')));
        system('find ' . APP_DIR . LEVEL_DB . ' -type d -exec chmod 0770 {} \; && find ' . APP_DIR . LEVEL_DB . ' -type f -exec chmod 0660 {} \;');
        $is = true;
    } catch (Exception $e) {
        error_log($e->getMessage());
    }
}

echo json_encode($is);

まとめ

ざーっと書きましたが、フローとそれぞれの役割的には

  1. ~/projects/pwa/public_html/index.html に相当するURLにアクセス
  2. ~/projects/pwa/public_html/manifest.json でpwaに関するマニフェストの読み込み
  3. ~/projects/pwa/public_html/service-worker.js でpush通知等の振る舞いに関する定義の読み込み
  4. ~/projects/pwa/public_html/common.js でpush通知を要求
  5. ~/projects/pwa/public_html/register.php で要求されたpush通知の購読者を登録
  6. ~/projects/pwa/sendpush.php で任意タイミングでpush通知の実施

となります。
sendpush.php はターミナルから以下のように実施してください。

php ~/projects/pwa/sendpush.php {通知したいメッセージ}

これでpush通知を許可した全端末にlevelDBに登録されている購読者をフェッチしながら配信をおこないます。

ss.png
上記画像のような通知が届くのを確認できるかと思います。

以下からソースの利用もできます。
https://github.com/cybist/pwa_push_simple_all_in_one

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?