Pocketはご存じでしょうか。
PCやスマホから、気に入ったWebサイトをブックマークとして保存し、共有できるサービスです。
今回は、このPocketに登録したサイトをLINEボットから参照できるようにします。
最終的には以下のような画面になります。
まずこちらが、LINEボットを友達登録した後に、最新のサイトリストを表示させたときのものです。
次に、こちらが、LIFFアプリとして表示させたときのものです。LIFFは、LINEアプリ内で起動させる方法と、普段お使いのWebブラウザから起動させる方法のどちらでも可能です。
構成としては以下の通りです。
ちょっとやり取りが多いですが、のちほど一つ一つ説明していきます。
ソースコードもろもろは以下に置いておきました。
poruruba/PocketLiff
準備
これらを実現するための前準備は以下の通りです。
LINE:プロバイダの作成
LINEのプロバイダが必要です。まだ作成していない場合は、以下のサイトから作成しておきます。
LINE Developersコンソール
LINE:チャネルの作成
LINE Developersコンソールから、作成したプロバイダにおいて、チャネルを作成します。チャネルの種類は「Messaging API」を選択します。このチャネルは、LINEボット用です。
いくつか入力を促されますが、適当に入力して作成します。
その後、いくつかの設定が必要です。
Messaging API設定から、チャネルアクセストークン(長期)を発行しておきます。
Webhook URLにこれから立ち上げるサイトを登録します。
https://[立ち上げるNode.jsサーバのホスト名:ポート番号]/pocket-line
まだNode.jsサーバを立ち上げていないので、「検証」ボタンを押下してもNGとなります。
Webhookの利用をOnにします。
LINE公式アカウント機能の応答メッセージを無効に変えておきます。
ということで、最後に以下の情報をメモっておきます。
・チャネルシークレット
・チャネルアクセストークン(長期)
・QRコード(友達登録用)
次に、もう一つチャネルを作成します。
チャネルの種類は「LINEログイン」です。こちらは、LIFF用です。
アプリタイプには、「ウェブアプリ」にチェックを入れておきます。
最後に、よければ「公開済み」に状態を変更しておきます。
ということで、最後に以下の情報をメモっておきます。
・チャネルID
LINE:LIFF登録
次にLIFFを登録します。
LIFFは、LINEログインのチャネルに登録します。
サイズには「Full」を選択するとよいでしょう。
エンドポイントURLには、これから立ち上げるNode.jsサーバを指定します。
(もちろんまだ実体を立ち上げていませんが)
https://[立ち上げるNode.jsサーバのホスト名:ポート番号]/pocket_liff/index.html
スコープには、以下のチェックをOnにします。
・profile
・openid
ということで、最後に以下の情報をメモっておきます。
・LIFF ID
・LIFF URL
LINE:リッチメニュー登録
LINEアプリで使いやすいようにするため、リッチメニューを登録します。
以下をご参照ください。
Bodyはこんな感じです。
{
"richMenuId": "richmenu-XXXXXXXXXXXXXXXXXXXXXXXXXX",
"name": "Pocketショートカットメニュー",
"size": {
"width": 2500,
"height": 250
},
"chatBarText": "ショートカット",
"selected": true,
"areas": [
{
"bounds": {
"x": 44,
"y": 44,
"width": 783,
"height": 170
},
"action": {
"type": "uri",
"uri": "https://liff.line.me/【LIFF-ID】"
}
},
{
"bounds": {
"x": 869,
"y": 44,
"width": 783,
"height": 170
},
"action": {
"type": "postback",
"data": "menu_help"
}
},
{
"bounds": {
"x": 1694,
"y": 44,
"width": 783,
"height": 170
},
"action": {
"type": "postback",
"data": "menu_latest"
}
}
]
}
もしくは、以下の、LINE Official Account Managerから登録します。
メニューはこんな感じです。
アプリボタンをクリックと、LIFF起動のためのURLが返るようにしましたので、LIFFが起動します。
ヘルプと最新リストをクリックすると、ポストバックが返るようにしましたので、のちほど立ち上げるNode.jsで対応する処理を実装します。
ということで、最後に以下の情報をメモっておきます。
・リッチメニューID
Pocket:Pocketアプリ登録
Pocketにアプリを登録します。そのための管理コンソールがあります。
「CREATE AN APPLICATION」ボタンを押下すると、Pocketアプリを登録できます。
Permissionsには、Retrieveにチェックを入れておきます。
Platformsには、Webにチェックを入れておきます。
作成後、URLを指定します。URLには、LINEのLIFF URLを指定します。
作成が完了すると、Consumer Keyが払いだされますので、それをメモっておきます。
Node.jsサーバ立ち上げ
もろもろのソースコードを上げていますので、ZIPダウンロード後以下のようにセットアップします。
> unzip PocketLiff-master.zip
> cd PocketLiff-master
> npm install
> mkdir public\pocket_liff\img\cache
ただし、HTTPSである必要がありますので、フロントにプロキシサーバを配置するか、certフォルダにSSL証明書を置く必要があります。
以下の部分を環境に合わせて変更します。
const LIFF_ID = "【LINEのLIFF ID】";
const LINE_CHANNEL_ID = "【LINEのチャネルID(LINEログイン)】";
const POCKET_CONSUME_KEY = "【PocketのConsumer Key】";
const LINE_CHANNEL_ACCESS_TOKEN = "【LINEのチャネルアクセストークン(長期)】";
const LINE_CHANNEL_SECRET = "【LINEのチャネルシークレット】";
const LINE_RICHMEMU_ID = "【LINEのリッチメニューID】";
const LIFF_ID = "【LINEのLIFF ID】";
起動は以下のように実行します。
> node app.js
今一度、LINE Developersコンソールを開き、Webhook URLの検証ボタンを押下すると、成功すればOKです。
ちなみに、以下、主要なフォルダの説明です。
・public\pocket_liff
静的Webページを公開しているフォルダです。今回の場合はLIFFアプリです。
以下のURLでアクセスできます。
https://[立ち上げるNode.jsサーバのホスト名:ポート番号]/pocket_liff/
ただし、LIFFアプリとして登録してあるため、以下でもアクセスできます。
https://liff.line.me/【LINEのLIFF ID】
・api\controllers\pocket-api
WebAPIのエンドポイントが実装されています。
エンドポイントは以下で設定しています。
api\controllers\pocket-api\swagger.yaml
以下の3つのエンドポイントがあることがわかります。
・/pocket-line : LINEボット用
・/pocket-signin : LIFFアプリ用
・/pocket-retrieve : LIFFアプリ用
・data\pocket\users.json
登録したユーザ情報の保持するためのJSONファイルです。最初は空配列です。
サイトリスト取得までの流れ
以下順番に説明していきます。
・LINEボットお友達登録
・LINEボット応答
・LIFF起動
・LINEログイン
・Pocket承諾
・サイトリスト取得(LIFF)
・サイトリスト取得(LINEボット)
LINEボットお友達登録
お手持ちのスマホのLINEアプリから、メモった「QRコード(友達登録用)」をスキャンして、LINEボットをお友達登録します。
登録すると、以下のようなメッセージが返ってきます。
{Nickname}さん はじめまして!{AccountName}です。 友だち追加ありがとうございます(moon wink) このアカウントでは、最新情報を定期的に配信していきます(loveletter) どうぞお楽しみに(gift)(2 stars)
これは、デフォルトの設定のままにしているためで、LINE Developersコンソールの応答メッセージにて変更可能です。
LINEボット応答
立ち上げたNode.jsサーバにて、LINEアプリからの操作に応答することができます。
今回は、テキスト入力したメッセージと、リッチメニューからのポストバックに対する応答を実装しています。
リッチメニューのヘルプと最新リストをクリックすると、ポストバックが返るようにしています。
一番単純なヘルプをクリックした場合は、データ「menu_help」のポストバックが遅れられます。以下の部分でそれを受信しています。
app.postback(async (event, client) =>{
console.log(event);
switch(event.postback.data){
case 'menu_help' : {
後はそれに対して、以下を実行することで、単純なメッセージを返しています。
var message = app.createSimpleResponse("フィルタリングしたいキーワードを入力するか、最新リストボタンを押してください。\nまだPocketを登録していない場合は、アプリを起動してください。");
return client.replyMessage(event.replyToken, message);
また、LINEアプリから、通常のテキスト入力した場合には、以下の部分で受信します。
app.message(async (event, client) =>{
それに対する応答処理は後述します。
ユーティリティの詳細は、以下の投稿か、「line-utils.js」をご確認ください。
LINEボットを立ち上げるまで。LINEビーコンも。
LIFF起動
リッチメニューの「アプリ」を選択すると、LIFF URLが送られてLIFFアプリが起動するようにしてあります。
最初は以下のような画面が表示されます。
LINEログイン
最初に起動すると、LINEログインを促されます。
※LINEアプリからLIFF起動した場合には、自動的にLINEログインが行われるため、以降のLINEログイン処理はありません。
(参考)
https://developers.line.biz/ja/docs/liff/
OKボタンを押下すると、LINEログイン画面に遷移します。
ログインが完了すると、以下のようにLINEのユーザのディスプレイ名が表示されます。
※LINEアプリからLIFFを起動した場合は、「ログアウト」ボタンはありません。LINEログインが自動的に行われログアウトしないためです。
ロジックの部分を抜粋します。
まずは、必ずまっさきにliff.initを呼び出す必要があります。Promiseですので、非同期で処理を待ちます。
liff.init({ liffId: LIFF_ID })
.then( () => {
this.initialize();
})
.catch( (err) => {
console.error(err);
alert(err);
})
.finally(() => {
history.replaceState('', '', location.pathname);
});
終わった後に、LINEログイン状態かどうか確認し、ログインしていなければアラートダイアログを表示したのちログインします。
initialize: async function(){
try{
this.line_inClient = liff.isInClient();
this.line_loggedin = liff.isLoggedIn();
console.log("isLoggedIn: " + this.line_loggedin);
if( liff.isLoggedIn() ){
・・・・
}else{
alert('最初にLINEログインが必要です。');
liff.login();
}
}catch(err){
console.error(err);
alert(err);
}
}
ログインは、LINE提供のWebページに遷移し、ログイン完了後、またこのLIFFアプリに戻ってきます。ログイン後に戻ってくると、今度はログイン状態がYesとなっています。
ちなみに、LINEログインページから戻ってきたときには、liff.initの中で、QueryStringを参照していますので、liff.initの前にQueryStringをいじらないようにしましょう。
Pocket承諾
さあいよいよ、Pocketと連携します。
Pocketサーバと連携して、アクセストークンを取得するところまでが最初のゴールです。
取得したアクセストークンは、LINEのuserIdに紐づけてNode.jsサーバ側で管理するので、LINEログインされていることが必須としています。
Pocketに登録した情報をNode.jsサーバが扱えるようにするには、ユーザによる承諾行為が必要となります。
そして、その承諾行為をする前に、まずはPocketサーバに対してcodeを一時的に払い出してもらいます。
以下の部分です。
var result = await do_post_pocket("https://getpocket.com/v3/oauth/request", {
consumer_key: POCKET_CONSUME_KEY,
redirect_uri: POCKET_REDIRECT_URL
});
console.log(result);
user.pocket_code = result.code;
(参考)Pocket Authentication API Documentation
https://getpocket.com/developer/docs/authentication
その後、ユーザに対して承諾をしてもらうためのPocketサーバが提供するページに遷移してもらいます。
以下のようにして、遷移してほしいURLを返しています。
return new Response( {
signin_url: "https://getpocket.com/auth/authorize?request_token=" + result.code + "&redirect_uri=" + POCKET_REDIRECT_URL,
});
ちなみに、「POCKET_REDIRECT_URL」は以下のように定義されています。
const POCKET_REDIRECT_URL = "https://liff.line.me/" + LIFF_ID + "/?cmd=PocketSignin";
Pocketサーバで承諾行為をしたのち、戻ってくるページのURLを指定しています。戻り先は同じくLIFFアプリではあるのですが、承諾した後の戻りであることがわかるようにcmd=PocketSigninというQueryStringを追加しています。
LIFFアプリで、指定されたURLに遷移すると、以下のPocketサーバ提供ページが表示されます。
許可または拒否すると、LIFFアプリに戻ってきます。
LIFFアプリでは、QueryStringを参照して、Pocketサーバからの戻りであることを判別し、以下を実行します。
if( searchs.cmd == 'PocketSignin' ){
var result = await do_post(base_url + "/pocket-signin", {
id_token: liff.getIDToken(),
cmd: 'PocketSignin'
});
console.log(result);
localStorage.setItem("pocket_username", result.pocket_username);
this.pocket_username = result.pocket_username;
this.line_displayName = result.line_displayName;
alert('Pocketサインインし、登録が完了しました。');
}
Node.jsサーバ側では、以下のようにして承諾されたか確認し、承諾されると、Pocketのアクセストークンが取得されます。もしユーザが承諾していなかった場合にはエラーが返ります。
if( body.cmd == 'PocketSignin'){
if( !user.pocket_code )
throw new Error("invalid status");
var result = await do_post_pocket("https://getpocket.com/v3/oauth/authorize", {
consumer_key: POCKET_CONSUME_KEY,
code: user.pocket_code
});
console.log(result);
user.pocket_code = null;
user.pocket_access_token = result.access_token;
user.pocket_username = result.username;
サイトリスト取得(LIFF)
LIFFアプリから、サイトリストを取得するには、Pocketのアクセストークンが必要です。そのトークンはNode.jsサーバが保持していますので、サーバに依頼します。
var result = await do_post(base_url + "/pocket-retrieve", {
id_token: liff.getIDToken(),
num_of_items: POCKET_SEARCH_COUNT,
offset: 0,
search: this.searching_word
});
依頼した人が正しいかを確認するために、LINEのIDトークンも渡しています。実はPocket承諾の際も、サーバ側でLINEのIDトークンの検証を行っています。
もし、IDトークンが正しければ、以下のWebAPI呼び出しは成功します。
var result = await do_post_urlencoded("https://api.line.me/oauth2/v2.1/verify", { id_token: body.id_token, client_id: LINE_CHANNEL_ID });
console.log(result);
var userId = result.sub;
var displayName = result.name;
これにより、アクセスしてきたユーザのLINEのuserIdがわかります。
あとは、以下のようにPocketのアクセストークンを使ってPocketサーバに対してWebAPI呼び出しをすると、サイトリストを取得することができます。
var params = {
consumer_key: POCKET_CONSUME_KEY,
access_token: user.pocket_access_token,
sort: "newest",
count: body.num_of_items
};
if( body.offset )
params.offset = body.offset;
if( body.search )
params.search = body.search;
var result = await do_post_pocket("https://getpocket.com/v3/get", params );
// console.log(result);
ただ、取得されたリストが少々扱いづらいので、以下のように変換したりソートしたりしています。また、リストによっては一部のデータが存在しない場合があるため、代替文字を設定するようにしています。
let list = Object.keys(result.list).reduce((list, item) => {
if( !result.list[item].resolved_title ) result.list[item].resolved_title = result.list[item].given_title || "no title";
if( !result.list[item].excerpt ) result.list[item].excerpt = "no description";
if( !result.list[item].resolved_url ) result.list[item].resolved_url = result.list[item].given_url;
list.push(result.list[item]);
return list;
}, []);
list.sort((first, second) =>{
let first_time = parseInt(first.time_added);
let second_time = parseInt(second.time_added);
if( first_time > second_time )
return -1;
else if( first_time == second_time )
return 0;
else
return 1;
});
サイトリスト取得(LINEボット)
サイトリスト取得(LIFF)とほぼ同じですが、差分について説明します。
LINEボットでのサイトリストの表示には、カルーセルを利用します。カルーセルのひな型は、Flex Message Simulatorで作成しました。
詳細は、以下を参照してください。
カルーセルを形成するためのデータを関数「createCarouselList」で作成しています。
こんな形式のJSONを作ればよいようにしています。
var obj = {
title: item.resolved_title,
desc: item.excerpt,
image_url: image_url,
action_text: "ブラウザ起動",
action: {
type: "uri",
uri: item.resolved_url
}
};
で、注意がありまして、カルーセルに表示する画像のサイズには上限があるため、リサイズが必要です。
そこで、以下のnpmモジュールを使って既定のサイズのJPEGファイルにリサイズします。
const sharp = require('sharp');
async function resize_image(url, id){
try{
var fname = CACHE_FNAME_BASE + id + ".jpg";
if( !await fileExists(fname) ){
var buffer = await do_get_binary(url);
await sharp(Buffer.from(buffer))
.resize({
width: CACHE_IMAGE_WIDTH,
height: CACHE_IMAGE_WIDTH,
fit: "inside"
})
.jpeg()
.toFile(fname);
}
return PUBLIC_BASE_URL + "img/cache/" + id + ".jpg";
}catch(error){
// console.log(error);
return PUBLIC_BASE_URL + "img/default_image.png";
}
}
作成したJPEGファイルは、publicフォルダに保存し公開します。
終わりに
一通り、説明したつもりです。
不明点あればお知らせください。
以上