Help us understand the problem. What is going on with this article?

FirebaseのCloud Messaging(FCM)とCloud FunctionsでWebプッシュ通知を行う

More than 1 year has passed since last update.

概要

この記事は、Webプッシュ通知にFirebase Cloud Messaging(FCM)とCloud Functionsを利用したデモアプリケーションの実装内容についての説明になります。
アプリケーションのデプロイはFirebase Hostingを利用しました。

環境

  • Windows 10 Professional
  • Node.js 6.14.0
  • Firebase-tools 3.18.6

参考

プロジェクトを生成する

プロジェクト名は「demo-webpush」とし、最初にfirebaseというコマンドラインツールでひな型を生成、それをベースに次の実装を行っていきます。

  • メッセージを受信する処理
    • Hostingで公開するウェブページ
      • 通知の認可処理
      • 登録トークンの取得処理
      • フロントエンドでメッセージを受信する処理
    • サービスワーカー
      • バックエンドでメッセージを受信する処理
  • メッセージを送信する処理
    • Cloud Functionsの関数
      • インフラインベントをトリガーとして起動

メッセージの受信処理とサービスワーカーはfirebase/quickstart-js/messaging - GitHubで公開されているリポジトリのコードをほぼそのまま流用しています。先にプロジェクト全体を確認したい場合はリポジトリをご覧ください。

開発環境

Node.js

  • バージョン6.xにしている理由は、Cloud Functionsの実行環境が6.14.0であるためです。
> node -v
6.14.0
> npm -v
3.10.10

firebase-tools

> firebase -V
3.18.6

ブラウザの通知設定の確認

動作確認で使用するブラウザはchromeのバージョン”67.0.3396.79(Official Build)”です。
chromeの設定で通知がブロックされていないか確認します。ブロックする設定だとプログラムが正常でもパーミッションや登録トークンの取得ができません。

通知設定の確認は、”設定” → ”詳細設定” → プライバシーとセキュリティ欄の”コンテンツの設定” → ”通知”です。
設定を変えていなければ、デフォルトの”送信する前に確認する”になっていると思います。

c0.png

次の図のように”ブロック”になっているとプッシュ通知はできません。

c1.png

プロジェクトディレクトリを作成

> mkdir demo-webpush
> cd demo-webpush

プロジェクトディレクトリの初期化

initコマンドでディレクトリを初期化します。いくつかの選択項目からプロジェクトの設定を決めていきますが、このプロジェクトでは、利用する機能にCloud Functions, Hosting、開発言語にJavaScript、Hostingのpublic directoryに"dist"を選択しました。

> firebase init

初期化直後のディレクトリの内容

distディレクトリ下のファイルがHostingに静的ページとしてデプロイされるリソースです。ここにサービスワーカーやmanifest.json、アイコンなどを配置していきます。
functionsディレクトリ下には、Cloud Functionsに関するファイルが配置されています。

/demo-webpush
  |
  +--- /dist
  |      |
  |      +--- index.html
  |
  +--- /functions
  |      |
  |      +--- node_modules
  |      +--- .eslintrc.json
  |      +--- index.js
  |      +--- package.json
  |
  +--- .firebaserc
  +--- firebase.json

Hostingへデプロイする

生成したひな型に手を入れず、そのままデプロイして動作するか確認してみます。
デプロイはdeployコマンドで行います。

> firebase deploy

// 省略...

+  Deploy complete!

Project Console: https://console.firebase.google.com/project/project*********/overview
Hosting URL: https://project*********.firebaseapp.com

デプロイが正常終了するとコンソールにHosting URLが表示されるので、そのURLにアクセスしてみます。
下の図のようなページが表示され、ピンクの下線のメッセージが表示されていればFirebase SDKが利用できる状態です。

hosting0.png

メッセージの受信処理を実装

Firebase SDKを利用してアプリケーション(index.html)に、通知の認可、登録トークンの取得、メッセージの受信処理を実装します。

アプリケーションにFirebase SDKを組み込む

