LoginSignup
8

posted at

updated at

Organization

ToDoアプリがいろいろありすぎるので、LINEにToDo機能を追加してみた。

ToDoアプリは多すぎるし、スマホのアプリを増やしたくない

私はToDo系のアプリを使用していないのですが、使用していない理由としては以下が挙げられます。

  1. ToDoアプリが多すぎるのでどれを使えば良いかわからない
  2. いちいちToDoアプリを開くのが面倒
  3. 使いなれるまでに時間が掛かる

面倒臭がり屋の私でも、いろいろと試してみたときはありました。
だけど、どれもすぐにそのアプリの存在を忘れてしまったり、使い勝手が面倒で諦めてしまいました:dizzy_face:

でもそんな私も毎日のように使用しているアプリがあります。

そう、それはLINE!!

今まではこのLINEにToDo機能があればなー、って思っていたわけですが、あるじゃないですか!LINE Botなんて便利機能が!
LINE Botならプッシュ通知も可能なので、ToDoを知らせてくれるにはもってこいやないか:thumbsup:

ということで、LINE Botを用いて、ToDo機能をLINEに実装してみました!

最低限欲しい機能

  • 手軽にToDoを入力できる
  • 午前、午後、夜のToDoを入力できる
  • 朝にその日のToDoをLINEに送ってくれる
  • 午前、午後、夜のToDoを知りたいときに知れる

まずは、この機能を実装することにしました。
そのために、自分の力量を考えて技術選定しました!

アプリ名はTODO:Botにしました!

使用する技術

LINE Bot(LINE Messaging API)

ToDoのプッシュ機能と、読出しを実施するために使用します。

参考URL:LINE Bot(LINE Messaging APIのドキュメント)

Google Sheets API

ToDo機能を有するということは、ToDoを保存する場所がいるなーということで、今回はGoogleSpreadSheet(以下:スプレッドシート)を使用することにしました!
スプレッドシートを使用するには、Google Sheets APIというAPIを使用します!

参考URL:Google Sheets APIのドキュメント

Google Forms

ToDoを保存するために使用します!
本当はLINE Botから保存できるようにしようと思ったのですが、午前と午後と夜を分けて登録するときに、いちいちそれを入力させるのは嫌だなーと思い、今回はこちらをGoogle Forms(以下:Googleフォーム)を使用しました!
LINEのリッチメニューにこれまたLINEの機能であるLIFFを使用して表示させるようにします!

参考URL:

GitHub Actions

こちらは、朝のプッシュ通知の機能を実装するために使用しました。
時刻をcronで設定して定期実行ができます。

参考URL:GitHub Actionsのドキュメント

完成アプリ

Googleフォームで入力した情報をLINEを通して取得することができました!

では以下は実装編です!

開発環境

  • Node.js v16.16.0
  • npm v8.11.0
  • line/bot-sdk v7.5.2
  • axios v0.27.2
  • express v4.18.1
  • google-spreadsheet v2.0.3
  • Visual Studio Code v1.71.2

下準備

*****各種機能の設定方法を記載しています。ご覧になりたい方はこちらを見てください*****

GoogleフォームからスプレッドシートにToDoを保存できるようにする

Googleフォームの作成

こちらの画面から「フォーム」をクリックして+マークの「空白」をクリックします。
すると、「無題のフォーム」が表示されるので、そちらをカスタマイズしました。
完成画像はこちらです。

  • TODOの日付
  • AM
  • PM
  • Sleep

の情報を保存するようになっています。

Googleフォームの保存先をスプレッドシートにする

こちらの記事を元に「回答」にある「回答先を選択」から「新しいスプレッドシート」作成にて、保存先となるスプレッドシートを作成します。

これでまずWeb上から回答すると、連携したスプレッドシートに値が保存されます。

先ほど作成したスプレッドシートにてGoogle Sheets APIを有効にする

先ほどのスプレッドシートのGoogle Sheets APIを有効にして、Node.jsからスプレッドシートを操作できるようにします。
私は以下の記を参考にして実施しました。

アクセスするための情報をダウンロード等する必要があります

Node.jsを用いてスプレッドシートからデータを取り出す

Node.jsにてスプレッドシートを使用するためのパッケージをインストールします。

ターミナル
npm i google-spreadsheet

 
 下記サンプルコードを用いて、実行結果がスプレッドシートに記載されている内容と同じであれば無事にAPIの接続ができています。

