Firebase Admin SDK for Node.js を使ってFCMのメッセージをサーバーから送信する

要件

Admin SDKを使用するには、Node.js 10.10.0以上が必要です。

firebase-adminのインストール

package.jsonファイルを作成し、firebase-admin npmパッケージをインストールします。

npm init
npm install firebase-admin --save

秘密鍵の作成

  1. Firebaseコンソールでプロジェクトを開く
  2. 「プロジェクトの概要」→「Settings」→「新しい秘密鍵の生成」を選択する
  3. 「キーを生成」を選択し、秘密鍵ファイルをダウンロードする。

SDKを初期化する

秘密鍵のファイルを読み込んで、SDKを初期化します。

// 秘密鍵ファイルのパス
const serviceAccount = require("./serviceAccountKey.json");
const app = admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
});
// console.log(defaultApp.name); //=> '[DEFAULT]'

特定のデバイスにメッセージを送信する

メッセージを送信するデバイスの登録トークンを設定します。

const registrationToken = 'YOUR_REGISTRATION_TOKEN';

送信するメッセージを作成します。

const message = {
    data: { score: '850', time: '2:45' },
    token: registrationToken
};

提供された登録トークンに対応するデバイスにメッセージを送信します。

admin.messaging().send(message)
    .then((response) => {
        // 応答はメッセージID文字列です。
        console.log('メッセージが正常に送信されました:', response);
        defaultApp.delete();
    })
    .catch((error) => {
        console.log('メッセージの送信中にエラーが発生しました:', error);
        defaultApp.delete();
    });

複数のデバイスにメッセージを送信する

メッセージを送信するデバイスの登録トークンを設定します。
最大500の登録トークンを含むリストを作成します。

const registrationTokens = [
    'YOUR_REGISTRATION_TOKEN_1',
    'YOUR_REGISTRATION_TOKEN_N',
];

送信するメッセージを作成します。

const message = {
    data: { score: '850', time: '2:45' },
    tokens: registrationTokens,
}

提供された登録トークンに対応するデバイスにメッセージを送信します。

admin.messaging().sendMulticast(message)
    .then((response) => {
        console.log(response.successCount + ' 通のメッセージが正常に送信されました');
        if (response.failureCount > 0) {
            const failedTokens = [];
            response.responses.forEach((resp, idx) => {
                if (!resp.success) {
                    failedTokens.push(registrationTokens[idx]);
                }
            });
            console.log('失敗の原因となったトークンのリスト: ' + failedTokens);
        }
        defaultApp.delete();
    })
    .catch((error) => {
        console.log('メッセージの送信中にエラーが発生しました:', error);
        defaultApp.delete();
    });

トピックにメッセージを送信する

トピックを付与したメッセージを作成します。

// トピック名
const topic = 'weather';

const message = {
    data: { score: '850', time: '2:45' },
    topic: topic
};

トピックをサブスクライブしているデバイスにメッセージを送信します。

admin.messaging().send(message)
    .then((response) => {
        // 応答はメッセージID文字列です。
        console.log('メッセージが正常に送信されました:', response);
        app.delete();
    })
    .catch((error) => {
        console.log('メッセージの送信中にエラーが発生しました:', error);
        app.delete();
    });

HTTPプロキシやロードバランサーを通過して接続したクライアントのIPアドレスを取得するには

通常、クライアントのIPアドレスはREMOTE_ADDRで取得できる。

// クライアントのIPアドレスを取得するPHPのコード
$ip = $_SERVER['REMOTE_ADDR'];

HTTPプロキシやロードバランサーを通過して接続したクライアントのIPアドレスは、REMOTE_ADDRでは取得できない。
REMOTE_ADDRは、HTTPプロキシやロードバランサーのIPアドレスになる。

では、どうすれば取得できるのかというと、X-Forwarded-Forを使用する。

X-Forwarded-Forの構文は次のようになっている。

X-Forwarded-For: <client>, <proxy1>, <proxy2>

カンマで区切られたIPアドレスのうち、一番左のIPアドレスがクライアントのIPアドレスになる。

HTTPプロキシやロードバランサーを通過して接続したクライアントのIPアドレスを取得するPHPのコードは次のようになる。

/**
  * HTTPプロキシやロードバランサーを通過して接続したクライアントのIPアドレスを取得する
  * @return string
  */