ひな型に含まれるindex.htmlのheadタグには下記のようにFirebase SDKを読み込むscriptが記述されています。srcの始まりが"/__/"になっていますが、Hostingにデプロイする場合は、この記述でscriptを読み込むことができます。
また、Hostingでは初期化処理はinit.jsで自動的に行ってくれます。

original
<!-- update the version number as needed -->
<script defer src="/__/firebase/5.0.4/firebase-app.js"></script>
<!-- include only the Firebase features as you need -->
<script defer src="/__/firebase/5.0.4/firebase-auth.js"></script>
<script defer src="/__/firebase/5.0.4/firebase-database.js"></script>
<script defer src="/__/firebase/5.0.4/firebase-messaging.js"></script>
<script defer src="/__/firebase/5.0.4/firebase-storage.js"></script>
<!-- initialize the SDK after all desired features are loaded -->
<script defer src="/__/firebase/init.js"></script>

firebase-app.jsがコア機能なので一番最初に記述する必要があります。それ以外のscriptはオプションで必要な機能の分だけ追加します。
この記事では、firebase-auth.js、firebase-database.js、firebase-storage.jsは使用しないので削除しました。

modify
<!-- update the version number as needed -->
<script defer src="/__/firebase/5.0.4/firebase-app.js"></script>
<!-- include only the Firebase features as you need -->
<script defer src="/__/firebase/5.0.4/firebase-messaging.js"></script>
<!-- initialize the SDK after all desired features are loaded -->
<script defer src="/__/firebase/init.js"></script>

Firebase Hosting以外の環境にデプロイする場合

CDN経由になります。またinit.jsが無いので初期化処理は下記のようにコードを実装する必要があります。

<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-messaging.js"></script>

<script>
  var config = {
    // ...
  };
  firebase.initializeApp(config);
</script>

configオブジェクトに設定する値は、次の場所から取得できます。

Firebase Consoleのトップ画面から”ウェブアプリにFirebaseを追加”アイコンをクリックします。

送信者id0.png

図のようにスニペットが表示されるのでそのままコピー&ペーストします。
(図ではキーやコードなど一部をマスクしていますが、これらは公開してもよい情報です。)

送信者id1.png

ちなみに、スニペットの1行目のfirebase.jsは、firebase-app.jsと機能別に分かれているjsを1ファイルにまとめたものです。個別に読み込むより1回で読み込みたい場合はこのjsを利用できます。

manifest.jsonの作成

distディレクトリに下記の内容を持つmanifest.jsonを作成します。
gcm_sender_idの値は固定です。(誰でもどのアプリケーションでも同じ値を利用します。)

{
  "name": "Demo Web Push",
  "short_name": "Demo Web Push",
  "start_url": "/",
  "display": "browser",
  "orientation": "portrait",
  "background_color": "#3E4EB8",
  "theme_color": "#2F3BA2",
  "icons": [{
    "src": "logo.png",
    "sizes": "128x128",
    "type": "image/png"
  }],
  "gcm_sender_id": "103953800507"
}

このファイルを読み込むようindex.htmlに下記の行を追加します。

<link rel="manifest" href="/manifest.json">

アイコンの配置

distディレクトリにlogo.pngという名前の画像ファイルを配置します。
この画像は、ポップアップ通知のアイコンとして使用します。

logo.png

通知の認可、登録トークンの取得、メッセージ受信処理の実装

新しい鍵ペアを生成する

Firebase Consoleのトップ画面の歯車アイコンをクリックして”プロジェクトの設定”を選択します。

VAPID_0.png

設定画面の”クラウドメッセージング”を選択しウェブ設定の欄で「鍵ペアを生成」ボタンをクリックします。

VAPID_1.png

鍵ペアが生成されると図のように表示されます。表示されているのは公開鍵の方です(なお、図ではモザイクをかけていますが公開鍵なので隠す必要はありません)。秘密鍵はレコードにマウスカーソルをあてると右端にハンバーガーメニューが表示されるので、そのメニューから"秘密鍵を表示"を選択します。

