3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LINEボットでPocketに登録したお気に入りサイトを参照する

Last updated at Posted at 2022-10-03

Pocketはご存じでしょうか。
PCやスマホから、気に入ったWebサイトをブックマークとして保存し、共有できるサービスです。

image.png

今回は、このPocketに登録したサイトをLINEボットから参照できるようにします。

最終的には以下のような画面になります。
まずこちらが、LINEボットを友達登録した後に、最新のサイトリストを表示させたときのものです。

image.png

次に、こちらが、LIFFアプリとして表示させたときのものです。LIFFは、LINEアプリ内で起動させる方法と、普段お使いのWebブラウザから起動させる方法のどちらでも可能です。

image.png

構成としては以下の通りです。

image.png

ちょっとやり取りが多いですが、のちほど一つ一つ説明していきます。

ソースコードもろもろは以下に置いておきました。

poruruba/PocketLiff

準備

これらを実現するための前準備は以下の通りです。

image.png

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アプリで使いやすいようにするため、リッチメニューを登録します。
以下をご参照ください。

 LINEBotのリッチメニューエディタがないので自作した

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から登録します。

メニューはこんな感じです。

image.png

アプリボタンをクリックと、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証明書を置く必要があります。

以下の部分を環境に合わせて変更します。

api\controllers\pocket-api\index.js
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】";
public\pocket_liff\js\start.js
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」のポストバックが遅れられます。以下の部分でそれを受信しています。

api/controllers/pocket-api/index.js
app.postback(async (event, client) =>{
	console.log(event);
	switch(event.postback.data){
		case 'menu_help' : {

後はそれに対して、以下を実行することで、単純なメッセージを返しています。

api/controllers/pocket-api/index.js
var message = app.createSimpleResponse("フィルタリングしたいキーワードを入力するか、最新リストボタンを押してください。\nまだPocketを登録していない場合は、アプリを起動してください。");
	return client.replyMessage(event.replyToken, message);

また、LINEアプリから、通常のテキスト入力した場合には、以下の部分で受信します。

api/controllers/pocket-api/index.js
app.message(async (event, client) =>{

それに対する応答処理は後述します。

ユーティリティの詳細は、以下の投稿か、「line-utils.js」をご確認ください。
 LINEボットを立ち上げるまで。LINEビーコンも。

LIFF起動

リッチメニューの「アプリ」を選択すると、LIFF URLが送られてLIFFアプリが起動するようにしてあります。

最初は以下のような画面が表示されます。

image.png

LINEログイン

最初に起動すると、LINEログインを促されます。

※LINEアプリからLIFF起動した場合には、自動的にLINEログインが行われるため、以降のLINEログイン処理はありません。

(参考)
https://developers.line.biz/ja/docs/liff/

image.png

OKボタンを押下すると、LINEログイン画面に遷移します。

image.png

ログインが完了すると、以下のようにLINEのユーザのディスプレイ名が表示されます。

image.png

※LINEアプリからLIFFを起動した場合は、「ログアウト」ボタンはありません。LINEログインが自動的に行われログアウトしないためです。

ロジックの部分を抜粋します。

まずは、必ずまっさきにliff.initを呼び出す必要があります。Promiseですので、非同期で処理を待ちます。

public/pocket_liff/js/start.js
        liff.init({ liffId: LIFF_ID })
        .then( () => {
            this.initialize();
        })
        .catch( (err) => {
            console.error(err);
            alert(err);
        })
        .finally(() => {
            history.replaceState('', '', location.pathname);
        });

終わった後に、LINEログイン状態かどうか確認し、ログインしていなければアラートダイアログを表示したのちログインします。

public/pocket_liff/js/start.js
        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を一時的に払い出してもらいます。

以下の部分です。

api/controllers/pocket-api/index.js
			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を返しています。

api/controllers/pocket-api/index.js
			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サーバ提供ページが表示されます。

image.png

許可または拒否すると、LIFFアプリに戻ってきます。

LIFFアプリでは、QueryStringを参照して、Pocketサーバからの戻りであることを判別し、以下を実行します。

public/pocket_liff/js/start.js
                    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のアクセストークンが取得されます。もしユーザが承諾していなかった場合にはエラーが返ります。

api/controllers/pocket-api/index.js
		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サーバが保持していますので、サーバに依頼します。

public/pocket_liff/js/start.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呼び出しは成功します。

api/controllers/pocket-api/index.js
	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呼び出しをすると、サイトリストを取得することができます。

api/controllers/pocket-api/index.js
		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);

ただ、取得されたリストが少々扱いづらいので、以下のように変換したりソートしたりしています。また、リストによっては一部のデータが存在しない場合があるため、代替文字を設定するようにしています。

api/controllers/pocket-api/index.js
		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で作成しました。

詳細は、以下を参照してください。

 LINEボットを立ち上げるまで。LINEビーコンも。

カルーセルを形成するためのデータを関数「createCarouselList」で作成しています。
こんな形式のJSONを作ればよいようにしています。

api/controllers/pocket-api/index.js
		var obj = {
			title: item.resolved_title,
			desc: item.excerpt,
			image_url: image_url,
			action_text: "ブラウザ起動",
			action: {
				type: "uri",
				uri: item.resolved_url 
			}
		};

で、注意がありまして、カルーセルに表示する画像のサイズには上限があるため、リサイズが必要です。

そこで、以下のnpmモジュールを使って既定のサイズのJPEGファイルにリサイズします。

api/controllers/pocket-api/index.js
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フォルダに保存し公開します。

終わりに

一通り、説明したつもりです。
不明点あればお知らせください。

以上

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?