spreadsheet.js
(async function() {
    // パッケージを使用する
    const {
        GoogleSpreadsheet
    } = require('google-spreadsheet');
    const creds = require('./client_secret.json'); // ダウンロードしたJSON
    // spreadsheet IDはURLに記載されている一部になります
    const doc = new GoogleSpreadsheet('*spreadsheet ID*');
    // 認証
    doc.useServiceAccountAuth(creds);

    await doc.loadInfo(); 
    doc.sheetsByTitle[doc.title]
    console.log(doc.title);
    // sheetsByIndex[シートNo]となります。シートが一枚しかなければ0でアクセスできます
    const sheet = doc.sheetsByIndex[0];
    // 行データを取得
    const rows = await sheet.getRows();
    console.log(rows[0].AM);
}());
ターミナル
node spreadsheet.js
- (スプレッドシート名)
- 起きる

参考URL

リッチメニューを作成し、Googleフォームを表示する

リッチメニューを作成することで、Botへの入力がやりやすくなります。
例えば、「PM」と入力をしたいとき普通ならいちいち手入力をして送信しないとダメですが、リッチメニューならメニューからPMボタンを押すと、「PM」を送信することができます。
また、今回はGoogleフォーム画面を表示するためにLIFFを表示できるようにもします。

リッチメニューを設定する

今回のTODO:Botでは以下のようなリッチメニューを作成しました。

  • TODO追加ボタン:Googleフォームを表示する
  • AMボタン:「午前中」を送信する
  • PMボタン:「午後」を送信する
  • Sleepボタン:「寝る前」を送信する

(設定画面)

参考URL

TODO追加に表示するためのLIFFを作成する

こちらの記事の「LIFFアプリを自分で作ってみる」を参考にして作っていきます。
以下を読み替えてください。

  • LFFアプリの元となるWebアプリを作成する ➡ GoogleフォームのURLを使用します
  • 「6.WebアプリにLIFF SDKを組み込む」は無視してください。

あとは登録したときにできる「LIFF URL」を、リッチメニューのTODO追加ボタンの「リンク」に登録すれば、Googleフォームが表示されるようになります。

LIFFの大きさはTallにしています。

LINE Bot側のプログラム

LINE Botの作成

ご自分で作成する場合は、下記を参考に作成してください。
ローカル環境構築ではngrokを使用して開発しました。

参考URL1: 1時間でLINE BOTを作るハンズオン
参考URL2: おうむ返しbotを作ろう

リプライメッセージ側プログラム

*****リプライメッセージのコードをご覧になりたい方はこちらを見てください*****
todo.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');

const PORT = process.env.PORT || 3000;

// Messaging APIを利用するための鍵を設定します。ここは自分のものに書き換えてください。
const config = {
    channelSecret: '**********************************',
    channelAccessToken: '********************************************************'
};
const client = new line.Client(config);
const {
    GoogleSpreadsheet
} = require('google-spreadsheet');
// ダウンロードしたJSON
const creds = require('./client_secret.json');
// スプレッドシートのIDを取得する IDはスプレッドシートのURLから確認できます。
const doc = new GoogleSpreadsheet('***************************************');
// 認証設定
doc.useServiceAccountAuth(creds);
// スプレッドシートから本日のTodoを取得
async function getTodosData() {
    // スプレッドシートと接続
    await doc.loadInfo();
    console.log(doc.title);
    // シートNo0=Todoシートを指定
    const sheet = doc.sheetsByIndex[0];
    doc.sheetsByTitle[doc.title]
    console.log(sheet);
    // 一行ずつ取得
    const rows = await sheet.getRows();
    // 現在時刻を取得 TODO: このコードではUTC時間になっているので、日本時間-9時間となっています。
    const today = new Date();
    // 本日の日時とTodoから取得した日時を比較して、本日の日時のTodoデータだけ取り出す
    const resData = rows.filter((item) => {
        const saveTime = new Date(item.TODOの日付)
        if (saveTime.getFullYear() === today.getFullYear() && saveTime.getMonth() === today.getMonth() && saveTime.getDate() === today.getDate()) {
            console.log(item.TODOの日付)
            console.log(saveTime.getDate())
            console.log(today.getDate())
            return true
        }
        return false;
    });
    console.log(resData);
    // 返答
    return resData;
}