VAPID_2.png

メッセージング オブジェクトの取得

// Retrieve Firebase Messaging object.
const messaging = firebase.messaging();

アプリケーションにウェブ認証情報を設定する

上記で生成した鍵ペアの公開鍵(VAPID)の値を設定します。

VAPIDとは

FCM のウェブ インターフェースでは、「Voluntary Application Server Identification 鍵(VAPID 鍵)」と呼ばれるウェブ認証情報を利用して、サポートされているウェブプッシュ サービスへの送信要求が承認されます。

// Add the public key generated from the console here.
messaging.usePublicVapidKey("BPi4Ti...");

通知の許可を求める

この処理を実行すると、下の図のようなダイアログが表示されます。許可を選択すると登録トークンの取得ができるようになります。

messaging.requestPermission()
.then(function() {
  console.log('Notification permission granted.');

  // ...
})
.catch(function(err) {
  console.log('Unable to get permission to notify.', err);
});

t1.png

通知のテスト

許可や拒否のテストを繰り返し行いたい場合、通知の設定を毎回デフォルトにリセットする必要がありますが、下の図の方法で簡単にリセットできます。

t8.png

登録トークンを取得する

通知の許可が得られたら、次にgetTokenメソッドで登録トークンを取得します。登録トークンはメッセージの送信時に必要となるため、メッセージを送信するサーバーへ送信し、なんらかの手段で永続化する必要があります。(FirebaseではFirestoreを利用するのが便利です)
この記事ではその部分の実装は行わないので、サーバー側の処理で使用する登録トークンはハードコードします。

messaging.getToken()
.then(function(currentToken) {
  if (currentToken) {
    console.log('Registration token. ', currentToken);

    // ...
  } else {
    console.log('No Instance ID token available. Request permission to generate one.');

    // ...
  }
})
.catch(function(err) {
  console.log('An error occurred while retrieving token. ', err);

  // ...
});

IID (InstanceID) Tokenとは

Firebaseのドキュメントを読んでいると、ときどき”IID (InstanceID) Token”という単語が出てきますが、このIID Tokenは登録トークンと同一のものです。

トークンの更新

下記に引用したようにトークンが更新された場合に、最新の登録トークンを取得する処理が必要です。

新しいトークンが生成されるたびにonTokenRefreshコールバックが呼び出されるため、そのコンテキストでgetTokenを呼び出すと、利用可能な最新の登録トークンに確実にアクセスできます

messaging.onTokenRefresh(function() {
  messaging.getToken().then(function(refreshedToken) {

    // ...
  });
});

メッセージの受信処理

アプリケーションがフォアグランドのときのメッセージの受信は、onMessageメソッドのコールバックで行います。

messaging.onMessage(function(payload) {
  console.log('Message received. ', payload);

  // ...
});

メッセージの受信をポップアップで通知する場合は、たとえば下記のような処理をonMessageメソッドのコールバックに実装します。

const title = 'this is title';
const options = {
  body: 'this is message body',
  icon: 'logo.png'
};
const notification = new Notification(title, options);

サービスワーカー(service worker)の実装

distディレクトリに下記の内容でfirebase-messaging-sw.jsという名前のjsを作成します。
なお、サービスワーカー内ではfirebase-messaging.js以外のscriptは利用できません。

importScripts('/__/firebase/5.0.4/firebase-app.js');
importScripts('/__/firebase/5.0.4/firebase-messaging.js');
importScripts('/__/firebase/init.js');

const messaging = firebase.messaging();

Firebase Hosting以外の環境にデプロイする場合

CDN経由でscriptを読み込みます。初期化処理で指定する<MESSAGING-SENDER-ID>は、プロジェクト認証情報の送信者IDか、”ウェブアプリにFirebaseを追加”のところで取得した設定情報のmessagingSenderIdを指定します。

importScripts('https://www.gstatic.com/firebasejs/5.0.4/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.0.4/firebase-messaging.js');

