26
26

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 5 years have passed since last update.

Docker + Node.jsでLINE BOTを書いてみる

Last updated at Posted at 2016-06-19

いまさらながら、DockerとNode.jsを使ってサンプルのLINE BOTを書いてみた。ソースはGithubにあります。

送った人名リストから、ランダムで指定の人数の当たりを選んでくれるBOTです。こんな感じで使います。

Screenshot_20160620-001906.png

ポイント

  • BOT側のcallbackのエンドポイントはHTTPS化が必須。ありがとうLet's Encrypt!(以前Let's EncryptではBOT APIが利用できない問題があったようですが、今は解消しています)
  • メッセージを送る際、body.toには宛先のIDを配列で渡す必要がある。間違えるとステータスコード400で返ってくる(はまった)
  • DockerでNodeを使う際のnode_modulesの扱いについては、POSTDの記事が参考になる
  • 残念ながら、現状のLINE BOT APIは、グループチャットをサポートしていないため、グループ参加者から自動で選んでくれるようなことはできません。本当はそれがやりたかった

使用方法

  1. https://developers.line.me からなんやかんやしてアカウント作成
  2. BOT用のドメインと、SSL証明書を用意。証明書持ってない方は、Let's Encryptなどを使うと良いと思います。
  3. $ git clone https://github.com/nosu/lottery-bot.git
  4. サンプルファイルを参考に、1.で作成したアカウントのトークン系を環境変数ファイルに記載($ cp .env.sample .env && vi .env
  5. docker-compose.ymlを開き、ホスト側ポート番号を適当に変更
  6. $ docker-compose build
  7. $ docker-compose up

ソース

app.js
'use strict';
const app        = require('express')();
const bodyParser = require('body-parser');
const request    = require('request');

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

const BASE_URI = "https://trialbot-api.line.me";
const SEND_URI = BASE_URI + '/v1/events';

/**
 * The sessions obj stores the id who sent the message and the list of the person names.
 * Each session will be deleted automatically SESSION_EXPIRE(ms) later.
 * Example: { ubcf6832fae5a186d9b8ac7261e3ff000: ['John', 'Doe'] }
 */
const SESSION_EXPIRE = 20000; // 20 seconds
let sessions = {};

app.post('/callback', (req, res) => {
  const body = req.body;
  body.result.forEach((msg) => {
    if(!sessions[msg.content.from]) {
      // if the session doesn't exist
      console.log('content: ', msg.content);
      const persons = msg.content.text.split('\n').map(p => p.trim());
      if(persons.length >= 2) {
        addSessionWithTimer(msg.content.from, persons, SESSION_EXPIRE);
        const newMsg = `${persons.join("さん, ")}さんから何人選ぶ?数字で教えてー`;
        console.log('sendMessage');
        sendMessage(newMsg, [msg.content.from]);
      } else {
        const newMsg = "1人だけじゃ抽選できないよ!";
        sendMessage(newMsg, [msg.content.from]);
      }
    } else {
      // if the session exists
      console.log('sessions: ', sessions);
      const matchResult = msg.content.text.match(/([0-9]+)/);
      const persons = sessions[msg.content.from];
      if(matchResult) {
        const numberOfWinners = parseInt(matchResult[0]);
        const winners = chooseRandomItems(persons, numberOfWinners);
        const newMsg = `${winners.join("さん, ")}さんが当たりだよ!`;
        sendMessage(newMsg, [msg.content.from]);
      } else {
        const winners = chooseRandomItems(persons, 1);
        const newMsg = `よくわからないから1人だけ選んだ結果…\n${winners[0]}さんが当たりだよ!`;
        sendMessage(newMsg, [msg.content.from]);
      }
      deleteSession(msg.content.from);
    }
  });
  res.send('OK');
});

const addSessionWithTimer = (id, persons, expire) => {
  sessions[id] = persons;
  setTimeout(deleteSession.bind(null, id), expire);
};

const deleteSession = (id) => {
  console.log(`The session <${id}> is deleted.`);
  delete sessions[id];
};

const chooseRandomItems = (array, number) => {
  let a = array.concat();
  let r = [];
  let l = array.length;
  let n = Math.min(number, array.length);
  while(n-- > 0) {
    let i = Math.floor(Math.random() * l--);
    r.push(a[i]);
    a.splice(i, 1);
  }
  return r;
};

const sendMessage = (text, toId) => {
  return new Promise((resolve, reject) => {
    const options = {
      uri: SEND_URI,
      method: "POST",
      json: true,
      headers: {
        "Content-Type": "application/json; charset=UTF-8",
        "X-Line-ChannelID": process.env.LINE_CHANNEL_ID,
        "X-Line-ChannelSecret": process.env.LINE_CHANNEL_SECRET,
        "X-Line-Trusted-User-With-ACL": process.env.LINE_CHANNEL_MID,
      },
      body: {
        to: toId,
        toChannel: 1383378250, // Fixed value
        eventType: "138311608800106203", // Fixed value
        content: {
          contentType: 1,
          toType: 1,
          text: text,
        }
      }
    };
    console.log('options: ', options);
    request(options, (err, res, body) => {
      if(err) {
        reject(new Error(err));
      }
      console.log('request complete without error\nbody: ', body);
      resolve(body);
    });
  });
};

app.listen(3000, () => {
  console.log('Listening on port 3000');
});
Dockerfile
FROM node:6.2.0

RUN useradd --user-group --create-home --shell /bin/false app &&\
  npm install --global npm@3.9.6

ENV HOME=/home/app

COPY package.json $HOME/bot/
RUN chown -R app:app $HOME/*

USER app
WORKDIR $HOME/bot
RUN npm install

USER root
COPY . $HOME/bot
RUN chown -R app:app $HOME/*
USER app

CMD ["node", "app.js"]
docker-compose.yml
version: '2'
services:
  bot:
    build:
      context: .
      dockerfile: Dockerfile
    command: node_modules/.bin/nodemon app.js
    environment:
      NODE_ENV: development
    env_file: .env
    ports:
      - '30002:3000'
    volumes:
      - .:/home/app/bot
      - /home/app/bot/node_modules
26
26
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
26
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?