// リプライメッセージ返答処理
// ユーザーからメッセージがあったときに呼び出されます
async function handleEvent(event) {
    if (event.type !== 'message' || event.message.type !== 'text') {
        return Promise.resolve(null);
    }
    // スプレッドシートからTodoデータを取得
    const data = await getTodosData()
    console.log(data)
    // 返答用配列
    let resMessage = []
    let todo = '';

    // 指定された文言に対応したTodoデータをtodo変数に格納する
    if (event.message.text == '午前中') {
        resMessage.push({
            type: 'text',
            text: '午前中の予定は・・・'
        });
        data.forEach(d => {
            if (d.AM != '') {
                todo += d.AM + '\n';
            }
        });
    }
    else if (event.message.text == '午後') {
        resMessage.push({
            type: 'text',
            text: '午後の予定は・・・'
        });
        data.forEach(d => {
            if (d.PM != '') {
                todo += d.PM + '\n';
            }
        });
    }
    else if (event.message.text == '寝る前') {
        resMessage.push({
            type: 'text',
            text: '寝る前の予定は・・・'
        });
        data.forEach(d => {
            if (d.Sleep != '') {
                todo += d.Sleep + '\n';
            }
        });
    }
    else {
        todo = "変なデータ送ってくるな"
    }
    // 最後の改行は取り除いておきます
    todo = todo.trim();
    // 最後にメッセージに入れます。
    resMessage.push({
        type: 'text',
        text: todo
    });

    console.log(resMessage)
    // ユーザーにリプライメッセージを送ります。
    return client.replyMessage(event.replyToken, resMessage);
}