firebase.initializeApp({
  'messagingSenderId': '<MESSAGING-SENDER-ID>'
});

const messaging = firebase.messaging();

プロジェクト認証情報から送信者IDを調べる場合は、Firebase Consoleのトップ画面の歯車アイコン → ”プロジェクトの設定” → ”クラウドメッセージング” → ”プロジェクト認証情報”で確認できます。
なお、サーバーキーは後ほど行うメッセージのテスト送信でも使用するので控えておきます。(このサーバーキーは公開してはいけない情報です)

送信者id2.png

サービスワーカーの登録

サービスワーカーを利用するには登録する必要があります。
下記にService Worker 登録 | Web | Google Developersで説明されている内容を引用します。

Service Worker の詳細を読んだことがある場合は、次のようなボイラプレートを見たことがあるでしょう。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js');
}

Firebase SDKを利用する場合は、サービスワーカーをアプリケーションのルートにfirebase-messaging-sw.jsという名前で実装しておくとFirebase SDK(firebase-messaging.js)が、このファイルを自動的に読み取りサービスワーカーとして登録してくれるので、上記で引用したようなボイラープレートは不要です。

任意の場所または任意のファイル名のサービスワーカーを利用する場合

アプリケーションのルートにfirebase-messaging-sw.jsという名前で実装できない場合は、以下の例のようにmessaging.useServiceWorkerを使って登録します。

navigator.serviceWorker.register('/path/to/sw.js')
.then((registration) => {
  messaging.useServiceWorker(registration);

  // ...
});

メッセージの受信処理

アプリケーションがバックグランドのときのメッセージの受信は、setBackgroundMessageHandlerメソッドのコールバックで行います。
このメソッドはサービスワーカーにしか実装できず、アプリケーション(index.html)に実装すると実行時にエラーになります。
ポップアップ通知の処理はフロントエンドの時と若干異なり、self.registration.showNotificationメソッドを使用します。

messaging.setBackgroundMessageHandler(function(payload) {
  const notificationTitle = payload.notification.title;
  const notificationOptions = {
    body: payload.notification.body,
    icon: payload.notification.icon
  };

  return self.registration.showNotification(notificationTitle, notificationOptions);
});

コード全体

firebase-messaging-sw.js
importScripts('/__/firebase/5.0.4/firebase-app.js');
importScripts('/__/firebase/5.0.4/firebase-messaging.js');
importScripts('/__/firebase/init.js');

const messaging = firebase.messaging();

messaging.setBackgroundMessageHandler(function(payload) {
  const notificationTitle = payload.notification.title;
  const notificationOptions = {
    body: payload.notification.body,
    icon: payload.notification.icon
  };

  return self.registration.showNotification(notificationTitle, notificationOptions);
});

self.addEventListener('install', function(event) {
  console.log('Service Worker installing.');
});

self.addEventListener('activate', function(event) {
  console.log('Service Worker activating.');  
});

メッセージのテスト送信

この状態でアプリケーションをデプロイします。

> firebase deploy

承認前

Webプッシュ通知の承認前にアプリケーションにアクセスした状態です。

t0.png

通知の許可

”REQUEST PERMISSION”ボタンをクリックすると通知を許可するかどうかのダイアログが表示されるので”許可”ボタンをクリックします。

t1.png

登録トークンの取得

ページを再読み込みします。
正常に動作すれば下の図のように登録トークンの値が表示されます。

t2.png

chromeのdevtoolで、サービスワーカーが登録されていることを確認します。

t3.png

これでメッセージを受信する準備ができたので、次にコマンドラインからメッセージを送信してプッシュ通知が機能していることを確認します。

curlを使ってメッセージを送信する

メッセージの送信プロトコルには"HTTP v1 API"と”Legacy HTTP Server Protocol”と呼ばれるプロトコルがあります。"HTTP v1 API"を使用するにはOAuth 2.0のトークンを取得する必要があり若干面倒なため、簡単な"Legacy HTTP Server Protocol"でメッセージを送信します。
テストに使用するメッセージオブジェクトを下記の内容で作成します。
メッセージのtoにセットする<YOUR-IID-TOKEN>はブラウザに表示された登録トークンの値を設定します。

