プッシュ通知とは端末からアクセスがなくてもサーバ側から能動的に情報を通知することができる仕組みです。TwitterだとTwitterにアクセスしていないときでも「○○さんがリツイートしました」という通知がきますが、このような機能を今回は実装することにします(いまはTwitterではなくX。リツイートではなくリポストと名称変更されているが…)。
Contents
クライアントサイドの処理
HTML部分を示します。
[Push通知を許可する]ボタンをクリックすると通知の許可設定がおこなわれます。そのあと[Push通知のテスト]ボタンをクリックするとPush通知がおこなわれます。この場合、通知はクリックした自分自身だけでなく全員に送られます。[Push通知をやめる]をクリックすると通知はされなくなります。
テストが終わったら[Push通知をやめる]をクリックしておいてください。誰かがテストボタンをクリックするたびに通知が来るとうっとうしいことになるので…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>鳩でもわかるプッシュ通知</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script defer src="./service-worker.js"></script> <script src="./index.js"></script> </head> <body> <button onclick="allowWebPush()">Push通知を許可する</button> <button onclick="testWebPush()">Push通知のテスト</button> <button onclick="clearAllowWebPush()">Push通知をやめる</button> </body> </html> |
サービスワーカー
JavaScript部分ですが、サービスワーカーの部分を先に示します。
ここではプッシュ通知を受け取ったときとプッシュ通知をクリックしたときの動作を定義しています。
service-worker.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function base64Decode(text,charset){ return fetch(`data:text/plain;charset=${charset};base64,`+text).then(response=>response.text()); } // プッシュ通知を受け取ったとき self.addEventListener('push', async function (event) { let msg = event.data.text(); msg = await base64Decode(msg); msg = msg.split('!|!'); // メッセージは'!|!'で連結された形で来るので分割する const title = msg[0]; const options = { body: msg[1] }; event.waitUntil(self.registration.showNotification(title + "", options)); }); // プッシュ通知のクリック時 self.addEventListener('notificationclick', function (event) { event.notification.close(); }); |
Push通知の処理をするためには公開鍵と秘密鍵が必要なのですが、これはこのサイトで生成します。
サービスワーカーの登録
index.jsではまずサービスワーカーを登録します。
index.js
1 2 3 4 5 |
self.addEventListener('load', async () => { if ('serviceWorker' in navigator) { window.sw = await navigator.serviceWorker.register('./service-worker.js', {scope: './'}); } }); |
Endpoint、UserPublicKey、UserAuthTokenの取得
Push通知をするためにはEndpoint、UserPublicKey、UserAuthToken(上記の公開鍵とは別物)が必要です。これを取得する関数を定義します。
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
class Subscription{ constructor(){ this.Endpoint = ''; this.UserPublicKey = ''; this.UserAuthToken = ''; } } async function getSubscription(){ if ('Notification' in window) { let permission = Notification.permission; if (permission === 'denied') { alert('Push通知が拒否されているようです。ブラウザの設定からPush通知を有効化してください'); return null; } } // https://web-push-codelab.glitch.me/ で取得した公開鍵をセットする // 公開して差し支えない鍵なのでそのまま書く const appServerKey = 'BKNAkRlMZG_nQ9u7-I7v99WsAKviCK3P51w0UKVqVkwuZi3wn8YXeHI5NJ7py_fR_wGVfcp3frMtxJLFGmGuGcg'; const applicationServerKey = urlB64ToUint8Array(appServerKey); // push managerにサーバーキーを渡し、トークンを取得 let subscription = undefined; try { subscription = await window.sw.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }); } catch (e) { alert('Push通知機能が拒否されたか、エラーが発生したのでPush通知は送信されません。'); return null; } // 必要なトークンを変換して取得 const key = subscription.getKey('p256dh'); const token = subscription.getKey('auth'); let obj = new Subscription(); obj.Endpoint = subscription.endpoint; obj.UserPublicKey = btoa(String.fromCharCode.apply(null, new Uint8Array(key))); obj.UserAuthToken = btoa(String.fromCharCode.apply(null, new Uint8Array(token))); return obj; } function urlB64ToUint8Array (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; } |
取得されたEndpoint、UserPublicKey、UserAuthTokenをサーバーに送り、これをデータベースに保存します。
Endpoint、UserPublicKey、UserAuthTokenをサーバーに送信する
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function allowWebPush() { let subscription = await getSubscription(); if(subscription == null) return; $.ajax("./allow-post.php",{ type:"POST", data:{ addOrRemove: 'add', endpoint: subscription.Endpoint, userPublicKey: subscription.UserPublicKey, userAuthToken: subscription.UserAuthToken, } }); alert('Push通知を許可しました'); } |
Push通知する
[Push通知のテスト]ボタンがクリックされたらPush通知をします。サンプルで通知しているのは通知時刻だけですが、ほかにも送れます。送信に成功したら成功した旨をアラートで表示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
function testWebPush(){ let date = new Date(); let nowText = date.toDateString() + ' ' + date.toLocaleTimeString(); $.ajax("./send.php",{ type:"POST", data:{ n_title: "Push通知テスト", n_body: "現在日時:" + nowText, } }); } |
Push通知の解除
Push通知を解除する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function clearAllowWebPush() { let subscription = await getSubscription(); if(subscription == null) return; $.ajax("./allow-post.php",{ type:"POST", data:{ addOrRemove: 'remove', endpoint: subscription.Endpoint, userPublicKey: subscription.UserPublicKey, userAuthToken: subscription.UserAuthToken, } }); alert('Push通知をクリアしました_'); } |
サーバーサイドの処理
Composerをインストールして、Composerを使ってweb-pushというライブラリをインストールします。
1 |
composer require minishlink/web-push |
関数の定義
必要な関数を先に定義します。
GetDbPath関数はデータベースのパスを取得する関数です。
functions.php
1 2 3 |
function GetDbPath(){ return "./db.sqlite3"; } |
CreateSubscriptionTable関数はデータベース上にテーブルを作成します。
functions.php
1 2 3 4 5 6 7 8 9 10 |
function CreateSubscriptionTable($db){ $text = 'CREATE TABLE if not exists SUBSCRIPTION_TABLE ('; $text .= 'id integer primary key autoincrement,'; $text .= 'endpoint text,'; $text .= 'user_public_key text,'; $text .= 'user_auth_token text);'; $sql = $text; $db->query($sql); } |
ExistSubscription関数は引数で渡されたデータをもつレコードがあるか調べます。
functions.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function ExistSubscription($db, $endpoint, $user_public_key, $user_auth_token){ $sql = "SELECT * FROM SUBSCRIPTION_TABLE WHERE endpoint = :endpoint "; $sql .= "AND user_public_key = :user_public_key AND user_auth_token = :user_auth_token"; $stmt = $db->prepare($sql); $stmt->bindValue(':endpoint', $endpoint); $stmt->bindValue(':user_public_key', $user_public_key); $stmt->bindValue(':user_auth_token', $user_auth_token); $stmt->execute(); $arr = $stmt->fetch(); if (is_array($arr) == true) return true; else return false; } |
InsertSubscription関数は引数で渡されたデータをもつレコードを追加します。
functions.php
1 2 3 4 5 6 7 8 |
function InsertSubscription($db, $endpoint, $user_public_key, $user_auth_token){ $sql = "INSERT INTO SUBSCRIPTION_TABLE (endpoint, user_public_key, user_auth_token) VALUES (:endpoint, :user_public_key, :user_auth_token)"; $stmt = $db->prepare($sql); $stmt->bindValue(':endpoint', $endpoint); $stmt->bindValue(':user_public_key', $user_public_key); $stmt->bindValue(':user_auth_token', $user_auth_token); $stmt->execute(); } |
DeleteSubscription関数は引数で渡されたデータをもつレコードを削除します。
functions.php
1 2 3 4 5 6 7 8 9 |
function DeleteSubscription($db, $endpoint, $user_public_key, $user_auth_token){ $sql = "DELETE FROM SUBSCRIPTION_TABLE WHERE endpoint = :endpoint "; $sql .= "AND user_public_key = :user_public_key AND user_auth_token = :user_auth_token"; $stmt = $db->prepare($sql); $stmt->bindValue(':endpoint', $endpoint); $stmt->bindValue(':user_public_key', $user_public_key); $stmt->bindValue(':user_auth_token', $user_auth_token); $stmt->execute(); } |
GetSubscriptions関数はすべてのレコードを返します。
functions.php
1 2 3 4 5 6 |
function GetSubscriptions($db){ $sql = "SELECT * FROM SUBSCRIPTION_TABLE"; $stmt = $db->prepare($sql); $stmt->execute(); return $stmt->fetchAll(); } |
Push通知の許可と解除の処理
allow-post.phpにPush通知を許可するリクエストが来たらこれをデータベースに追加し、解除するリクエストがきたらデータベースから削除する処理を示します。
allow-post.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php require_once('functions.php'); if(isset($_POST["addOrRemove"]) && isset($_POST["endpoint"]) && isset($_POST["userPublicKey"]) && isset($_POST["userAuthToken"])){ $db_path = GetDbPath(); $db = new PDO("sqlite:{$db_path}"); if($_POST["addOrRemove"] == 'add'){ if(!ExistSubscription($db, $_POST["endpoint"], $_POST["userPublicKey"], $_POST["userAuthToken"])){ InsertSubscription($db, $_POST["endpoint"], $_POST["userPublicKey"], $_POST["userAuthToken"]); } } if($_POST["addOrRemove"] == 'remove'){ DeleteSubscription($db, $_POST["endpoint"], $_POST["userPublicKey"], $_POST["userAuthToken"]); } $db = null; } |
Push通知の実行
[Push通知のテスト]がクリックされたらsend.phpにリクエストが来るのでPush通知を許可している端末に通知をおこないます。
send.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<?php require_once 'vendor/autoload.php'; require_once('functions.php'); use Minishlink\WebPush\WebPush; use Minishlink\WebPush\Subscription; if(!isset($_POST["n_title"]) || !isset($_POST["n_body"])) exit("error"); const VAPID_SUBJECT = 'https://lets-csharp.com'; // サイトのドメイン // https://web-push-codelab.glitch.me/ で取得した公開鍵と秘密鍵をセットする // 公開鍵が公開して差し支えない鍵なのでそのまま書く。秘密鍵は秘密です。 const PUBLIC_KEY = 'BKNAkRlMZG_nQ9u7-I7v99WsAKviCK3P51w0UKVqVkwuZi3wn8YXeHI5NJ7py_fR_wGVfcp3frMtxJLFGmGuGcg'; const PRIVATE_KEY = '自分で取得してね'; $auth = [ 'VAPID' => [ 'subject' => VAPID_SUBJECT, 'publicKey' => PUBLIC_KEY, 'privateKey' => PRIVATE_KEY, ] ]; $webPush = new WebPush($auth); // 通知する文字列をつくる $body_msg = base64_encode($_POST["n_title"].'!|!'.$_POST["n_body"]); // データベースからpush通知するための情報を取得して通知する $db_path = GetDbPath(); $db = new PDO("sqlite:{$db_path}"); $arr = GetSubscriptions($db); foreach ($arr as $subscription){ $sub = Subscription::create([ 'endpoint' => $subscription['endpoint'], 'publicKey' => $subscription['user_public_key'], 'authToken' => $subscription['user_auth_token'], ]); $webPush->sendOneNotification( $sub, $body_msg ); } $db = null; |