// ここ以降は理解しなくてOKです
const app = express();
app.get('/', (req, res) => res.send('Hello LINE BOT! (HTTP GET)'));
app.post('/webhook', line.middleware(config), (req, res) => {

    if (req.body.events.length === 0) {
        res.send('Hello LINE BOT! (HTTP POST)');
        console.log('検証イベントを受信しました!');
        return;
    } else {
        console.log('受信しました:', req.body.events);
    }

    Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// ローカルで確認するときに必要
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);

プッシュメッセージ側プログラム

*****プッシュメッセージのコードをご覧になりたい方はこちらを見てください*****
push.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');

const PORT = process.env.PORT || 3000;

// Messaging APIを利用するための鍵を設定します。ここは自分のものに書き換えてください。
const config = {
    channelSecret: '**********************************',
    channelAccessToken: '********************************************************'
};
const client = new line.Client(config);
const {
    GoogleSpreadsheet
} = require('google-spreadsheet');
// ダウンロードしたJSON
const creds = require('./client_secret.json'); 
// スプレッドシートのIDを取得する IDはスプレッドシートのURLから確認できます。
const doc = new GoogleSpreadsheet('***********************************');
// 認証設定
doc.useServiceAccountAuth(creds);
// スプレッドシートから本日のTodoを取得
async function getTodosData() {
    // スプレッドシートと接続
    await doc.loadInfo();
    console.log(doc.title);
    // シートNo0=Todoシートを指定
    const sheet = doc.sheetsByIndex[0];
    doc.sheetsByTitle[doc.title]
    console.log(sheet);
    // 一行ずつ取得
    const rows = await sheet.getRows();
    // 現在時刻を取得 TODO: このコードではUTC時間になっているので、日本時間-9時間となっています。
    const today = new Date();
    // 本日の日時とTodoから取得した日時を比較して、本日の日時のTodoデータだけ取り出す
    const resData = rows.filter((item) => {
        const saveTime = new Date(item.TODOの日付)
        if (saveTime.getFullYear() === today.getFullYear() && saveTime.getMonth() === today.getMonth() && saveTime.getDate() === today.getDate()) {
            console.log(item.TODOの日付)
            console.log(saveTime.getDate())
            console.log(today.getDate())
            return true
        }
        return false;
    });
    console.log(resData);
    // 返答
    return resData;
}
// プッシュメッセージ処理
const main = async () => {
    // スプレッドシートからTodoデータを取得
    const data = await getTodosData()
    console.log(data)
    // 返答用配列
    let resMessage = [{
        type: 'text',
        text: '今日の予定は・・・'
    }]
    let todoAM = '午前の予定は・・・' + '\n';
    let todoPM = '午後の予定は・・・' + '\n';
    let todoSleep = '寝る前の予定は・・・' + '\n';
    // 当日のTodoをすべて格納します
    data.forEach(d => {
        if (d.AM != '') {
            todoAM += d.AM + '\n';
        }
        if (d.PM != '') {
            todoPM += d.PM + '\n';
        }
        if (d.Sleep != '') {
            todoSleep += d.Sleep + '\n';
        }
    });
    todoAM = todoAM.trim();
    todoPM = todoPM.trim();
    todoSleep = todoSleep.trim();
    // 3つのメッセージに分けて送信します
    resMessage.push({
        type: 'text',
        text: todoAM
    },{
        type: 'text',
        text: todoPM
    },{
        type: 'text',
        text: todoSleep
    },);

    if (resMessage.length === 0) {
        resMessage = [{
            type: 'text',
            text: '今日の予定はありません'
        }]
    }
    console.log(resMessage)
    // 送信処理
    try {
        const res = await client.broadcast(resMessage);
        console.log(res);        
    } catch (error) {
        console.log(`エラー: ${error.statusMessage}`);
        // console.log(error.originalError.response.data);
    }
}
main();

送信に使用するメッセージオブジェクト配列resMessage[]に格納できるオブジェクト数は最大5件のため、それ以上同時に送信しようとするとエラーとなります。

GitHub Actions側の実装

毎朝7時にプッシュ通知するように設定しています。

GitHub Actionsを実装する際、「 set up a workflow yourself 」を使用する記事が多いですが、個人的にはNode.jsの「Configure」から一部変更する方がやりやすかったです。


name: Node.js CI
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
  schedule:
    # 定期実行する時間・・・ここを変更する UTC時刻のため送信したい時刻の-9を設定する
    - cron: '0 22 * * *'

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: npm ci
    - run: npm run build --if-present
    - run: node push.js # ここにコードを起動する処理をいれる


参考:こちらのYoutubeのGitHub Actionsの箇所を参考にしました。

プッシュ通知ですが、7時に通知するよう設定はしていますが、画像の通り30分程度のタイムラグがありました。

Renderにてデプロイを実施する

最終的にLINE Botのサーバー側を静的ページとしてデプロイし、Webhookとして接続することでTODO:Botが完成となります。
デプロイは以下の記事を参考にして実施しました。記事内のリンクに登録方法なども記載されています。

参考URL:RenderにLINE Botをデプロイする

一部、記事と違う所としては、私の環境ではLINE Botのソースコードはprivateで非公開でしたので、RenderとGitHubの連携が必要でした。記事と同じところに対象のソースコードを入れると、接続が必要です。という画面が出てくるので、メール等で認証作業をすれば無事接続ができます。

デプロイには記事に記載されている通り、時間がかかります!
私は途中でコードの間違いに気がついてデプロイが完了する前に、Pushし直したりしたので途中でデプロイが失敗したりしました。ログ等をみながら変更する際はデプロイが完了したことを確認してからの方が良いと思います。

最後に

今回はLINEに無事Todo機能を実装することができました:bowtie:

がしかし!!

機能としてはまだまだ不十分です。

足りない機能

  • Todoデータを取ってくる時刻がUTC時刻のため日本時間-9時間を起点としているので、朝のプッシュ通知は前日のデータが送られてくる
    ➡朝9時になるまで前日のTodoが送られてきます
  • Googleフォームの日付入力が面倒
    ➡LIFFアプリを独自に作成すれば改善しそう?
  • プッシュ通知と言いながら、実はブロードキャストメッセージを使っているため、ユーザーごとにTodoを管理できていない
    ➡これもLIFFアプリを独自に作成すれば、入力したユーザーを特定できそう!
  • データが増えると、おそらくTodoデータの取得に膨大な時間が掛かる
    ➡終了したTodoデータは削除する等が必要になってくる

思いつく限りでもこれだけあります、、、:sob:

なんとも歯がゆい、、、
まだまだ精進あるのみだな!と思いました!
最終的にはクラウドのデータベースとか使ったほうがやりやすいのかな?と思ったりしています。

みなさんにもお使いいただけますよう、ブラッシュアップしていきたいと思います!:sparkles:

最後までお読みいただきありがとうございました!

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
What you can do with signing up
8