© 2025 JWS

Vue+LIFF(LINE Front-end Framework)+GASでフォームを使ったBOTを作ってみた

  • Vue.js
  • GAS

LIFF を使用したアプリケーションを何か作ってみたかったので、勤怠連絡の BOT を作ってみました。

開発準備

フロントは、jQuery でもよかったのですが、せっかくなので Vue.js の勉強のため Vue.js で構築しています。

Vue CLI3をインストール

まずは、Vue CLI3をインストールします。

インストール

npm install -g @vue/cli
# OR
yarn global add @vue/cli

作成(設定はデフォルトのままで大丈夫です)

vue create vue-liff-bot

パッケージのインストール

yarn install

bootstrap-vueを追加

今回は、BootStrap を利用するので、BootStrap もインストール

yarn add bootstrap-vue

開発スタート

以下で開発を始めます。

yarn run serve

bootstrap-vueの組み込み

main.jsbootstrap-vueを組み込みます。

import Vue from 'vue';
import App from './App.vue';
// BottstrapVueの読み込み
import BootstrapVue from 'bootstrap-vue';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
Vue.config.productionTip = false;
// BootstrapVueの使用
Vue.use(BootstrapVue);
new Vue({
  render: (h) => h(App)
}).$mount('#app');

Vue ファイルの作成

App.vue

まずは、App.vue を変更します。

<template>
  <div id="app" class="pt-3 pb-3">
    <input-form :ttl="title" />
  </div>
</template>
<script>
  // inputFormをComponentとして設定
  import InputForm from '@/components/InputForm.vue';
  export default {
    name: 'app',
    components: {
      InputForm
    },
    data() {
      return {
        // titleをpropとしてComponentに渡す
        title: '勤怠連絡 BOT'
      };
    }
  };
</script>
<style>
  #app {
    color: #2c3e50;
  }
</style>

InputForm.vue

続いて、InputForm.vueを作成します。

<template>
  <div class="container">
    <!-- 親のpropsを受け取る -->
    <h1 class="text-center pb-2">{{ ttl }}</h1>
    <!-- submitイベントを無効に -->
    <form @submit.prevent="">
      <div class="form-group">
        <label for="name">名前</label>
        <input
          type="text"
          class="form-control"
          id="name"
          v-model="data.name"
          @change="createMessage()"
        />
      </div>
      <div class="form-group">
        <label for="reason">遅刻理由</label>
        <select
          class="form-control"
          v-model="data.reason"
          id="reason"
          @change="createMessage()"
        >
          <option v-for="(reason, index) in reasons" :key="index">
            {{ reason }}
          </option>
        </select>
      </div>
      <!-- 理由がその他の時のみ表示 -->
      <div class="form-group" v-show="data.reason == 'その他'">
        <input
          type="text"
          class="form-control"
          id="reson_other"
          @change="createMessage()"
          placeholder="その他の場合は理由を書いてください。"
          v-model="data.reason_other"
        />
      </div>
      <div class="form-group">
        <label>種別</label>
        <div class="form-check" v-for="(type, index) in types" :key="index">
          <input
            class="form-check-input"
            type="radio"
            v-model="data.schedule"
            :id="index"
            :value="type"
            :checked="index != 0 ? '' : 'checked'"
            @change="createMessage()"
          />
          <label class="form-check-label" :for="index">{{ type }}</label>
        </div>
      </div>
      <!-- 種別が遅刻しますの時のみ表示 -->
      <div class="form-group" v-show="data.schedule == '遅刻します'">
        <label>遅刻時間</label>
        <div class="form-row align-items-center">
          <div class="col-5 form-group">
            <select
              class="form-control"
              v-model="data.time1"
              @change="createMessage()"
            >
              <!-- createTimes()の配列を設定 -->
              <option v-for="(time, index) in createTimes(0, 12)" :key="index">
                {{ time }}
              </option>
            </select>
          </div>
          <div class="col-2 text-center form-group">
            <span>~</span>
          </div>
          <div class="col-5 form-group">
            <!-- createTimes()の配列を設定 -->
            <select
              class="form-control"
              v-model="data.time2"
              @change="createMessage()"
            >
              <option v-for="(time, index) in createTimes(1, 13)" :key="index">
                {{ time }}
              </option>
            </select>
          </div>
        </div>
      </div>

      <div class="form-group">
        <label for="message">メッセージ</label>
        <!-- createMessageで生成されたデータを挿入 -->
        <textarea
          class="form-control"
          id="message"
          rows="5"
          v-model="data.message"
        ></textarea>
      </div>

      <div class="form-group pt-2">
        <button
          class="btn btn-primary btn-lg btn-block"
          id="send"
          type="submit"
        >
          送信
        </button>
      </div>
    </form>
  </div>
