はじめに
Google CloudのCloud Functionsを使用して、Qiitaの人気記事を配信するLINE Botを作成しました!
理由として、Qiitaから送られてくるメールは1週間に1回かつあまり個人メールを見ないので毎日見るLINEなら記事を見る回数増えそうだなーと思って作成してみました!
完成イメージ
最終的なイメージは下記になります。人気記事が平日に毎朝自動で配信されます!
今回の構成
今回はGoogle Cloudメインで作成しました!
下記構成図となります。
ざっくりとした処理の流れです。
- Cloud Schedulerで指定した時間にPub/SubからCloud Functionsにメッセージを配信
- Pub/Subからのメッセージ受信をトリガーにCloud Functionsを実行
- Cloud FunctionsがQiita API v2から人気記事を取得し、Messaging APIを使用してLINEにメッセージを配信
Cloud Functions単体で定期実行の仕組みがなく、またCloud SchedulerとCloud Functionsの直接連携ができないため、Cloud Scheduler + Pub/Subを使用してCloud Functionsの定期実行を行います。
公式ドキュメントにもCloud Functionsを定期実行する手法として上記の連携手法が紹介されています。
使用したサービス
サービス名 | 用途 |
---|---|
Cloud Scheduler | 1日1回ジョブを起動し、Pub/Subでメッセージを配信する。 |
Pub/Sub | Cloud Schedulerで指定した時間にメッセージを配信し、Cloud Functionsのトリガーとする。 |
Cloud Functions | Qiita API v2にアクセスして、人気記事を取得し、LINEのMessaging APIを使用してメッセージを配信する。 |
Firestore | 1度配信した人気記事を記録して、既に配信した人気記事を配信しないようにする。(※次回使用) |
Qiita API v2 | Qiitaが公開しているWeb API。今回はこちらにアクセスして記事を取得する。 |
Messaging API | LINEが提供しているメッセージ配信用のAPI。 |
下準備
チャネル作成・ChannelSecretとChanelAccessTokenを取得
以前投稿した記事にチャネルの作成方法やトークンなどの取得方法を紹介しているので、参考にしながらチャネルを作成してChannelSecret
とChanelAccessToken
を取得します。
Pub/Subでトピック作成
Cloud SchedulerとCloud Functionsを連携するために、繋ぎのサービスとしてPub/Subを使用します。
今回トピック名はarticle-topic
としてスクリーンショットのように作成します。
Cloud Schedulerでスケジューラー設定
スケジュールの定義
決まった日時でPub/Subでメッセージを配信するためにスケジューラーを設定します。
cron式で設定します。今回は通勤中に確認したく平日の朝6時に配信してほしいので0 06 * * 1-5
と設定し、名前はpublish-article-topic-job
にします。
実行内容の構成
ターゲットタイプPub/Sub
を選択して、先ほど作成したトピックarticle-topic
を選択します。メッセージ本文は使用しないですが、必須のため適当なメッセージを入力しておきます。
Cloud Functionsの作成
構成とトリガーの追加
環境は第2世代
を選択し、関数名はLINEにQiita記事を送信するのでsendQiitaArticles
と命名します。
Pub/Subから配信されたメッセージの受信をトリガーに関数実行してほしいので、EVENTARC トリガーを追加
ボタンをクリックしてトリガーを追加します。
トリガーの設定
イベント プロパイダにCloud Pub/Sub
を選択して、先ほど作成したトピックarticle-topic
を選択します。
また注意メッセージに従ってサービスアカウントのロール付与も行います。
このロール付与を行う理由はトリガー起動時に権限エラーが発生し、連携が上手くいかないためです。
デプロイ
ソースコードは後でアップロードするため、一旦デフォルトのままデプロイを行います。
各サービス連携の疎通確認
作成したCloud Functionsにメッセージを配信するソースコードをデプロイして各サービスが連携できるか確認してみましょう。
ライブラリインストール
必要なライブラリをインストールします。Cloud Functionsの標準ライブラリ、LINE Bot SDK、環境変数、HTTPライブラリを使用したいため下記ライブラリをインストールして実装を進めていきます。
npm install @line/bot-sdk dotenv @google-cloud/functions-framework axios
環境変数
.env
ファイルに取得したCHANNEL_ACCESS_TOKEN
とCHANNEL_SECRET
を設定します。
CHANNEL_ACCESS_TOKEN = "XXX"
CHANNEL_SECRET = "XXX"
ソースコード
index.js
にLINEにメッセージを配信するコードを記載します。
const line = require("@line/bot-sdk");
require("dotenv").config();
const functions = require("@google-cloud/functions-framework");
//channelAccessTokenとchannelSecretを環境変数から取得
const config = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.CHANNEL_SECRET,
};
//LINEのクライアントインスタンス生成
const client = new line.Client(config);
//エントリポイント
functions.cloudEvent("sendQiitaArticles", async (cloudEvent) => {
const messages = [
{
type: "text",
text: "テスト送信です!",
},
];
try {
//LINEにメッセージを配信
await client.broadcast(messages);
console.log("配信に成功しました!");
} catch (error) {
console.log(`エラー: ${error.statusMessage}`);
console.log(error.originalError.response.data);
}
});
デプロイ・動作確認
作成したソースコードをzipで圧縮してCloud Functionsへアップロードします。
アップロード後、Cloud Schedulerを手動実行して、LINEへメッセージが配信されるか確認します。
デプロイ
動作確認
publish-article-topic-job
を選択して、強制実行をクリックしてジョブを起動します。
実行結果
上手く配信できていますね!
下準備は完了で、本格的にQiitaの人気記事を取得してメッセージを配信するよう実装を進めていきます。
実装
Qiitaから人気記事を取得
Qiitaから記事を取得するため、Qiita API v2を使用します。
リファレンスを確認してどのAPIが使えそうか探していきましょう。
使用するエンドポイント
確認したところ記事を取得するにはGET /api/v2/itemsが使用できそうです!
このエンドポイントを使用するのに認証トークンなども必要ないのでお手軽ですね!
設定パラメータ
1回の配信で4記事配信したいので、パラメータの取得ページ
は1
で1ページ数あたりの取得記事
は4
を設定します。
query
は検索条件に当たり、今回は直近の人気記事を取得したいので、ストック数20以上で1週間以内の記事を取得するcreated:>"1週間前の日付"+likes:>20
を設定します。
いいね数ではなく、ストック数を使用しているのはAPIの仕様でいいね数で検索できないためです。
検索で使用できるオプションは興味があれば確認してください。
検索時に使用できるオプション
クエリパラメータ | パラメータ名 | 設定値 |
---|---|---|
page | 取得ページ数 | 1 |
per_page | 1ページ数あたりの取得記事 | 4 |
query | 取得条件 | created:>"1週間前の日付"+stocks:>20 |
動作確認
試しにPostmanでGetリクエストを送信して取得できるか確認してみましょう。
無事取得できていますねー!ちなみにレスポンスのデータ構造がどうなっているか確認しておきます。
{
// 一部抜粋
"coediting": false,
"comments_count": 0,
"created_at": "2021-03-11T18:49:59+09:00",
"group": null,
"id": "ac01a5214c9fa32fff2f",
"likes_count": 6,
"private": false,
"reactions_count": 0,
"stocks_count": 0,
"tags": [
{
"name": "Node.js",
"versions": []
},
{
"name": "Heroku",
"versions": []
},
{
"name": "Express",
"versions": []
},
{
"name": "linebot",
"versions": []
},
{
"name": "LINEmessagingAPI",
"versions": []
}
],
"title": "Node.jsを使ってお手軽インドカレー検索LINE Bot を作ってみた(1.簡単な応答)",
"updated_at": "2021-03-17T08:27:05+09:00",
"url": "https://qiita.com/nanndot/items/ac01a5214c9fa32fff2f",
"user": {
"description": "ピチピチフレッシュマンです。クラウド技術・モダン開発などに興味があり、フレッシュらしく何事にも熱く取り組むことをモットーにしています。世界一インドカレー・ナンが大好きで、好きなナンはチーズナン。",
"facebook_id": "",
"followees_count": 0,
"followers_count": 3,
"github_login_name": null,
"id": "nanndot",
"items_count": 4,
"linkedin_id": "",
"location": "",
"name": "",
"organization": "アビームシステムズ株式会社",
"permanent_id": 1178305,
"profile_image_url": "https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/1178305/f2a2224406a0bd9488573b5a952e4b601880c72e/x_large.png?1614940376",
"team_only": false,
"twitter_screen_name": null,
"website_url": ""
},
"page_views_count": null,
"team_membership": null,
"organization_url_name": "abeam-s"
}
記事のタイトル、URL、いいね数、ストック数などしっかり取得できていますね!後続の処理で活用していきます!
ソースコード
人気記事取得処理
Qiitaから人気記事を取得する部分を別のjsファイルとして切り出します。
命名はgetArticles.js
とします。
const axios = require("axios");
//デフォルトパラメータ
//PAGE:取得ページ数
//PER_PAGE:1ページ数あたりの取得記事
//ストック数
const DEFAULT_PARAMS = {
PAGE: 1,
PER_PAGE: 4,
STOCKS: 20,
};
/**
* 記事をQiitaから取得する。
* @module getArticles
* @param {Object} params - 使用するパラメータオブジェクト
*/
const getArticles = async (params = DEFAULT_PARAMS) => {
const targetDate = new Date();
//今日の日付から一週間前を設定
targetDate.setDate(targetDate.getDate() - 7);
//YYYY-MM-DD部分だけ抽出
const dateBeforeWeek = targetDate.toISOString().split("T")[0];
//Qiita API V2URL、パラメータを設定
const qiitaURL = `https://qiita.com/api/v2/items?page=${params.PAGE}&per_page=${params.PER_PAGE}&query=created:>${dateBeforeWeek}+stocks:>${params.STOCKS}`;
try {
//Qiita API v2をコール
const result = await axios.get(qiitaURL);
//取得した記事からtitle,urlを抽出
const articlesList = result.data.map((article) => {
return {
title: article.title,
url: article.url
};
});
return articlesList;
} catch (error) {
console.log(`エラー: ${error.statusMessage}`);
console.log(error.originalError.response.data);
}
};
module.exports = getArticles;
HTTPライブラリaxios
を使用して、Qiita API v2
にアクセスします。取得した記事からtitle
とurl
だけ抽出してエントリポイントに返却します。
エントリポイント
エントリポイントもgetArticles
を使用するよう修正して、メッセージを配信します。
const line = require("@line/bot-sdk");
require("dotenv").config();
const functions = require("@google-cloud/functions-framework");
+ const getArticles = require("./getArticles");
//channelAccessTokenとchannelSecretを環境変数から取得
const config = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.CHANNEL_SECRET,
};
const client = new line.Client(config);
functions.cloudEvent("sendQiitaArticles", async (cloudEvent) => {
+ const articles = await getArticles();
+ //取得した記事をLINEで配信するよう加工
+ const articlesMessages = articles.map((article) => {
+ return {
+ type: "text",
+ text: "タイトル:" + article.title + "\nURL:" + article.url,
+ };
+ });
const messages = [
{
type: "text",
text: "本日の上位記事4件です!",
},
+ ...articlesMessages,
];
try {
//LINEにメッセージを配信
await client.broadcast(messages);
console.log("配信に成功しました!");
} catch (error) {
console.log(`エラー: ${error.statusMessage}`);
console.log(error.originalError.response.data);
}
});
修正完了したら、先ほどと同様にCloud Schedulerから強制実行して挙動を確認してみます!
動作確認
人気記事をLINE上で確認できていますね!!これで今回はおしまい...と思いきやちょっとデザインが味気なくないですかね...?
もう少しおしゃれな感じにするために一工夫していきます!
Flex Messageの活用
LINEでデザインを意識したメッセージを配信するにはFlex Messageを使用します。
CSSのFlexBoxを活用して、レイアウトの自由度が高いメッセージを作成することが可能です。
ただゼロから作成するのは中々大変なので、公式で提供されているFlex Message Simulatorを活用して、GUIベースでメッセージのデザインを作成することが可能です!
今回は紹介だけに留めて出来上がりのメッセージ定義だけ記載しておきます。興味があれば使用してみることをおすすめします!
出来上がりイメージ
アイコンやいいね数、ストック数、ユーザー名なども表示するようなカードにします。
カードをタッチしたらQiitaの記事を表示するようにURLを埋め込みます。
FlexMessage定義
下記メッセージ定義をflexMessage.json
として作成して配置しておきます。
{
"type": "bubble",
"size": "giga",
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "box",
"layout": "horizontal",
"contents": [
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "image",
"url": "",
"aspectMode": "cover",
"size": "full"
}
],
"cornerRadius": "100px",
"width": "72px",
"height": "72px",
"spacing": "md",
"position": "relative",
"margin": "md",
"offsetTop": "lg"
},
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"contents": [
{
"type": "span",
"text": "Title:",
"weight": "bold",
"color": "#000000"
},
{
"type": "span",
"text": " "
},
{
"type": "span",
"text": ""
}
],
"size": "sm",
"wrap": true
},
{
"type": "box",
"layout": "baseline",
"contents": [
{
"type": "text",
"text": "",
"size": "sm",
"color": "#bcbcbc"
}
],
"spacing": "sm",
"margin": "md"
},
{
"type": "box",
"layout": "baseline",
"contents": [
{
"type": "text",
"text": "",
"size": "sm",
"color": "#bcbcbc"
}
],
"spacing": "sm",
"margin": "md"
}
]
}
],
"spacing": "xl",
"paddingAll": "20px"
}
],
"paddingAll": "0px"
},
"action": {
"type": "uri",
"label": "action",
"uri": ""
}
}
ソースコードの修正
FlexMessageを作成してメッセージを配信するようソースコードを全体的に修正していきます。
人気記事取得処理
articlesList
にprofile_image_url,likes_count,stocks_count,name
を追加するよう修正します。
const axios = require("axios");
//デフォルトパラメータ
//PAGE:取得ページ数
//PER_PAGE:1ページ数あたりの取得記事
//ストック数
const DEFAULT_PARAMS = {
PAGE: 1,
PER_PAGE: 4,
STOCKS: 20,
};
/**
* 記事をQiitaから取得する。
* @module getArticles
* @param {Object} params - 使用するパラメータオブジェクト
*/
const getArticles = async (params = DEFAULT_PARAMS) => {
const targetDate = new Date();
//今日の日付から一週間前を設定
targetDate.setDate(targetDate.getDate() - 7);
//YYYY-MM-DD部分だけ抽出
const dateBeforeWeek = targetDate.toISOString().split("T")[0];
//Qiita API V2URL、パラメータを設定
const qiitaURL = `https://qiita.com/api/v2/items?page=${params.PAGE}&per_page=${params.PER_PAGE}&query=created:>${dateBeforeWeek}+stocks:>${params.STOCKS}`;
try {
//Qiita API v2をコール
const result = await axios.get(qiitaURL);
+ //取得した記事からtitle,url,profile_image_url,likes_count,stocks_count,nameを抽出
const articlesList = result.data.map((article) => {
return {
title: article.title,
url: article.url,
+ profile_image_url: article.user.profile_image_url,
+ likes_count: article.likes_count,
+ stocks_count: article.stocks_count,
+ name: article.user.name
};
});
return articlesList;
} catch (error) {
console.log(`エラー: ${error.statusMessage}`);
console.log(error.originalError.response.data);
}
};
module.exports = getArticles;
FlexMessage作成処理
makeFlexMessages.js
を新規作成します。
ここでは、取得した人気記事とflexMessage.json
を元にFlexMessageを作成します。
const baseMessage = require("./flexMessage.json");
/**
* 取得した記事を元にFlexMessageに作成
* @module makeFlexMessages
* @param {Array} articles - 取得した人気記事
*/
const makeFlexMessages = (articles) => {
console.log(articles)
const flexMessages = articles.map((article) => {
const flexMessage = Object.assign(
{},
JSON.parse(JSON.stringify(baseMessage))
);
//user_icon
flexMessage.body.contents[0].contents[0].contents[0].url =
article.profile_image_url;
//title
flexMessage.body.contents[0].contents[1].contents[0].contents[2].text =
article.title;
//url
flexMessage.action.uri = article.url;
//likes and stocks
flexMessage.body.contents[0].contents[1].contents[1].contents[0].text = `${article.stocks_count} Stocks 🗂️ ${article.likes_count} Likes 👍`;
//create_user
flexMessage.body.contents[0].contents[1].contents[2].contents[0].text = `created by ${article.name}`;
return {
type: "flex",
altText: "#",
contents: {
...flexMessage,
},
};
});
return flexMessages;
};
module.exports = makeFlexMessages;
エントリポイント
makeFlexMessages
を実行してFlexMessageを作成してメッセージを配信するよう修正します。
const line = require("@line/bot-sdk");
require("dotenv").config();
const functions = require("@google-cloud/functions-framework");
const getArticles = require("./getArticles");
+ const makeFlexMessages = require("./makeFlexMessages");
//チャネルアクセストークンとチャネルシークレットを環境変数から取得
const config = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.CHANNEL_SECRET,
};
const client = new line.Client(config);
functions.cloudEvent("sendQiitaArticles", async (cloudEvent) => {
const articles = await getArticles();
- //取得した記事をLINEで配信するよう加工
- const articlesMessages = articles.map((article) => {
- return {
- type: "text",
- text: "タイトル:" + article.title + "\nURL:" + article.url,
- };
- });
- const messages = [
- {
- type: "text",
- text: "本日の上位記事4件です!",
- },
- ...articlesMessages,
- ];
+ //取得した記事をFlexMessageで配信するよう加工
+ const articlesFlexMessages = makeFlexMessages(articles);
+ const messages = [
+ {
+ type: "text",
+ text: `本日の上位記事${articlesFlexMessages.length}件です!`,
+ },
+ ...articlesFlexMessages,
+ ];
try {
//LINEにメッセージを配信
await client.broadcast(messages);
console.log("配信に成功しました!");
} catch (error) {
console.log(`エラー: ${error.statusMessage}`);
console.log(error.originalError.response.data);
}
});
動作確認
Cloud Schedulerから手動実行して挙動を確認してみます!
おお!!デザインもいい感じですね!
今回はこれで無事実装完了です!
おわりに
無事完了としたいところですが、今のままだと1度配信された記事は人気であれば何度でも配信されるような作りになっています。。。
次回でNoSQLサービスであるFirestoreを活用して一度配信した記事は配信されないような機能を実装していきます!
ご覧いただきありがとうございました!