push.json
{
  "to": "<YOUR-IID-TOKEN>",
  "time_to_live": 60,
  "notification": {
    "title": "Demo web push notification",
    "body": "this is message body",
    "icon": "logo.png",
    "click_action": "https://project*********.firebaseapp.com/"
  },
  "data": {
   "key1": "value1",
   "key2": "value2"
  }
}

アプリケーションのページを開いた(フォアグランドにした)状態で下記のコマンドを実行します。
リクエストヘッダーにセットする<YOUR-SERVER-KEY>はプロジェクト認証情報で確認できるサーバーキーの値を設定します。

> curl -H "Authorization: key=<YOUR-SERVER-KEY>" \
       -H "Content-Type: application/json" \
       -X POST "https://fcm.googleapis.com/fcm/send" -d @push.json

メッセージの送信に成功すると下記のようなレスポンスがあります。

{
  "multicast_id":7364335035897749001,
  "success":1,
  "failure":0,
  "canonical_ids":0,
  "results":[
    {
      "message_id":"0:1528986699115793%e609af1cf9fd7ecd"
    }
  ]
}

アプリケーションでメッセージを受信できていることを確認します。

t4.png

次に、アプリケーションのページとは別のタブを開いた(バックグラウンドにした)状態でもう一度メッセージを送信します。
モニターの右下に下の図のようなポップアップ通知が表示されます。

t5.png

クリックすると、メッセージオブジェクトのclick_actionに指定したページが表示されます。この記事ではアプリケーションのトップページ(index.html)を指定しているので下図のページが表示されるはずです。

t6.png

アプリケーションが起動していない時のテスト

アプリケーション(ブラウザ)が起動していない状態でメッセージを送信します。この記事では有効期間を60秒に設定しているので、メッセージ送信後に60秒以内にブラウザを立ち上げて、メッセージが通知されることを確認します。

メッセージの有効期間の設定

Android およびウェブ / JavaScript では、メッセージの最大有効期間を指定できます。その値は 0~2,419,200 秒(28 日)の間で設定する必要があります。28 日は FCM が配信を試みるまでにメッセージを保存できる最大期間です。このフィールドがないリクエストには、有効期間として最長の 4 週間(28 日)がデフォルトで設定されます。

メッセージの通知処理を実装

メッセージの通知処理はCloud Functionsの関数で行います。Cloud Functionsはインフライベントをトリガーにして関数を実行することができるので、この記事ではストレージにファイルをアップロードしたイベントでメッセージを送信する処理を実装します。

関数にFirebase Admin SDKを追加する

ブラウザで動作するアプリケーションやサービスワーカーではFirebase SDKを利用しましたが、関数では、Firebase Admin SDKを利用します。

Firebase Admin SDKで出来ることを下記に引用しました。この内容の通りサーバーからメッセージを送信する処理や、トピックの管理はAdmin SDKを利用すると簡単に実装することができます。

Firebase Admin SDK を使用すると、独自のバックエンド サービスを Firebase Cloud Messaging(FCM)と統合できます。Admin FCM API は、Firebase サーバーへの認証を処理する一方で、メッセージの送信とトピック登録の管理を容易にします。

カレントディレクトリをfunctionsディレクトリにして、下記のコマンドでfirebase-functions、firebase-adminをインストールします。(ひな型ではすでにインストール済みかもしれませんが、念のため最新のバージョンにしておきます。)

> cd functions
> npm install firebase-functions@latest firebase-admin@latest --save

このときは、下記のワーニングが表示されました。
ワーニングで示されたURL「Firebase SDK for Cloud Functions 移行ガイド: ベータ版からバージョン 1.0 へ」にアクセスすると変更内容が確認できるので一度見ておくと良いと思います。

======== WARNING! ========

This upgrade of firebase-functions contains breaking changes if you are upgrading from a version below v1.0.0.