function getClientIpAddress() {
    if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
        $xForwardedFor = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
        if (!empty($xForwardedFor)) {
            return trim($xForwardedFor[0]);
        }
    }
    if (isset($_SERVER['REMOTE_ADDR'])) {
        return (string)$_SERVER['REMOTE_ADDR'];
    }
    return "";
}

LaravelのEloquentでLike検索する

LaravelのEloquentでLike検索するとき、検索キーワードをエスケープしないと、予期しない結果が返ることがあります。

// エスケープしないコードの例
$products = Product::where('name', 'like', '%'.$name.'%')->get();

productsテーブルには、「オレンジ100%」と「オレンジ100個分」が登録されています。

>>> Product::all();
=> Illuminate\Database\Eloquent\Collection {#4148
     all: [
       App\Product {#3901
         id: 1,
         name: "オレンジ100%",
       },
       App\Product {#3533
         id: 2,
         name: "オレンジ100個分",
       },
     ],
   }

キーワード「100%」で検索します。

>>> $name = '100%';
=> "100%"

キーワードをエスケープせずに検索すると、キーワードを含まないレコードもヒットします。

>>> Product::where('name', 'like', '%'.$name.'%')->get();
=> Illuminate\Database\Eloquent\Collection {#3218
     all: [
       App\Product {#4151
         id: 1,
         name: "オレンジ100%",
       },
       App\Product {#4006
         id: 2,
         name: "オレンジ100個分",
       },
     ],
   }

キーワードをエスケープする関数を作成します。

>>> function escape_like(string $value, string $char = '\\'): string
... {
...     return str_replace(
...         [$char, '%', '_'],
...         [$char.$char, $char.'%', $char.'_'],
...         $value
...     );
... }

キーワードをエスケープして検索します。

>>> Product::where('name', 'like', '%'.escape_like($name).'%')->get();
=> Illuminate\Database\Eloquent\Collection {#4165
     all: [
       App\Product {#4170
         id: 1,
         name: "オレンジ100%",
       },
     ],
   }

正しい結果が得られました。

参考:Laravel: Escape “LIKE” clause?

WKWebViewのJavaScriptからNative側にメッセージを送る

WKWebViewで下記のHTMLを開く。
ボタンを押すと、Native側のメソッドが呼ばれ、メッセージを受け取る。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
</head>
<body>
<button onclick="window.webkit.messageHandlers.test1.postMessage('こんにちは')">こんにちは</button>
<button onclick="window.webkit.messageHandlers.test2.postMessage('こんばんは')">こんばんは</button>
</body>
</html>

Native側の(Objective-Cによる)実装は下記のようになる。

@interface ViewController () <WKScriptMessageHandler>

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    WKWebViewConfiguration* webViewConfiguration = [WKWebViewConfiguration new];
    [webViewConfiguration.userContentController addScriptMessageHandler:self name:@"test1"];
    [webViewConfiguration.userContentController addScriptMessageHandler:self name:@"test2"];
    WKWebView* webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:webViewConfiguration];
    [self.view addSubview:webView];
    [webView loadRequest:[[NSURLRequest alloc]initWithURL:[[NSURL alloc] initWithString:kURL]]];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSLog(@"%@", message.name);
    NSLog(@"%@", message.body);
}

@end

WKWebViewConfigurationのuserContentControllerのaddScriptMessageHandlerで、
メッセージを受け取るWKScriptMessageHandlerオブジェクトとメッセージハンドラーの名前と指定する。

たとえば、

[webViewConfiguration.userContentController addScriptMessageHandler:self name:@"test1"];

このコードでは、メッセージハンドラーの名前は”test1″を指定しているので、JavaScriptの

window.webkit.messageHandlers.test1.postMessage('こんにちは')

このコードが実行されたら、メッセージを受け取る。

JavaScript側の呼び出しは、

window.webkit.messageHandlers.メッセージハンドラー名.postMessage(送信するメッセージ)

となる。 

メッセージを受け取ったらWKScriptMessageHandlerの
「- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;」
が呼ばれる。

どのメッセージハンドラーから、何のメッセージが送られたかは、WKScriptMessageオブジェクトのnameとbodyで取得できる。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSLog(@"%@", message.name);
    NSLog(@"%@", message.body);
}