</template>
<script>
export default {
  name: 'InputForm',
  props: {
    ttl: String
  },
  data() {
    return {
      data: {
        name: '',
        reason: '',
        reason_other: '',
        schedule: '',
        time1: '',
        time2: '',
        message: ''
      },
      // 遅刻理由の中身
      reasons: ['電車遅延', '体調不良', '忘れ物', 'その他'],
      // 種別の中身
      types: [
        '遅刻します',
        '全休をいただきます',
        '午前休をいただきます',
        '午後休をいただきます'
      ],
      // メッセージのテンプレート
      text1: 'おはようございます。',
      text2: 'よろしくお願いします。'
    };
  },

  methods: {
    // 時間のオプションを配列で生成
    createTimes: function (init, to) {
      const times = [];
      for (let index = init; index < to; index++) {
        times.push(`${index * 5}分`);
      }
      return times;
    },

    // inputに変更があった場合にメッセージの作成
    createMessage: function () {
      let text, text_reson, text_schedule;
      // その他の場合は、reason_otherのデータを使用
      if (this.data.reason == 'その他') {
        text_reson = this.data.reason_other;
      } else {
        text_reson = this.data.reason;
      }
      // 遅刻の場合は時間に入力した時間を使用して生成
      if (this.data.schedule == '遅刻します') {
        text_schedule = ${this.data.time1}~${this.data.time2}ほど${this.data.schedule};
      } else {
        text_schedule = this.data.schedule;
      }
      // メッセージを生成
      text = ${this.text1}${this.data.name}です。${text_reson}のため${text_schedule}。${this.text2};
      // データに渡す
      this.data.message = text;
    }
  }
};
</script>

inputselectの変更を検知して、dataを更新+messageを都度生成します。

完成

以下のコマンドでビルドします。

yarn run build

BOT の設定

参考

LIFF の設定は以下の記事が大変参考になります。

LINE Bot : LINE Front-end Framework を liff npm パッケージで操作

設定

https://developers.line.me/ja/docs/liff/getting-started/を参考にして進めます。

  • アクセストークンを作成してコピーしておく
  • 一番下のYour user IDもコピーしておく
  • Channel Secretも後ほど利用するので生成してコピーしておく
  • Webhook送信
  • BOTのグループトーク参加利用するに設定は後ほど設定するので、利用するに設定
  • 自動応答メッセージ利用しない
  • 友だち追加時あいさつ利用しない
  • LINEアプリへのQRコードがありますので、LINE アプリより追加してください。

LIFF を設定

コードの追加

まずは、コードを追加します。

※Vue に組み込みたかったのですが、うまく動作させることができず、外部ファイルで対応しました・・・。

public/index.htmlhead に以下を追加します。

<script src="https://d.line-scdn.net/liff/1.0/sdk.js"></script>
<script src="./send.js"></script>

続いて、send.jsを新規作成します。場所は、public/send.jsです。

window.onload = function (e) {
  // sendにclickイベントを追加
  document.getElementById('send').addEventListener('click', function () {
    // messageの中身を取得し、sendMessagesのtextに入れる
    var ms = document.getElementById('message').value;
    liff
      .sendMessages([
        {
          type: 'text',
          text: ms
        }
      ])
      .then(function () {
        window.alert('送信完了');
        // aleartでOKを押すと、自動でLIFFウィンドウが閉じる
        liff.closeWindow();
      })
      .catch(function (error) {
        window.alert('Error sending message: ' + error);
      });
  });
};

liff.sendMessageで送信します。

公開

LIFFhttpsが必要なため、わざわざサーバーを用意する必要のないGitHub Pagesで公開します。

まずは、buildします。

yarn run build

続いて

  • .gitignoreからdistを外す
  • githubにリポジトリを作成
git init
git add .
git commit -m 'init'
git remote add origin https://github.com/xxxxxxx/xxxxxx.git
git push -u origin master
  • gitpushした後は、Settingsをクリック
  • GitHub PagesSourcemaster branchにしてsave

これで、GitHub Pagesに公開されます。

公開された URL は、

https://<ユーザー名>.github.io/<リポジトリ名>/dist/

になります。

LIFF の設定

まずは LIFFのnpmパッケージをインストールします。

npm install -g liff

liff initで初期化します。上記で取得したチャネルのアクセストークンを入れます。

liff init <アクセストークン>

LIFF アプリケーションの登録します。今回はtallを指定します。

liff add https://<ユーザー名>.github.io/<リポジトリ名>/dist/ tall

戻り値から LIFF ID および Accessible URL をコピーしておきます。

[LIFF ID] xxxxxxxxxxxxxxx created
accessible uri : line://app/xxxxxxxxxxxxxxxx

liff send コマンドでリンクを送付します。LINE アプリよりリンクをクリックしてテスト。上記で取得したLIFF IDYour user IDを入れます。

liff send <LIFF ID> <Your user ID>

BOT より送られたリンクを確認して、フォームから送信を押してメッセージが表示されれば完了です!

ブラッシュアップ

リンクのクリックから起動させるのではなく、BOT のリッチメニューか LIFF を起動させると、より良いでしょう。

リッチメニューの実装は以下を参考にしてください。

https://developers.line.me/ja/docs/messaging-api/using-rich-menus/

  • LINE@マネージャーからログインして作成した BOT を選択します。
  • リッチメニューより、メニューを作成します。
  • コンテンツ設定リンクを上記で取得したAccessible URLに設定します。

これでメニューから LIFF を起動させることができます。

GAS の設定

参考

GAS の設定は以下の記事が参考になります。

設定手順

以下の手順で GAS を設定します。

  1. Googleアカウントにログイン
  2. Googleドライブを開く
  3. マイドライブをクリック
  4. その他アプリを追加Google Apps Script
  5. コードを記述 → 保存
  6. 公開ウェブアプリケーションとして導入
  7. アプリケーションにアクセスできるユーザー全員(匿名ユーザーを含む)に変更
  8. 公開をクリックし、出てきた現在のウェブ アプリケーションの URLをコピーする
  9. LINE Developersの対象 BOT のWebhook URLに先ほどコピーした GASのURLを張り付ける

以上で GAS を利用した Webhook の設定が完了です。

コード

スクリプトは、以下になります。

//LINE Developersで取得したアクセストークンを入れる
var CHANNEL_ACCESS_TOKEN = '<LINE BOT アクセストークン>';
// 送信先のグループID
var GROUP_ID = '<グループへ参加させて set を送ると送られる>';
// グループIDを取得するためのキーワード
var SETTING_MESSAGE = 'set';

var line_reply = 'https://api.line.me/v2/bot/message/reply';
var line_push = 'https://api.line.me/v2/bot/message/push';

//ポストで送られてくるので、送られてきたJSONをパース
function doPost(e) {
  var webhook = JSON.parse(e.postData.contents).events[0];

  //返信するためのトークン取得
  var reply_token = webhook.replyToken;
  if (typeof reply_token === 'undefined') {
    return;
  }

  //送られたメッセージ内容を取得
  var message = webhook.message.text;
  var gid = webhook.source.groupId;

  // ユーザーの会話(グループIDが存在しない)
  if (typeof gid === 'undefined') {
    // 確認用メッセージの返信
    UrlFetchApp.fetch(line_reply, {
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN
      },
      method: 'post',
      payload: JSON.stringify({
        replyToken: reply_token,
        messages: [
          {
            type: 'text',
            text: 'ご利用ありがとうございます。以下のメッセージをグループへ送信しました。'
          },
          {
            type: 'text',
            text: message
          }
        ]
      })
    });
    // グループへメッセージをpush
    UrlFetchApp.fetch(line_push, {
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN
      },
      method: 'post',
      payload: JSON.stringify({
        to: GROUP_ID,
        messages: [
          {
            type: 'text',
            text: message
          }
        ]
      })
    });
    // グループIDを取得する
  } else if (message == SETTING_MESSAGE) {
    // キーワードが等しい場合にグループID返信する
    UrlFetchApp.fetch(line_reply, {
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        Authorization: 'Bearer ' + CHANNEL_ACCESS_TOKEN
      },
      method: 'post',
      payload: JSON.stringify({
        replyToken: reply_token,
        messages: [
          {
            type: 'text',
            text: 'グループIDは以下の通りです。'
          },
          {
            type: 'text',
            text: gid
          }
        ]
      })
    });
  }

  return ContentService.createTextOutput(
    JSON.stringify({ content: 'post ok' })
  ).setMimeType(ContentService.MimeType.JSON);
}

解説

メッセージにgroupIdが含まれるかで条件を分岐させています。

  • 対ユーザーには送信メッセージのリピート(確認メッセージ)
  • 対グループにユーザーから送信されたメッセージを送信

また、groupIdを取得するためには、

  1. BOT をグループに参加させる
  2. setを発言する
  3. BOT がgroupIdを発信します

以上で取得できます。

取得したgroupIdは GAS 上のコードのGROUP_IDの箇所へ入れてください。

入れると、指定したグループへ発信できます。

まとめ

groupIdの取得やグループへの発信、対個人への発信などがやや面倒だと感じました。

また、グループに参加したときのイベントjoinを利用すればよかったのですが、うまくイベントを発生させることができず、setというキーワードを今回は利用しています。

そのほかにも、GAS を利用すれば、

  • メールの送信
  • スプレッドシートへのログの記載

などなど、色々と派生させることができると思いますので、色々カスタマイズできると思います。

Share