To see a complete list of these breaking changes, please go to:

https://firebase.google.com/docs/functions/beta-v1-diff

メッセージを送信する

特定のユーザー(登録トークン)へ送信する

ひな型に含まれるindex.jsを下記のように書き換えます。
この実装では登録トークンをハードコードしていますが、本来ならデータベースから取得するといった実装になると思います。

Firebase Admin SDKでメッセージを送信するときのプロトコルは"HTTP v1 API"になります。このため前述のcurlでテストしたときのメッセージオブジェクトのフォーマットが違います。

'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.noticeUploadfile = functions.storage.object().onFinalize((object, context) => {
  const bucket = object.bucket;
  const filePath = object.name;
  const contentType = object.contentType;
  const size = object.size;

  const registrationToken = '<REGISTRATION_TOKEN>';
  const title = 'File uploaded.';
  const body = `File was uploaded to (${filePath}) in [${bucket}].`;

  const message = {
    notification: {
      title: title,
      body: body
    },
    data: {
      bucket: bucket,
      filePath: filePath,
      contentType: contentType,
      size: size
    },
    webpush: {
      headers: {
        TTL: "60"
      },
      notification: {
        icon: 'logo.png',
        click_action: 'https://project*********.firebaseapp.com/'
      }
    },
    token: registrationToken
  };

  return admin.messaging().send(message)
    .then((response) => {
      console.log('Successfully sent message:', response);
      return Promise.resolve(response);
    })
    .catch((error) => {
      console.log('Error sending message:', error);
      return Promise.reject(error);
    });

});

関数のデプロイ

> firebase deploy --only functions

動作確認

関数のデプロイが終わったらアプリケーションをフォアグランド/バックグラウンドにした状態で、Firebase ConsoleのStorageダッシュボードからファイルをアップロードしてみます。
アプリケーションがフォアグランドのときはページにメッセージが出力され、バックグラウンドのときはモニターの右下にポップアップ通知が出ることを確認します。

t7.png

トピックへ送信する

トピックを指定してメッセージを送信するにはトピックと登録トークンを関連付ける(サブスクライブ)必要があります。

登録トークンをトピックに関連付ける

通常はサーバー側の処理で登録トークンとトピックを関連付けますが、ここでは簡単にテストするために下記のAPIでサブスクライブを行います。

> curl -H "Authorization: key=<YOUR-SERVER-KEY>" \
       -H "Content-Type: application/json" \
       -H "Content-Length: 0" \
       -X POST "https://iid.googleapis.com/iid/v1/<YOUR-IID-TOKEN>/rel/topics/<topic_name>"

登録トークンがどのトピックをサブスクライブしているかは下記のAPIで確認できます。

> curl -H "Authorization: key=<YOUR-SERVER-KEY>" \
       -H "Content-Type: application/json" \
       "https://iid.googleapis.com/iid/info/<YOUR-IID-TOKEN>?details=true"

トピックへ送信する処理

メッセージオブジェクトのtokenの代わりにtopicを指定します。

const message = {
  //...

  topic: 'general'
}

補足

Webプッシュ通知について (Web Push Notifications)

Webプッシュ通知の仕様は、Google DevelopersのWeb Fundamentalsというページで詳細を確認することができます。

メッセージのタイプ

通知メッセージ

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    }
  }
}

データメッセージ

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    }
  }
}

データペイロードを含む通知メッセージ

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    },
    "data" : {
      "Nick" : "Mario",
      "Room" : "PortugalVSDenmark"
    }
  }
}

共通キー

プラットフォームに関係なく、すべてのアプリ インスタンスによって解釈される共通キーは、message.notification.title、message.notification.body、および message.data です。

webプッシュ固有のキー

"webpush": {
  "headers": {
    string: string,
    ...
  },
  "data": {
    string: string,
    ...
  },
  "notification": {
    object
  }
}
rubytomato@github
今までJavaをメインにやってきましたが、JavaScript(Node.js)の習得に取り組み始めました。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした