.NETCoreコンソールアプリからFCMでのPUSH通知

AndroidスマホにPUSH通知を送るというだけのことに、てこずりました。
参考にできそうな記事はたくさんあり、それを真似ているのに、なぜかPUSH通知されないのです。
とりあえず(自分は)こうやってできた、というメモ書きとして残します。

##  環境(状況)
C# .NETCore3.1
AndroidにPUSH通知を受信するサンプルアプリを作成・インストール済み
FCM(Firebase)にPUSH通知のためのプロジェクトを作成済み

## ゴール
FCMにPUSH通知のリクエストをするC#ソースコードを得る。Azure内で動くC# .NETCore3のソースがほしいが、まずはコンソールアプリでよい。

(追記)
Azureで実行する際に、key.p12というファイルの取得を行うコードを変更しました。


## やってみて分かったこと
- PUSH通知のリクエスト(WebAPI)を送るエンドポイント(URL)が変更されている。
- Google Api HTTP v1というAPIを使う。
- Googleサービスアカウントに対しPrivate keyを作成し、それをOAuth2.0のアクセストークン取得に使う


紹介記事のほとんどは旧のエンドポイントが使われています。FCMに移行された2019/5以降の記事さえ、旧のエンドポイントが使われています。
紛らわしいのは、いまだに旧のエンドポイントにFCM HTTP APIで送ることができ、そのリクエストのレスポンスは"success": 1を返すことです。~~でも、AndroidにはPUSH通知がきません。もしかすると、FCMに移行された2019/5/29以降に作られたFirebaseプロジェクトでは、このようにリクエストは成功するけどPUSH通知は発生しない、という挙動になるのかもしれません。~~ (編集) 誤解の可能性がありました。PUSHデータ内の通知ペイロード(notification)の有無でAndroid側の挙動が変わること、Androidアプリ側が正しくない可能性もあったこと、等々・・。以下、旧プロトコルが使えないという記載を削除しました。


旧

```
 POST https://fcm.googleapis.com/fcm/send
```

新

```
POST https://fcm.googleapis.com/v1/projects/{projectID}/messages:send
```

v1では新のほうのエンドポイントとなります。

旧のエンドポイントを使っている記事(例えばQiitaにもある)によれば、PostmanでPOSTコマンド一発でPUSH通知ができるようです。それに倣ってやってみると、確かにPOSTは成功し、レスポンスも"success": 1 が返ってきます。

一方、新のエンドポイントでは、APIがv1に変わっているので、上記のような記事の内容そのままとはいきません。さらに、v1ではOAuth2.0が使われておりアクセストークンを使うためやりとりが増えるはずなので、PostmanでPOSTコマンド一発、というわけにはいかなそうです。なのでPostmanで試してみることはやめ、いきなりC#コードを書いてみることにしました。

Googleドキュメントを読むと、一応、v1 APIでOAuthを使うにはどうしたらいいのかが書かれています。・・・が、いろんなことが沢山書かれているので、迷子になってしまいました。

結局こうした、というのを書き出してみると、
作成済みのFirebaseプロジェクトで、サービスアカウントに対し、Private keyを作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/60c70282-dfae-b81b-3005-d08416e7d128.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/032bfab0-2837-7953-9558-1e154acadebb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/5f1b3cb9-bfc9-e0af-3043-ff156a085081.png)
Jsonファイルを作る手順もあるようですが、今回のコードではP12を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/e9bf7345-6b2c-9a2e-7c4d-aa4e73444cf4.png)
作成された(ダウンロードされた) P12ファイルを、key.p12にリネームします。

.NETCore3.1で、コンソールアプリを作成します。この手順の説明は割愛します。

NuGetでGoogle.Api.AuthおよびGoogle.Apis.FirebaseCloudMessaging.v1をインストールします。また、key.p12をプロジェクトに追加し、出力ディレクトリにコピーで「新しい場合はコピーする」とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/3fd43f0d-69f4-b79e-d2d8-2230d2d4f4d3.png)

(追記) Androidへの通知のときにchannelを追加できるよう修正。

```cs:Program.cs
using System;
using System.Security.Cryptography.X509Certificates;

using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.FirebaseCloudMessaging.v1;

namespace SendToFcm
{
    class Program
    {

        static void Main(string[] args)
        {
            String serviceAccountEmail = "hoge_webservices@appspot.gserviceaccount.com";


            // fine on local. but it doesn't work on Azure. not sure why.
            // var certificate = new X509Certificate2(@"key.p12", "notasecret", X509KeyStorageFlags.Exportable);
            // this works on Azure.
            var certificate = new X509Certificate2(file, "notasecret", X509KeyStorageFlags.MachineKeySet |
                                                                        X509KeyStorageFlags.PersistKeySet |
                                                                        X509KeyStorageFlags.Exportable);


            ServiceAccountCredential credential = new ServiceAccountCredential(
               new ServiceAccountCredential.Initializer(serviceAccountEmail)
               {
                   Scopes = new[] { Google.Apis.FirebaseCloudMessaging.v1.FirebaseCloudMessagingService.Scope.CloudPlatform }
               }.FromCertificate(certificate));

            // Create the service.
            var service = new FirebaseCloudMessagingService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "PUSH API Sample",
            });

            
            
            var requestBody = new Google.Apis.FirebaseCloudMessaging.v1.Data.SendMessageRequest();
            requestBody.Message = new Google.Apis.FirebaseCloudMessaging.v1.Data.Message();

            // token is a target which you want to send a PUSH to, and was made by an android-terminal with an android-app.
            requestBody.Message.Token = "eGxxxxx0VxI:APA91xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxNG-f";
            // Notification is to be seen on the android display.
            requestBody.Message.Android = new Google.Apis.FirebaseCloudMessaging.v1.Data.AndroidConfig();
            requestBody.Message.Android.Priority = "high";
            requestBody.Message.Android.Notification = new Google.Apis.FirebaseCloudMessaging.v1.Data.AndroidNotification();
            requestBody.Message.Android.Notification.Body = "body of your notif";
            requestBody.Message.Android.Notification.Title = "title of your notif";
            requestBody.Message.Android.Notification.ChannelId = "channelId";
            // Data is data used by android app.
            requestBody.Message.Data = new System.Collections.Generic.Dictionary<string, string>();
            requestBody.Message.Data.Add("key1", "123");
            requestBody.Message.Data.Add("key2", "456");

            // projectId is from Firebase projcect
            var projectId = "hoge_webservices";
            // "Projects" is an instance of ProjectsResource. Projects.Messages is a REST resource.
            var send = service.Projects.Messages.Send(requestBody, $"projects/{projectId}");

            try
            {
                // Send HttpRequest
                // Execute() returns a supposed-to-be "response" but no information in it, either success or error.
                // Errors will be thrown as exceptions.
                var response = send.Execute();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }


            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }
}
```

## 参考
結局、Google公式ドキュメントを拾い読みしました。

エンドポイントが変わっていますよ、というドキュメント
[以前の HTTP から HTTP v1 に移行する](https://firebase.google.com/docs/cloud-messaging/migrate-v1)
HTTP v1のPUSH通知に載せるデータが比較的簡単に説明されているドキュメント。
[FCM メッセージについて](https://firebase.google.com/docs/cloud-messaging/concept-options#customizing_a_message_across_platforms)
C#(.NET)コード実装例は、Google API クライアントライブラリのドキュメントにあります。残念ながらずばり自分の欲しいものがある可能性は低そう。
[API Client Libraries .NET ガイド Get Started](https://developers.google.com/api-client-library/dotnet/get_started)
そこからさらにOAuth周りの実装は下記のページ。ここから、"サービスアカウント"を使えばよさそうだとわかり、"Service Account"のところのサンプルコードにより、ServiceAccountCredentialやxxxService(⇒FirebaseCloudMessagingService)というクラスを使えばよいことが分かります。
[API Client Libraries .NET ガイド OAuth 2.0](https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth)
ここら辺までわかったら、GitHubのソースを見ながら、FirebaseCloudMessagingServiceを使ったサンプルコードを作ってみます。
[GitHub googleapis/google-api-dotnet-client](https://github.com/googleapis/google-api-dotnet-client)
[そこからAPIごとのクラス](https://github.com/googleapis/google-api-dotnet-client/tree/master/Src/Generated)
[そこからFCM](https://github.com/googleapis/google-api-dotnet-client/tree/master/Src/Generated/Google.Apis.FirebaseCloudMessaging.v1)

上記のGitHubのソースから、さらにProjectsResourceおよびProjectsResource.MessagesResourceへ進めばよいと見当がつきます。ここで下記のAPIドキュメントが参考になることがわかりました。
[APIリファレンス REST Resource: projects.messages](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages)
このAPIリファレンスではメソッドを試す機能があるようです。ここでの例なら、Methodsのsendをクリックすると下記ページに移り、そこにTry　this APIとあります。私は残念ながらOAuthをどうすればよいか分からず使えませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/383511bf-d5b9-3d7a-d09d-0f5070ea942b.png)