20191224のNode.jsに関する記事は8件です。

【Node.js】NW.jsでnodeなしで動くアプリを作りたい!

大幅に遅れましたが、この記事は 株式会社ピーアールオー(あったらいいな!を作ります) Advent Calendar 2019 の19日目の記事です。

はじめに

最近、お仕事でもNode.jsを使うことが増えてきました。
JSベースなので結構雑にツールを作れてありがたいのですが、そのツールを誰かに連携する時に相手方もNode.jsを入れてもらう必要があって連携は中々ハードル高めです。
という訳で、今回はWindowsでNode.jsで作成したものをスタンドアロンアプリとして出力できるNW.jsを触ってみました。

実行環境

OS: Windows 10 Pro v1903
node.js: v10.13.0
NW.js: 0.43.2
NW-Builder: 3.5.7

導入手順…と言いたいところですが

NW.jsの導入についてはQiita上にかなりしっかりまとめていただいている記事が存在します。

NW.jsでデスクトップアプリの夢を見る!

大まかな流れは上記通りなので、細かい手順は省略して、実際に触ってみて躓いたりしたところをまとめてみます。

まずはNW.jsを動か…せない!

もう新しいものに触ると必ず起こる現象ですね。
今回は以下を通らせるために苦戦しました。

nw ./src

当然といえば当然ですが、npm install --saveでインストールしたモジュールにはパスが通っていません。

nwjs, nw-builderの各実行ファイルはプロジェクト直下の./node_modules/.bin/配下にインストールされてますので、
実際に実行する場合はパスを指定して叩く必要があります。
例えば、プロジェクトフォルダ直下で実行する場合は

NW.js
./node_modules/.bin/nw

nw-builder
./node_modules/.bin/nwbuilder

という感じですね。global installしてしまえば解決しちゃいますが…。

package.jsonのscriptに実行エイリアスを書く場合は、プロジェクト直下のpackage.jsonに記載する必要があります。
また、mainをsrc/.../index.htmlのように直下からの相対パスに直しておきましょう。

よく見るおすすめ構成ではpackage.jsonが二重になっているため、そこ起因で変な事が起きやすいです。
上手くコマンドが通らないときはまずpackage.jsonを疑ってみるのが良さそうです。

沈黙を保つDevTools

NW.jsはChromiumベースなので、Chromiumで使用できるDevToolがそのまま使えるとのこと。
F12を押せばお馴染みのDevToolが…出てこない!

NW.jsではDevToolsさんは通常のNW.jsをインストールしただけでは働いてくれません。(2019/12時点)

https://github.com/nwjs/nw.js/issues/4383
上記issueで回答されているように、DevToolsを使用したい場合は以下のコマンドを使用してSDKモードでインストールする必要があります。

npm install nw --nwjs_build_type=sdk

ちなみに、このモードのままビルドを実施すると出力されたアプリでもDevToolsが利用可能になります。
出力されたアプリでDevToolsを使われたくない場合はビルド前に切り替えておきましょう。

ビルド時に失踪するNPMパッケージ

開発環境で動作確認OK!ビルドも正常完了!当然、出力されたアプリも動かない!ヨシ!…あれ?

こういう時は落ち着いてSDKモードで再度アプリを出力してDevToolsを起動してみましょう。
今回の場合は以下のエラーが発生していました。

Uncaught Error: Cannot find module '(モジュール名)'

なぜか開発環境では動いていたはずモジュールが失踪してしまってますね…。
この場合、疑うべきはNPMパッケージのインストール場所です。

先ほど紹介した手順でプロジェクトを作成した場合、大体以下のプロジェクト構成になっていると思われます。

/project
  + /node_modules
  + /src
    + (index.js などなど)
    + package.json
  + package.json

このプロジェクト構成では/project直下は、NW.jsでbuildするために必要な外殻であり、
動かしたいプロジェクトの実体は/src配下です。

ここまで言えばもうわかる方も多いと思いますが、今回のケースでは/project 直下でNPMインストールをしてました。
nw-builderでビルドするのは当然/src配下なのでビルド後のアプリには./project/node_modulesに含まれてるNPMパッケージは含まれていません。そのため、今回の事象が発生しました。でもやりたくなるじゃん!

正しく動かすためには、npm installは、/src配下で実施する必要があります。
(package.jsonが/src配下にあるんですから当然と言えば当然ですが…。思い込みって怖い。)

MacOS向けのビルドが通らない

NW-builderではWindows, MaxOSX, Linux用のアプリをそれぞれ出力することが可能ですが、
MaxOSX用のビルドは以下のエラーでこけます。

 UnhandledPromiseRejectionWarning: Error: EPERM: operation not permitted

nw-builderのissueを確認する限り、どうやらWindows10のシンボリックリンクの権限の問題のようです。
https://github.com/nwjs-community/nw-builder/issues/463

即時解決は難しかったので、今回はdocker上のlinuxで動かすことで解決してしまいました…。やはりdockerはすべてを解決する

アプリケーションサイズが大きすぎる

これはそのままです。Chromiumを動かすためのドライバーがセットで入ることになるので、非常にサイズが大きくなります。
Hello Worldを出力するだけの簡単なWindowsアプリでどのくらい大きくなるかを確認してみると、

プロジェクト本体:108 B → 出力後:298 MB

これだけ膨れます。増えるワカメみたいですね。

地がこれだけ大きいので、重い事で有名なNPMパッケージをアプリ内に大量に取り入れるのはあまり得策ではなさそうです。
NPMパッケージをあまり使わない、小規模なツールを作成するために使用するのが向いていそうに思えます。

おわりに

色々躓いたり引っかかったところもありましたが、Node.jsの資産を活かしつつ相手の環境を縛らない上にUIまで作れちゃうNW.js、中々いい感じです。一方で、大きくなりがちなサイズやChromiumに依存している所を見ると、頒布向けというより内製ツール向けな印象も受けました。
とにかくプロジェクト作成から出力までが手軽なので、簡単な内製ツールで、UIが必要な場合などは使ってみるのどうでしょうか?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Heroku Postgres] Herokuを使って無料でデータベースを利用する

はじめに?

メリークリスマス!!!!??
クリスマスいかがお過ごしでしょうか???
ふっけです。今回はNitKitアドベントカレンダーということで、高専祭のクラス展示で入退場システムを作成したときにHerokuのPostgresSQLを使用したので、その方法について書きます。

環境?

Node.js v12.8.0

データベースの導入?

Herokuでのプロジェクトの作成は省略します。
Herokuでプロジェクトを作成したあとResourcesのAdd-onsからHeroku Postgresを追加します。
スクリーンショット 2019-12-24 15.20.29.png
追加後 Heroku Postgresのリンクから管理画面に飛ぶことができます。
スクリーンショット 2019-12-24 15.28.10.png

データベースへのアクセス?

先程作成したデータベースにターミナルから接続します。
データベースの管理画面の Settings > Database Credentials > View Credentials のHeroku CLIをコピーして実行します。
すると以下のように接続することができます。接続ができればテーブルの作成等を行えます。
スクリーンショット 2019-12-24 20.58.35.png

PostgreSQLの基本的なコマンド
https://qiita.com/H-A-L/items/fe8cb0e0ee0041ff3ceb

プログラムの作成?

PostgresSQLへの接続のために使用するモジュールを追加します。

yarn add pg

接続用クラスファイルを作成します。

database.js
import Pool from 'pg'
module.exports = class DB {
  constructor() {
    this.pool = new Pool({
      connectionString: DATABASE_URI,
      ssl: true,
    )}
  }
  async query(param) {
    const client = await this.pool.connect()
    const { row } = await client.query(param)
    client.release()
    return row
  }
}

Expressでサーバを立てます。

yarn add express
index.js
import express from 'express'
import database from './database'
const app = express()
const PORT = process.env.PORT || 3000
app.use(function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*")
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
    next()
})

// jsonを扱えるようにする
app.use(express.json())
app.use(express.urlencoded({
    extended: true
}))

const db = new database
// メイン処理
api.get('/', async (req, res) => {
  const result = await db.query(SQL)
  return res.status(200).send(result).end()
})

app.listen(PORT)
console.log(`Server running at ${PORT}`)

このような感じでqueryにSQL文を渡すと結果が帰ってくるAPIサーバを作ることができます。

デプロイ?

GitHubのリポジトリと連携させると簡単にデプロイすることができます。
またデータベースのURIなどをHerokuの環境変数にしましょう。

heroku 初級編 - GitHub から deploy してみよう -
https://qiita.com/sho7650/items/ebd87c5dc2c4c7abb8f0

監視?

Heroku PostgresではDataclipsという機能でデータベースの中身を見ることができます。この機能とても便利です。
スクリーンショット 2019-12-24 21.18.37.png

最後に?

Herokuを使うと無料でデプロイからデータベースの利用までできるのでおすすめです。
ぜひ使ってみてください。

実際に使用したリポジトリ
https://github.com/FukeKazki/3i-entry-exit-server
入退場システムについてのブログ
https://bit.ly/35RHPDm

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINE WORKS Bot APIをひと通り触ってみる(node.js)#2

LINEWORKS Advent Calendar 2019 / 25日目の最終日の記事です。

本記事では、LINE WORKS Bot のメッセージ送信 API をnode.jsでひと通り触ってみたいと思います。

前回:LINE WORKS Bot APIをひと通り触ってみる(node.js)#1

0. はじめに

記事の流れになります。

  1. こんなの作ります
  2. 環境準備
  3. 作ってみる
  4. 動かしてみる
  5. 気づいたこと
  6. まとめ

1. こんなの作ります

LINE WORKS Botのメッセージ送信には下表のタイプが存在し、そのタイプをすべて網羅するBotを作ります。
また、quick reply (クイックリプライ)も使用します。

送信タイプ 説明
text テキストメッセージ送信
image 画像送信
link リンク送信
sticker スタンプ送信
button_template ボタンテンプレート送信
list_template リストテンプレート送信
carousel カルーセル送信
image_carousel 画像カルーセル送信
  • LINE WORKS アプリより、下記の値を入力するとそれぞれのメッセージを Bot が返します。
入力値(※1) Botが返すメッセージ
Q クイックリプライ
B ボタンテンプレート
L リストテンプレート
C カルーセル
I 画像カルーセル
U リンク + クイックリプライ
スタンプ(※2) 同じスタンプ + クイックリプライ
画像 同じ画像 + クイックリプライ
場所 住所、経度緯度の文字 + クイックリプライ
その他の文字 テキストメッセージ

  ※1)入力値は大文字小文字を意識しません
  ※2)使用できるスタンプはこちら

  • クイックリプライ、ボタンテンプレート、リストテンプレート、カルーセル、画像カルーセルが使用できる Action Object もすべて網羅します。
04.png
05.png
07.png
10.png

2. 環境準備

LINE WORKS Bot APIをひと通り触ってみる(node.js)#1 」の 「2. 環境準備 」を参照。

3. 作ってみる

LINE WORKS Bot APIをひと通り触ってみる(node.js)#1 」の 「メッセージの送受信制御 (BotMessageService.js) 」の BotMessageService クラスをカスタマイズします。
ベタ書き&すべての機能を網羅するため長いプログラムになってすいません。

BotMessageService.js
const request = require('request');

/**
 * コールバックタイプ
 */
const CALL_BACK_TYPE = {
  /**
   * メンバーからのメッセージ
   */
  message : 'message',
  /**
   * Bot が複数人トークルームに招待された
   * このイベントがコールされるタイミング
   *  ・API を使って Bot がトークルームを生成した
   *  ・API を使って Bot がトークルームを生成した
   *  ・メンバーが Bot を含むトークルームを作成した
   *  ・Bot が複数人のトークルームに招待された
   * ※メンバー1人と Bot のトークルームに他のメンバーを招待したらjoinがコールされる(最初の1回だけ)
   *  招待したメンバーを退会させ、再度他のメンバーを招待するとjoinedがコールされるこれ仕様?
   *  たぶん、メンバー1人と Botの場合、トークルームIDが払い出されてないことが原因だろう。。。
   */
  join : 'join',
  /**
   * Bot が複数人トークルームから退室した
   * このイベントがコールされるタイミング
   *  ・API を使って Bot を退室させた
   *  ・メンバーが Bot をトークルームから退室させた
   *  ・何らかの理由で複数人のトークルームが解散した
   */
  leave : 'leave',
  /**
   * メンバーが Bot のいるトークルームに参加した
   * このイベントがコールされるタイミング
   *  ・Bot がトークルームを生成した
   *  ・Bot が他のメンバーをトークルームに招待した
   *  ・トークルームにいるメンバーが他のメンバーを招待した
   */
  joined : 'joined',
  /**
   * メンバーが Bot のいるトークルームから退室した
   * このイベントがコールされるタイミング
   *  ・Bot が属するトークルームでメンバーが自ら退室した、もしくは退室させられた
   *  ・何らかの理由でトークルームが解散した
   */
  left : 'left',
  /**
   * postback タイプのメッセージ
   * このイベントがコールされるタイミング
   *  ・メッセージ送信(Carousel)
   *  ・メッセージ送信(Image Carousel)
   *  ・トークリッチメニュー
   */
  postback : 'postback',
};

/**
 * コールバックコンテンツタイプ
 */
const CALL_BACK_MESSAGE_CONTENT_TYPE = {
  /**
   * テキスト
   */
  text : 'text',
  /**
   * 場所
   */
  location : 'location',
  /**
   * スタンプ
   */
  sticker : 'sticker',
  /**
   * 画像
   */
  image : 'image'
};

/**
 * メッセージコンテンツタイプ
 */
const MESSAGE_CONTENT_TYPE = {
  /**
   * テキスト
   */
  text : 'text',
  /**
   * 画像
   */
  image : 'image',
  /**
   * リンク
   */
  link : 'link',
  /**
   * スタンプ
   */
  sticker : 'sticker',
  /**
   * ボタンテンプレート
   */
  buttonTemplate : 'button_template',
  /**
   * リストテンプレート
   */
  listTemplate : 'list_template',
  /**
   * カルーセル
   */
  carousel : 'carousel',
  /**
   * 画像カルーセル
   */
  imageCarousel : 'image_carousel'
};

/**
 * BotMessageServiceクラス
 */
module.exports = class BotMessageService {

  /**
   * BotMessageServiceを初期化します。
   * @param {string} serverToken Serverトークン
   */
  constructor (serverToken) {
    this._serverToken = serverToken;
    this.imageIndex = 0;
  }

  /**
   * LINE WORKS にBotメッセージを送信します。
   * @param {object} callbackEvent リクエストのコールバックイベント
   */
  async send(callbackEvent) {
    let res = this._getResponse(callbackEvent);
    if (!res) return;
    return new Promise((resolve, reject) => {
      // LINE WORKS にメッセージを送信するリクエスト
      request.post(this._createMessage(res), (error, response, body) => {
          if (error) {
            console.log('BotService.send error');
            console.log(error);
          }
          console.log(body);
          // 揉み消してます!
          resolve();
      });
    });
  }

  /**
   * LINE WORKS に送信するBotメッセージを作成して返します。
   * @param {object} res レスポンスデータ
   */
  _createMessage(res) {
    return {
      url: `https://apis.worksmobile.com/r/${process.env.API_ID}/message/v1/bot/${process.env.BOT_NO}/message/push`,
      //url: `https://apis.worksmobile.com/${process.env.API_ID}/message/sendMessage/v2`,
      headers: {
        'Content-Type': 'application/json;charset=UTF-8',
        consumerKey: process.env.CONSUMER_KEY,
        Authorization: `Bearer ${this._serverToken}`
      },
      json: res
    };
  }

  /**
   * メンバーIDを連結して返します。
   * @param {Array} memberList メンバーリスト
   * @return {string} メンバーIDリスト文字列
   */
  _buildMember(memberList) {
    let result = '';
    if (!memberList) return result;
    memberList.forEach(m => {
      if (result.length > 0) result += ',';
      result += m;
    });
    return result;
  }

  /**
   * Bot実装部
   * @param {object} callbackEvent リクエストのコールバックイベント
   * @return {string} レスポンスメッセージ
   */
  _getResponse(callbackEvent) {
    console.log(callbackEvent);

    let res = {};

    if (callbackEvent.source.roomId) {
      // 受信したデータにトークルームIDがある場合は、送信先にも同じトークルームIDを指定します。
      res.roomId = callbackEvent.source.roomId;
    } else {
      // トークルームIDがない場合はBotとユーザーとの1:1のチャットです。
      res.accountId = callbackEvent.source.accountId;
    }

    switch (callbackEvent.type) {
      case CALL_BACK_TYPE.message:

        switch (callbackEvent.content.type) {
          case CALL_BACK_MESSAGE_CONTENT_TYPE.text:
            if (callbackEvent.content.postback == 'start') {
              // メンバーと Bot との初回トークを開始する画面で「利用開始」を押すと、自動的に「利用開始」というメッセージがコールされる
              console.log(`start`);
              res.content = { type: MESSAGE_CONTENT_TYPE.text, text: 'ト〜クルームに〜〜。ボトやまが〜くる〜!\n下記を入力するとボトやまが特別な応答をします(大文字小文字を区別しません)。\n・b:button template\n・l:List template\n・c:carousel\n・i:image carousel\n・q:quick reply' };
              return res;
            }

            let content = this._getButtonTemplateContent(callbackEvent.content.postback, callbackEvent.content.text)
              || this._getListTemplateContent(callbackEvent.content.postback, callbackEvent.content.text)
              || this._getCarouselContent(callbackEvent.content.postback, callbackEvent.content.text)
              || this._getImageCarouselContent(callbackEvent.content.postback, callbackEvent.content.text)
              || this._getQuickReplyContent(callbackEvent.content.postback, callbackEvent.content.text)
              || this._getLinkContent(callbackEvent.content.postback, callbackEvent.content.text);
            if (content) {
              res.content = content;
            } else {
              console.log(CALL_BACK_TYPE.message);
              res.content = { type: MESSAGE_CONTENT_TYPE.text, text: `ですよね〜〜〜。\n(受信データ:${callbackEvent.content.text})` };
            }
            break;

          case CALL_BACK_MESSAGE_CONTENT_TYPE.location:
            // 場所のコールバックは場所データをテキストで返す
            res.content = { 
              type: MESSAGE_CONTENT_TYPE.text, 
              text: `住所:${callbackEvent.content.address}\n緯度:${callbackEvent.content.latitude}\n経度:${callbackEvent.content.longitude}`,
              quickReply: this._getQuickReplyItems()
             };
            break;

          case CALL_BACK_MESSAGE_CONTENT_TYPE.sticker:
            // スタンプのコールバックはおうむ返し(同じスタンプを返す)
            // ※使えないスタンプがあるようです(LINE WORKSぽいスタンプは使えない。。。)
            res.content = { 
              type: MESSAGE_CONTENT_TYPE.sticker, 
              packageId: callbackEvent.content.packageId, 
              stickerId: callbackEvent.content.stickerId,
              quickReply: this._getQuickReplyItems()
            };
            break;

          case CALL_BACK_MESSAGE_CONTENT_TYPE.image:
            // 画像のコールバックはおうむ返し(同じ画像を返す)
            res.content = { 
              type: MESSAGE_CONTENT_TYPE.image, 
              resourceId: callbackEvent.content.resourceId,
              quickReply: this._getQuickReplyItems()
            };
            break;

          default:
            console.log('知らないcontent.typeですね。。。');
            return null;
        }
        break;

      case CALL_BACK_TYPE.join:
        console.log(CALL_BACK_TYPE.join);
        res.content = { type: MESSAGE_CONTENT_TYPE.text, text: 'うぃーん!' };
        break;

      case CALL_BACK_TYPE.leave:
        console.log(CALL_BACK_TYPE.leave);
        break;

      case CALL_BACK_TYPE.joined: {
        console.log(CALL_BACK_TYPE.joined);
        res.content = { type: MESSAGE_CONTENT_TYPE.text, text: `${this._buildMember(callbackEvent.memberList)} いらっしゃいませ〜そのせつは〜` };
        break;
      }

      case CALL_BACK_TYPE.left: {
        console.log(CALL_BACK_TYPE.left);
        res.content = { type: MESSAGE_CONTENT_TYPE.text, text: `${this._buildMember(callbackEvent.memberList)} そうなります?` };
        break;
      }

      case CALL_BACK_TYPE.postback:
        // QuickReply, Carousel, ImageCarouselからのPostback(このコールバック後、CALL_BACK_TYPE.messageのコールバックがコールされる)
        console.log(CALL_BACK_TYPE.postback);
        let content = this._getButtonTemplateContent(callbackEvent.data) 
        || this._getListTemplateContent(callbackEvent.data)
        || this._getCarouselContent(callbackEvent.data)
        || this._getImageCarouselContent(callbackEvent.data)
        || this._getQuickReplyContent(callbackEvent.data);
        if (content) res.content = content;
        break;

      default:
        console.log('知らないコールバックですね。。。');
        return null;
    }

    return res;
  }

  /**
   * Link コンテンツを返します。
   * @param {Array} conditions 条件
   * @return {object} コンテンツ
   */
  _getLinkContent(...conditions) {
    if (!conditions.some(condition => condition && condition.toUpperCase() === 'U')) return;
    return { 
      type: MESSAGE_CONTENT_TYPE.link, 
      contentText: 'Link からの〜〜〜。',
      linkText: 'LINE WORKS',
      link: 'https://line.worksmobile.com/jp/',
      quickReply: this._getQuickReplyItems()
    };
  }

  /**
   * Quick reply コンテンツを返します。
   * @param {Array} conditions 条件
   * @return {object} コンテンツ
   */
  _getQuickReplyContent(...conditions) {
    if (!conditions.some(condition => condition && condition.toUpperCase() === 'Q')) return;
    return { 
      type: MESSAGE_CONTENT_TYPE.text, 
      text: 'QuickReply からの〜〜〜。',
      quickReply: this._getQuickReplyItems()
    };
  }

  /**
   * Quick reply アイテムリストを返します。
   * @return {Array} アイテムリスト
   */
  _getQuickReplyItems() {
    return {
      items: [
        {
          imageUrl: `${process.env.IMAGE_FILE_HOST}/images/giraffe01.png`,
          action: {
            type: 'postback',
            label: 'ButtonTemp',
            data: 'button_template',
            displayText: 'button_template ください'
          }
        },
        {
          imageUrl: `${process.env.IMAGE_FILE_HOST}/images/panda01.png`,
          action: {
            type: 'postback',
            label: 'ListTemp',
            data: 'list_template',
            displayText: 'list_template ください'
          }
        },
        {
          imageUrl: `${process.env.IMAGE_FILE_HOST}/images/giraffe02.png`,
          action: {
            type: 'postback',
            label: 'Carousel',
            data: 'carousel',
            displayText: 'carousel ください'
          }
        },
        {
          imageUrl: `${process.env.IMAGE_FILE_HOST}/images/panda02.png`,
          action: {
            type: 'postback',
            label: 'ImageCarousel',
            data: 'image_carousel',
            displayText: 'image_carousel ください'
          }
        },
        {
          imageUrl: `${process.env.IMAGE_FILE_HOST}/images/sushi.png`,
          action: {
            type: 'postback',
            label: 'QuickReply',
            data: 'q',
            displayText: 'QuickReply ください'
          }
        },
        {
          imageUrl: `${process.env.IMAGE_FILE_HOST}/images/sushi.png`,
          action: {
            type: 'message',
            label: 'すし',
            text: 'すし'
          }
        },
        {
          imageUrl: `${process.env.IMAGE_FILE_HOST}/images/lw.png`,
          action: {
            type: 'uri',
            label: 'LINE WORKS',
            uri: 'https://line.worksmobile.com/jp/'
          }
        },
        {
          action: {
            type: 'camera',
            label: 'カメラ'
          }
        },
        {
          action: {
            type: 'cameraRoll',
            label: 'カメラロール'
          }
        },
        {
          action: {
            type: 'location',
            label: '場所'
          }
        }
      ]
    }
  }

  /**
   * Button template コンテンツを返します。
   * @param {Array} conditions 条件
   * @return {object} コンテンツ
   */
  _getButtonTemplateContent(...conditions) {
    if (!conditions.some(condition => condition && (condition.toUpperCase() === 'B' || condition === MESSAGE_CONTENT_TYPE.buttonTemplate))) return;
    return { 
      type: MESSAGE_CONTENT_TYPE.buttonTemplate, 
      contentText: 'ButtonTemplate からの〜〜〜。',
      actions: this._getButtonActions(),
      //quickReply: this._getQuickReplyItems()
    };
  }

  /**
   * Buttonアクションリストを返します。
   * @return {Array} アクションリスト
   */
  _getButtonActions() {
    return [
      // button_templateのactionでは typeはmessageとuriしか使えない。つまり postback、camera、cameraRoll、locationは使えない
      {
        type: 'message',
        label: 'Message lable',
        text: 'Message text'
      },
      {
        type: 'message',
        label: 'Button postback',
        text: 'button_template ください',
        postback: 'button_template' 
      },
      {
        type: 'message',
        label: 'List postback',
        text: 'list_template ください',
        postback: 'list_template'
      },
      {
        type: 'message',
        label: 'Carousel postback',
        text: 'carousel ください',
        postback: 'carousel'
      },
      {
        type: 'message',
        label: 'Image carousel pb',
        text: 'image_carousel ください',
        postback: 'image_carousel'
      },
      {
        type: 'message',
        label: 'QuickReply postback',
        text: 'QuickReply ください',
        postback: 'q'
      },
      {
        type: 'uri',
        label: 'LINE WORKS',
        uri: 'https://line.worksmobile.com/jp/'
      }
    ];
  }

  /**
   * List template コンテンツを返します。
   * @param {Array} conditions 条件
   * @return {object} コンテンツ
   */
  _getListTemplateContent(...conditions) {
    if (!conditions.some(condition => condition && (condition.toUpperCase() === 'L' || condition === MESSAGE_CONTENT_TYPE.listTemplate))) return;
    return { 
      type: MESSAGE_CONTENT_TYPE.listTemplate, 
      coverData: {
        backgroundImage: `${process.env.IMAGE_FILE_HOST}/images/lw.png`,
        //backgroundResourceId: '',
        title: 'ListTemplate からの〜〜〜。(title)',
        subtitle: 'サブタイトル',
      },
      // 最大4つの要素を指定可能
      elements: this._getListElements(),
      // 最大2*2の配列でアクションを指定可能
      actions: this._getListActions(),
      //quickReply: this._getQuickReplyItems()
    };
  }

  /**
   * List要素リストを返します。
   * @return {Array} 要素リスト
   */
  _getListElements() {
    // list_template.elementsのactionでは typeはmessageとuriしか使えない。つまり postback、camera、cameraRoll、locationは使えない
    return [
      {
        title: 'List message title',
        subtitle: 'List message subtitle',
        image: `${process.env.IMAGE_FILE_HOST}/images/lw.png`,
        //resourceId: '',
        action: {
          type: 'message',
          label: 'Message',
          text: 'Message text'
        }
      },
      {
        title: 'Button postback title',
        subtitle: 'Button postback subtitle',
        image: `${process.env.IMAGE_FILE_HOST}/images/lw.png`,
        //resourceId: '',
        action: {
          type: 'message',
          label: 'Button',
          text: 'button_template ください',
          postback: 'button_template'
        }
      },
      {
        title: 'List postback title',
        subtitle: 'List postback subtitle',
        image: `${process.env.IMAGE_FILE_HOST}/images/lw.png`,
        //resourceId: '',
        action: {
          type: 'message',
          label: 'List',
          text: 'list_template ください',
          postback: 'list_template'
        }
      },
      {
        title: 'List uri title',
        subtitle: 'List uri subtitle',
        image: `${process.env.IMAGE_FILE_HOST}/images/security.png`,
        //resourceId: '',
        action: {
          type: 'uri',
          label: 'LINE WORKS',
          uri: 'https://line.worksmobile.com/jp/'
        }
      }
    ];
  }

  /**
   * Listアクションリストを返します。
   * @return {Array} アクションリスト
   */
  _getListActions() {
    // list_template.actionsのactionでは typeはmessageとuriしか使えない。つまり postback、camera、cameraRoll、locationは使えない
    return [
      [
        {
          type: 'message',
          label: 'Carousel postback',
          text: 'carousel ください',
          postback: 'carousel'
        },
        {
          type: 'message',
          label: 'Image Car postback',
          text: 'image_carousel ください',
          postback: 'image_carousel'
        }
      ],
      [
        {
          type: 'message',
          label: 'QuickReply',
          text: 'QuickReply ください',
          postback: 'q'
        },
        {
          type: 'message',
          label: 'No',
          text: 'No'
        }
      ]
    ];
  }

  /**
   * Carousel コンテンツを返します。
   * @param {Array} conditions 条件
   * @return {object} コンテンツ
   */
  _getCarouselContent(...conditions) {
    if (!conditions.some(condition => condition && (condition.toUpperCase() === 'C' || condition === MESSAGE_CONTENT_TYPE.carousel))) return;
    return { 
      type: MESSAGE_CONTENT_TYPE.carousel,
      //imageAspectRatio: '',
      //imageSize: '',
      columns: this._getCarouselColumns(),
      //quickReply: this._getQuickReplyItems()
    };
  }

  /**
   * Carousel カラムリストを返します。
   * @return {Array} カラムリスト
   */
  _getCarouselColumns() {
    // carousel.columnsのactionでは typeはmessageとuri、postbackしか使えない。つまり camera、cameraRoll、locationは使えない
    // carouselは、postbackをつかえる!!!!
    return [
      {
        thumbnailImageUrl: `${process.env.IMAGE_FILE_HOST}/images/giraffe01.png`,
        //thumbnailImageResourceId: '',
        title: 'Carousel postback title',
        text: 'Carousel postback text (default button)',
        defaultAction: {
          type: 'postback',
          label: 'ButtonTemp',
          data: 'button_template',
          displayText: 'button_template ください'
        },
        actions: [
          {
            type: 'postback',
            label: 'ListTemp',
            data: 'list_template',
            displayText: 'list_template ください'
          },
          {
            type: 'postback',
            label: 'Carousel',
            data: 'carousel',
            displayText: 'carousel ください'
          },
          {
            type: 'postback',
            label: 'QuickReply',
            data: 'q',
            displayText: 'QuickReply ください'
          }
        ]
      },
      {
        thumbnailImageUrl: `${process.env.IMAGE_FILE_HOST}/images/panda01.png`,
        //thumbnailImageResourceId: '',
        title: 'Carousel uri title',
        text: 'Carousel uri text',
        defaultAction: {
          type: 'uri',
          label: 'LINE WORKS',
          uri: 'https://line.worksmobile.com/jp/'
        },
        actions: [
          {
            type: 'uri',
            label: 'LINE WORKS',
            uri: 'https://line.worksmobile.com/jp/'
          },
          {
            type: 'uri',
            label: 'bot Action Objects',
            uri: 'https://developers.worksmobile.com/jp/document/1005050?lang=ja'
          }
        ]
      },
      {
        thumbnailImageUrl: `${process.env.IMAGE_FILE_HOST}/images/sushi.png`,
        //thumbnailImageResourceId: '',
        title: 'Carousel message title',
        text: 'Carousel message text',
        defaultAction: {
          type: 'message',
          label: 'Message',
          text: 'Message text'
        },
        actions: [
          {
            type: 'message',
            label: 'Yes',
            text: 'Yes'
          },
          {
            type: 'message',
            label: 'No',
            text: 'No'
          }
        ]
      }
    ];
  }

  /**
   * Image carousel コンテンツを返します。
   * @param {Array} conditions 条件
   * @return {object} コンテンツ
   */
  _getImageCarouselContent(...conditions) {
    if (!conditions.some(condition => condition && (condition.toUpperCase() === 'I' || condition === MESSAGE_CONTENT_TYPE.imageCarousel))) return;
    return { 
      type: MESSAGE_CONTENT_TYPE.imageCarousel,
      columns: this._getImageCarouselColumns(),
      //quickReply: this._getQuickReplyItems()
    };
  }

  /**
   * Image carousel カラムリストを返します。
   * @return {Array} カラムリスト
   */
  _getImageCarouselColumns() {
    // image_carousel.columnsのactionでは typeはmessageとuri、postbackしか使えない。つまり camera、cameraRoll、locationは使えない
    // image_carousellは、postbackを使える!!!!
    // 最大3つまでのカラムしか使えない!!!
    return [
      {
        imageUrl: `${process.env.IMAGE_FILE_HOST}/images/giraffe01.png`,
        //imageResourceId: '',
        action: {
          type: 'postback',
          label: 'ButtonTemp',
          data: 'button_template',
          displayText: 'button_template ください'
        }
      },
      {
        imageUrl: `${process.env.IMAGE_FILE_HOST}/images/lw.png`,
        //imageResourceId: '',
        action: {
          type: 'postback',
          label: 'ListTemp',
          data: 'list_template',
          displayText: 'list_template ください'
        }
      },
      {
        imageUrl: `${process.env.IMAGE_FILE_HOST}/images/sushi.png`,
        //imageResourceId: '',
        action: {
          type: 'postback',
          label: 'Carousel',
          data: 'carousel',
          displayText: 'carousel ください'
        }
      }
    ];
  }
}

環境変数 (.env)

  1. .env.sample ファイルを .env に変更する
  2. 「LINE WORKS Bot APIの利用準備」で発行した接続情報を設定する
  3. 「IMAGE_FILE_HOST」に ngrog で取得したホスト ( https://xxxxx.io ) を指定する
.env
API_ID="API ID"
CONSUMER_KEY="Consumer key"
SERVER_ID="Server ID"
PRIVATE_KEY="認証キー"
BOT_NO="Bot No"
IMAGE_FILE_HOST="ホスト名(ngrogで取得したホスト https://xxxxx.io)"

4. 動かしてみる

LINE WORKS Bot APIをひと通り触ってみる(node.js)#1 」の 「いざデバッグ開始!」を参照して、Botを起動し、LINE WORKSアプリにBotを追加した状態にする。

シナリオ

  1. Botの利用開始
  2. 「I」を入力し送信(画像カルーセル要求)
  3. 画像カルーセルの「ButtonTemp」をクリック(ボタンテンプレート要求)
  4. ボタンテンプレートの「List postback」をクリック(リストテンプレート要求)
  5. リストテンプレートの「QuickReply」をクリック(クイックリプライ要求)
  6. クイックリプライの「場所」をクリック(マップ要求)
  7. マップの「位置を共有」をクリック
  8. クイックリプライの「Calousel」をクリック(カルーセル要求)
aaa.gif

5. 気づいたこと

camera、cameraRoll、location、postback Action は、すべての 送信タイプで指定できるわけではない

送信タイプ camera cameraRoll location postback
quick reply (共通プロパティ) :ok_woman_tone1: :ok_woman_tone1: :ok_woman_tone1: :ok_woman_tone1:
text
image
link
sticker
button_template :no_good_tone1: :no_good_tone1: :no_good_tone1: :no_good_tone1:
list_template :no_good_tone1: :no_good_tone1: :no_good_tone1: :no_good_tone1:
carousel :no_good_tone1: :no_good_tone1: :no_good_tone1: :ok_woman_tone1:
image_carousel :no_good_tone1: :no_good_tone1: :no_good_tone1: :ok_woman_tone1:

※ ー は Action Object を使用できない送信タイプです

postback には2つの種類がある

  • 送信タイプ: button_template、list_template の Action Object の場合、type:message で postback プロパティにデータを乗せる
ActionObject
{
  type: 'message',
  label: 'Button',
  text: 'button_template ください',
  postback: 'button_template'
}
  • 送信タイプ: quick reply(共通プロパティ)、carousel、image_carousel の Action Object の場合、type:postback で data プロパティにデータを乗せる

(この Action が実行された場合、postback と message の2つのイベントが Bot 側にコールバックされる)

ActionObject
{
  type: 'postback',
  label: 'ImageCarousel',
  data: 'image_carousel',
  displayText: 'image_carousel ください'
}

6. まとめ

LINE WORKS Bot APIのメッセージ送信部分の動作をひと通り確認できました。
ベタ書きのコードですいません。でも、一通りは網羅したつもりです。
今回作成たコードは GitHub の line-works-bot01-node の tag:v2.0 で公開してま〜す。(issueがあればお知らせください。修正します。)

次回は、トーク固定メニューとリッチメニューをやってみたい!

Link

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

obniz-nobleでnoble使ってるプロジェクトをreplaceしてみる

サマリ

遠隔地のBLEデバイスをnobleで動かせるよ with obniz-noble

背景

最近BLE周りしか触ってないじゃないか!?っていうぐらいBLEのことばっかやってますが、先日、obniz-nobleなるものをリリースしました

どういうものかというと、nobleでobnizを操作しようよ!っていうものです。

node.jsでBLEといえばnoble一択、というほど強いんですが、このnoble、なんとnodejsのバージョン8でしか動きません。

正確にはnobleはバージョン依存していないのですが、nobleが使ってるライブラリがバージョン依存で動きません。nobleのgithubも2年以上放置されてますし、メンテナンスする予定ないんですかね・・・

いろいろなかたがこれに対応してnode12でも動くnobleとかを作っていたりシますが、ここはやっぱりobnizでしょ!ということでobnizで動くnobleを作ったのがobniz-nobleです

構成

会社とか、外部においたPC(もしくはクラウド)でnobleを動かし、家においたobnizを通じてBLEデバイスをコントロールします

image.png

nobleのリプレイス方法

いろんなプロジェクトやライブラリでnobleが使われているので、それをobniz-nobleにリプレイスしてみます。

通常のリプレイス方法

READMEに書いてありますがこの1行を2行に変えるだけです。

 //before 
 cosnt noble = require("noble");


 //after
 const obnizNoble = require("obniz-noble")
 cosnt noble = obnizNoble("OBNIZ_ID_HERE");

・・・でもちょっとまってください、直接noble使うときは自分のコード内にcosnt noble = require("noble");があるからいいですが、他人のライブラリの中にあるときはどうしましょう??

そんなときはyarnの選択的な依存関係の解決をつかいます。

yarnの選択的な依存関係の解決

npmを使ってる人が多いと思いますが、その上位互換のyarnというものがあり、
いろんな便利機能が実装されています。

そのうちの1つがyarnの選択的な依存関係の解決なのですが、この機能ちょっとずるくて、npm installで入るパッケージが使用しているライブラリをひっそりと入れ替えることができます。

ちょうどnode-linkingというのがnoble使っていたのですが、こんなふうに入れ替えることができます

image.png

node-linkingはnobleを使ってるつもりなのに、いつの間にかobniz-nobleをつかっていた!?ということができるわけです。

単純なnobleのリプレースだとobnizIdを入れるところがないので、obniz-nobleをラップしたnoble-replaceフォルダを作ってreplaceします.

ProjectRoot
├─┬ main@1.0.0
│ └─┬ node-linking@0.4.0
│   └── @abandonware/noble@>=1.9.2-5      // <- ここはnoble-replaceで置換される
│
└─┬ noble-replace@1.0.0
  └── obniz-noble@2.0.0

具体的なフォルダ構成はこちらのgithubにおいていますが、特徴的なところはmain/package.jsonです。

main/package.json
{
  .
  .
  .
  "dependencies": {
    "node-linking": "^0.4.0"
  },
  "resolutions": {
    "node-linking/@abandonware/noble": "file:../noble-replace"
  },

}

このように書くことでnode-linkingが使ってる@abandonware/nobleのかわりにnoble-replaceを読み込みます。

noble-replaceの中にはindex.jsで

noble-replace/index.js
const obnizNoble = require("obniz-noble");
module.exports = obnizNoble("86014802");

この2行だけのファイルを置いています。

動かしてみた

node-linkingのREADMEに書いてあるプログラムをデバイス名だけ変更して実行してみます。

// node-inking をロードし、`Linking` コンストラクタオブジェクトを取得
const Linking = require('node-linking');
// `Linking` オブジェクトを生成
const linking = new Linking();

// `LinkingDevice` オブジェクト
let device = null;

// `LinkingDevice` オブジェクトを初期化
linking.init().then(() => {
  // 名前が `Tukeru` で始まるデバイスを 5 秒間発見を試みる
  return linking.discover({
    duration: 5000,
    nameFilter: 'Sizuku'
  });
}).then((device_list) => {
  if(device_list.length > 0) {
    // 発見したデバイスを表す `LinkingDevice` オブジェクト
    device = device_list[0];
    // デバイス名
    let name = device.advertisement.localName;
    console.log('`' + name + '` was found.');
    // デバイスに接続
    console.log('Connecting to `' + name + '`...');
    return device.connect();
  } else {
    throw new Error('No device was found.');
  }
}).then(() => {
  console.log('Connected.');
  console.log('This device suports:');
  for(let service_name in device.services) {
    if(device.services[service_name]) {
      console.log('- ' + service_name);
    }
  }
  // デバイスを切断
  console.log('Disconnecting...');
  return device.disconnect();
}).then(() => {
  console.log('Disconnected');
}).catch((error) => {
  console.log('[ERROR] ' + error.message);
  console.error(error);
});

実行したら無事うごきました!
BLEデバイスは手元じゃなくてもobnizの近くにあれば発見できます

結果

`Sizuku_tha0142155` was found.
Connecting to `Sizuku_tha0142155`...
Connected.
This device suports:
- deviceName
- led
- battery
- temperature
- humidity
Disconnecting...
Disconnected

IMG_7045.JPG

↑これがSizuku_tha0142155です

ライブラリの中で使われてるライブラリをひっそり入れ替える事ができるのはすごいですね!

まとめ

obnizのこと書いてるのかnobleのこと書いてるのかyarnのこと書いてるのかよくわからなくなりますが、obnizでもnobleつかえるよ!って話でした

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Hubotを使ったslack botの作り方【2020年版】

Hubotとは

HubotとはGithub社が開発したNode.js上で動くBotアプリケーションを作成するためのフレームワークです。

Hubotは、Hubot本体アダプター拡張スクリプトから構成されます。
Hubot本体とチャットアプリの間を仲介するアダプターが存在するため、slack、chatworkなどの様々なチャットツールとHubotを連携することが可能です。
Bot作成フレームワークはHubot以外にも存在しますが、Hubotは様々なチャットツールと連携できる点が他のものと比べて大きな特徴です。

TH800_001.png
GitHub社謹製! bot開発・実行フレームワーク「Hubot」より引用

Hubotを使うことにより、複雑な振る舞いをするBotを作成することが可能です。
slackの既存機能だけでもslack上で"hello"と発言すれば、Botが"hi"と返すような単純な振る舞いであれば可能ですが、それ以上複雑になると対応できません。

Hubotを使えば、
・決められた時刻に発言
・あるサイトから内容をスクレイピングで取得してチャットツール上に表示
・ある発言をするとapiを経由してGoogleカレンダーに予定を登録
が可能になります。

やること

  1. Hubotアプリケーションの作成
  2. Hubot Integrationの設定してHubotとslackを連携
  3. Heroku上でHubotアプリケーションを動かす

1.Hubotアプリケーションの作成

Hubotアプリケーションを新規作成

node.jsとnpmが入っている確認

node -v
npm -v

yoemanとhubotジェネレータをインストールします。
yoとはyoeman(ヨーマン)と呼ばれるクライアント側のwebアプリケーション作成ツールです。
yoemanを使ってhubotアプリケーションを作成します。

npm install -g yo generator-hubot
mkdir myhubot
cd myhubot
yo hubot

yo hubotを実行すると以下のものがターミナル上に表示されます。
スクリーンショット 2019-12-24 12.54.21.png

4つ質問がされるので以下の通り答えていきます。

Owner→Enter
Bot name→Enter
Description→Enter
Bot adapter→今回はslackと連携したbotを作るので「slack」と入力

これでHubotアプリケーションの雛形が作成できました。

注意
別の記事ではhubot --create myhubotを使ったhubotアプリケーションの作成を紹介しているが、実際にコマンドを実行すると以下のエラーが発生する。
'hubot --create'は非推奨になったみたい。
指示通りにyoemanを使ってhubotを作成する。

'hubot --create' is deprecated. Use the yeoman generator instead:
npm install -g yo generator-hubot
mkdir -p myhubot
cd myhubot
yo hubot
See https://github.com/github/hubot/blob/master/docs/index.md for more details on getting started.

Hubotをローカルで起動

Hubotをローカルで起動して、動作確認をしていきます。

pwd #mybotディレクトリにいることを確認
bin/hubot

bin/hubotはmyhubotディレクトリ内にあるシェルスクリプトhubotを実行しています。
bin/hubotでは、npm installpackage.jsonに記述されたパッケージをインストールし、その後Hubot本体を起動しています。

hubot起動時に使用するアダプターを明示していないと、対話モードでHubotが起動します。
スクリーンショット 2019-12-24 13.06.04.png

まだ独自のスクリプトを作成していませんが、Hubotではデフォルトでスクリプトが登録されています。
mybubot echo "ping"と打って、"ping"と返ってくることを確認しましょう。

mybubot> mybubot echo "ping"
mybubot> "ping"

Hubotの独自スクリプト作成

helloと打つとhiを返してくる独自のBotスクリプトを作成してみましょう。

scripts/hello.coffee
module.exports = (robot) ->
  robot.hear(/hello/i, (res) ->
    res.reply("hi")
  )

Robotクラスのhearメソッドは、hear(マッチさせたい正規表現, コールバック関数)
Responseクラスのreplyメソッドは、reply(発言内容)

robot.hear(/hello/i, (res)で"hello"という文字列が渡ってくると、第2引数のコールバック関数を実行します。res.reply("hi")で"hi"という文字列をチャットツール側で発言するようにしています。

2.Hubot Integrationの設定してHubotとslackを連携

次はローカルで動くHubotとslackを連携して、slack上にbotを導入してみましょう。

Hubot Integrationの設定

slackのアプリ連携からHubotを追加します。
スクリーンショット 2019-12-24 13.21.28.png

slackのアプリディレクトリからHubotを選択して、HubotのBotユーザを作成します。
ここで入力されたユーザ名が、slack上で発言するBotのユーザ名になります。
スクリーンショット 2019-12-23 12.28.41.png

HubotアダプターのAPIトークンが表示されるので、メモしておきます。

Hubot___Slack_App_ディレクトリ.jpg

アダプターを指定してHubotを起動

slackと連携したHubotをローカル上に起動するために、以下コマンドでHubotを起動します。

HUBOT_SLACK_TOKEN=上記でメモしたHubotアダプターのAPIトークン ./bin/hubot --adapter slack

slack上でhelloと発言するとBotがhiと返してくれるようになりました。

スクリーンショット 2019-12-24 13.31.00.png

ちなみに、slackチャンネル上でBotを動かすには、チャンネルにBotを/invite @ユーザ名で招待しておく必要があります。

3.Heroku上でHubotアプリケーションを動かす

今まではローカル上でHubotを起動していました。
しかしこれだと、常にローカルPCを起動させておく必要がありますし、もしローカルのPCを閉じるとBotが動かなくなります。
そこでHeroku上でHubotを動かしておくことにします。

Herokuに登録

Herokuの登録自体は多くの記事がありますので、ここでは説明を省略します。
以下などの記事を参考に3まで完了させてください。
【Rails】Herokuの登録&初期設定からデプロイ方法までまとめ

1.Herokuのアカウント登録
2.Toolbeltインストール
3.heroku login

Herokuに環境変数設定&Push

Herokuに環境変数HUBOT_SLACK_TOKENを設定します。

heroku config:set HUBOT_SLACK_TOKEN=hoge

Hubotアプリケーションのcommit

git init
git add .
git commit -m "first commit"
git push heroku master  #herokuにmasterブランチをpushする

するとBuild failedエラーが発生。

% git push heroku master                                                          
Total 0 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:        NODE_VERBOSE=false
remote:
remote: -----> Installing binaries
remote:        engines.node (package.json):  0.10.x
remote:        engines.npm (package.json):   unspecified (use default)
remote:
remote:        Resolving node version 0.10.x...
remote:        Downloading and installing node 0.10.48...
remote:        Detected package-lock.json: defaulting npm to version 5.x.x
remote:        Bootstrapping npm 5.x.x (replacing 2.15.1)...
remote:        npm 5.x.x installed
remote:
remote: /tmp/build_9e550049100749e72c047c136d3131ae/.heroku/node/lib/node_modules/npm/bin/npm-cli.js:79
remote:       var notifier = require('update-notifier')({pkg})
remote:                                                     ^
remote: SyntaxError: Unexpected token }
remote:     at Module._compile (module.js:439:25)
remote:     at Object.Module._extensions..js (module.js:474:10)
remote:     at Module.load (module.js:356:32)
remote:     at Function.Module._load (module.js:312:12)
remote:     at Function.Module.runMain (module.js:497:10)
remote:     at startup (node.js:119:16)
remote:     at node.js:945:3
remote:
remote: /tmp/build_9e550049100749e72c047c136d3131ae/.heroku/node/lib/node_modules/npm/bin/npm-cli.js:79
remote:       var notifier = require('update-notifier')({pkg})
remote:                                                     ^
remote: SyntaxError: Unexpected token }
remote:     at Module._compile (module.js:439:25)
remote:     at Object.Module._extensions..js (module.js:474:10)
remote:     at Module.load (module.js:356:32)
remote:     at Function.Module._load (module.js:312:12)
remote:     at Function.Module.runMain (module.js:497:10)
remote:     at startup (node.js:119:16)
remote:     at node.js:945:3
remote:
remote: /tmp/build_9e550049100749e72c047c136d3131ae/.heroku/node/lib/node_modules/npm/bin/npm-cli.js:79
remote:       var notifier = require('update-notifier')({pkg})
remote:                                                     ^
remote: SyntaxError: Unexpected token }
remote:     at Module._compile (module.js:439:25)
remote:     at Object.Module._extensions..js (module.js:474:10)
remote:     at Module.load (module.js:356:32)
remote:     at Function.Module._load (module.js:312:12)
remote:     at Function.Module.runMain (module.js:497:10)
remote:     at startup (node.js:119:16)
remote:     at node.js:945:3
remote:
remote: -----> Build failed
remote:
remote:        We're sorry this build is failing! You can troubleshoot common issues here:
remote:        https://devcenter.heroku.com/articles/troubleshooting-node-deploys
remote:
remote:        If you're stuck, please submit a ticket so we can help:
remote:        https://help.heroku.com/
remote:
remote:        Love,
remote:        Heroku
remote:
remote:  !     Push rejected, failed to compile Node.js app.
remote:
remote:  !     Push failed
remote: Verifying deploy...
remote:
remote: !   Push rejected to rocky-gorge-05187.
remote:
To https://git.heroku.com/rocky-gorge-05187.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/rocky-gorge-05187.git'

You can troubleshoot common issues here: https://devcenter.heroku.com/articles/troubleshooting-node-deploysこのURL見れば同じ問題を解決できるよーと言われているので、見てみる。

まずはbuildpackを設定して再実行。build failedは解消できず。

heroku buildpacks:set heroku/nodejs

次に章のCompare Node and npm Versionsでは開発環境と本番環境でNodeとnpmのversionを揃えなさいと言われているので、まずはローカルでnodeとnpmのバージョンを見る。

node --version
npm --version

表示されたバージョンをpackage.jsonに設定。
nodeとnpmのバージョンを指定。

package.json
  "engines": {
    "node": "10.16.0",
    "npm": "6.9.0"
  }

再度、git push heroku masterでbuild成功。

参考文献

GitHub社謹製! bot開発・実行フレームワーク「Hubot」
https://gihyo.jp/dev/serial/01/hubot/0001

Hubotを使ってSlackへBotを投げる
https://qiita.com/shosho/items/057d7b67d1dd3a700554

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

expressメモ

サニタイズするとは

HTMLフォームなどから悪意のあるスクリプトコードが入力されたとしても単なる文字列として扱うようにして無力化してしまうこと。

mongooseでdb接続

connect成功と失敗でlogを出力させた。

// Set up Default mongoose connection
var mongoDB = require('./.db_url');
var DB_URL = mongoDB.LOCAL_DB;
// connect mongoDB
mongoose
  .connect(DB_URL, {
    useUnifiedTopology: true,
    useNewUrlParser: true,
  })
  .then(() => console.log('DB Connected!'))
  .catch(err => {
    console.log(`DB Connection Error:${err.message}`);
  });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ヒーローズ・リーグ 駅すぱあと賞受賞】みんなの現在地から集合場所を決めてくれる機能、他。飲み会幹事おたすけ LINE BOT 「鯨飲くん」を作った

はじめに

飲み会幹事の仕事って、なかなか大変ですよね。
みんな飲み会はやりたいけど、幹事はやりたくない。そんな幹事の負担を少しでも軽減してくれるサービスがあったら、もっと気軽に飲み会を開催できるようになって、みんなの繋がりをより広く、深くできるのではないか…
そんな思いを形にした LINE Bot「鯨飲くん」を作りました。

ProtoPedia はこちら。

https://protopedia.net/node/1899

この作品はヒーローズ・リーグ 2019 のハッカソン予選で制作したものです。ありがたいことに、「駅すぱあと賞」を頂くことができました。
image.png

2日間のハッカソンを通しての制作プロセスや、制作時にやってよかったこと、反省点などを後半でご紹介いたします。

作ったもの

LINE のグループチャットに参加させて利用する LINE Bot です。

  1. みんなの現在地からだいたいの中間地点になる集合駅を提案
  2. 集合駅付近の居酒屋をピックアップ、投票を募る
  3. 一番票の多かった居酒屋に自動音声で電話して予約

以上をワンストップで行います。

1. みんなの現在地を教えてもらう

ユーザの「集合」のかけ声で鯨飲くんが起動します。
参加希望者は鯨飲くんに自分の位置情報を教えます。
鯨飲くんは位置情報を受け取るたび、最寄り駅を探して返答します。
image.png

2. 集合駅を決める

ユーザの「集合場所」のかけ声で参加希望を締め切ります。
鯨飲くんは全員の最寄り駅からの移動時間の合計が最小になる駅を教えてくれます。
image.png

3. 店を提案し、投票を募る

鯨飲くんは集合駅周辺の居酒屋をピックアップし、カルーセルで表示します。
参加者は「ここがいい!」を押して投票することができます。
カルーセル.gif

4. 店を決める

投票の結果、一番票が多かった店に決定します。
image.png

5. 店を予約する

ユーザが「予約しといて!」を押すと、鯨飲くんは予約する時間を尋ねます。
ユーザが時間を回答すると、時間を答えた人の名前で電話予約をはじめてくれます。
image.png

店にはこのような電話がかかってきます。

6. 予約完了を通知する

予約が完了すると、鯨飲くんが予約の成否を通知してくれます。
image.png

あとはみんなで集まって飲むだけですね!

仕組み

構成図

image.png

使ったもの

  • LINE Messaging API
    • いわゆる LINE Bot
    • UI 開発なしで、LINE の仕組みを利用してユーザとやりとりする仕組みを手早く構築可能
  • Express.js
    • LINE Messaging API のバックエンド
    • SDK があるため開発が容易
  • 駅すぱあと Web Service
    • ヒーローズ・リーグのパートナー企業であるヴァル研究所提供
    • 緯度経度からの周辺駅検索
      • 位置情報から最寄り駅を検索するのに利用
    • 範囲探索
      • 各参加者の最寄り駅から集合駅を決定するのに利用
  • Twilio
    • ヒーローズ・リーグのパートナー企業である KDDI 提供
    • 「電話をかける」「自動音声を流す」「番号入力を受け取る」などの様々なアクティビティから構成されるフローを GUI で構築可能
      • image.png
  • ぐるなび Web Service

当日の開発の流れ

アイデアソン

ヒーローズ・リーグの説明、パートナー企業提供サービスの紹介などが済んだあと、アイデアソンが行われました。
各参加者が個別にアイデアスケッチを出し、参加者同士で見せあって、自分のアイデアの実現に協力してくれるチームメンバーを集めます。
ありがたいことに私が出したアイデアに2人が協力を申し出てくれたため、3人でチームを組むことになりました。

  • アイデアスケッチ(再現・当時のものを紛失したため) image.png

開発

あとはひたすら開発です。
以下のようにタスクを分解し、チームメンバーで分担して進めました。

  • LINE Bot のバックエンド開発
  • 各種 Web API を呼び出すモジュールの開発
  • Twilio の電話応答フロー構築
  • ブランディング (鯨飲くんというマスコットの考案)
  • 発表資料の作成

開発を進め、動くものができあがっていく中で、こうするともっといいんじゃない?というアイデアはどんどん出てきます。
そうしたアイデアも時間が許す限り取り込んでいきました。
最初は集合駅を決めるというシンプルなアイデアだったものが、最終的には電話予約まで取ってくれるものに膨らんでいきました。

振り返って

やってよかったこと

デモに必要ないタスクは捨てる

ハッカソンは、デモで印象づけることが命です。
実際のプロダクト開発では、エラー系や異常系、セキュリティなどを考慮して開発を行います。
しかし、デモで見せるのは正常系だけです。限られた時間の中でユーザーにとって魅力的な機能を可能な限り多く実装することを最優先にしました。

具体的には以下のような割り切りをしました。

  • エラー系、異常系は一切実装しない。ユーザーは常に正しい入力をする前提を置く
  • 各種シークレットはソースコードにベタ書き。GitHub のプライベートリポジトリに直コミットして共有
  • 駅すぱあと Web Service 側の制約で、6人以上が参加する場合は集合駅を出せない。でもデモは3人でやるから?ヨシ!

各メンバーの開発成果物を npm モジュールとして分離する

複数人でソースコードを共有して開発すると、他のメンバーの開発がボトルネックになって開発が進まない、ということが往々にして起きます。
これを防止するために、メンバーごとの開発成果物を npm モジュールとして分離し、LINE Bot のバックエンドから require() して利用してもらいました。

具体的には以下のように開発を進めました。
私が、駅すぱあと Web Service を呼び出して位置情報から最寄り駅を取得する機能を開発していたときのケースです。

  1. 駅すぱあと Web Service を呼び出す npm モジュールに findNearestStation という function を作成する
    • この function は、緯度経度を受け取って最寄り駅を返すものだが、実際に Web API を呼び出す処理の実装は後回しにし、常に「新宿駅」を返すよう実装した状態でコミットする
  2. 他のメンバーには、常に「新宿駅」を返す実装のまま、この function を利用して開発を進めてもらう
  3. 正しく最寄り駅を返すよう実装が終わったら再度コミットする
  4. 他のメンバーは最新のコードを pull するだけで、自らのソースコードに一切変更を加えることなく正しい最寄り駅を受け取ることができる

また、モジュールの使い方を説明するときには、実際にモジュールを利用するサンプルコードを用意して渡すと非常にスムーズでした。
以下は実際に利用したサンプルコードです。
駅すぱあと Web Service を利用してメンバー3人の最寄り駅を取得し、集合駅を決めるところまでを実行しています。

ekispertUsage.js
const ekispert = require("ekispertjs");

const nomikai = new ekispert.Nomikai();

const main = async () => {
    // 横浜
    const station1 = await ekispert.findNearestStation("35.450199", "139.627466");
    console.log(station1.name);
    nomikai.currentStations.push(station1.code);
    // 立川
    const station2 = await ekispert.findNearestStation("35.700230", "139.413544");
    console.log(station2.name);
    nomikai.currentStations.push(station2.code);
    // 大宮
    const station3 = await ekispert.findNearestStation("35.907608", "139.631058");
    console.log(station3.name);
    nomikai.currentStations.push(station3.code);

    // 集合場所を探す
    const meetPlace = await nomikai.findPlace();
    console.log(meetPlace.name);
    console.log(meetPlace.geoPoint);
    // 各自の所要時間を位置情報を送った順に並べたarray
    console.log(meetPlace.costs);

}

main();

ブランディング

今回作ったものが「飲み会幹事お助けBot」という名前だったらどうでしょうか。
確かにわかりやすい名前ですが、印象に残らないし、きっとすぐに忘れられてしまいます。

「鯨飲くん」というマスコットを作成し、また特徴的な口調でキャラクター付けをすることで、一気にキャッチーさが増し、すぐに覚えてもらえるようになりました。
ハッカソンに限らず、実際のプロダクト開発でも非常に大切なことです。

このブランディング作業は、私が何も言っていないにも関わらず、メンバーが率先してやってくれました。
いいチームメンバーに恵まれたことに感謝しています。

反省点

プレゼンの練習をする余裕がなかった

可能な限り多くのアイデアを詰め込むことに前のめりになった結果、成果発表プレゼンの練習をする時間が取れませんでした。結果、プレゼンが少々ぐだりました。
どんなプロダクトでも、プロダクト自体の品質と同じくらい、「どう見せるか」が重要になります。
プロダクトの技術力そのものが「すごい」と言えるほどではなくても、見せ方が非常にうまくて賞をかっさらっていく作品はハッカソンでは珍しくありません。
成果発表プレゼンは少なくとも1回以上練習しておくことをおすすめします。

タッチ & トライの準備不足

デモでは、やはりハードウェアとして動く部分があるものが強いです。
今回のハッカソンでも、プロジェクションマッピングを利用した作品が最も多くの票を集めていました。

一方で、鯨飲くんはソフトウェアで完結しているため、デモのインパクトは弱いです。
しかし、実際にタッチ & トライで触ってもらえればその魅力は伝わります。実際に触った人たちからは口々に「これはすごいね」という評価を頂けました。

ところが、鯨飲くんにはタッチ & トライに至るまでの準備に時間がかかるという欠点があります。
鯨飲くんと友だちになってもらった上で、鯨飲くんのいるLINEグループに参加してもらう必要があるためです。
ここに手間取った結果、会場にいるうちの半分程度の人にしか試してもらうことができませんでした。

タッチ & トライに至るまでの時間で、参加者に声をかけて事前にグループに入ってもらえていたら、もっと多くの人に試してもらうことができたでしょう。
これは鯨飲くんに限った話ではなく、開発途中にでも作品を見てもらうよう周囲の人に積極的に声をかけることはとても効果的です。
早い段階でフィードバックを得て、ブラッシュアップをかけることができるからです。

感想

このハッカソンの中では、自分の中では満足の行くものができつつも、「反省点」に記したように見せ方に失敗し、総合賞に届くことができませんでした。

しかし、これはあくまでも「ハッカソン予選」です。ヒーローズ・リーグでは全国の至るところで予選が開催され、最終的にヒーローズ・リーグ全体での賞が決まります。
その全体賞として、パートナー企業のヴァル研究所様に評価頂き、「駅すぱあと賞」を受賞することができました。
見ていてくれる人はいるんだな、と、報われたような気持ちがして、非常に嬉しかったです。

今後も、「反省点」に記したことを胸に留めつつ、ハッカソンに挑戦していきたいです。
本当にいい経験になりました。

後日談

LINE Bot でお店探しや予約代行をしてくれるぺこったーというサービスがあるそうです。
鯨飲くんの後半の機能と被っている…どころか、こちらのほうが遥かに便利そうですね。
予約の電話ってやはり腰が重いタスクなので、多くの人にとって非常にありがたいサービスではないでしょうか。
image.png

かわいいマスコットを作って、口調でキャラクター付けをしているあたり、まさにブランディングですね。

鯨飲くんでは自動音声での予約を行います。いつかそれが普通になる日が来るのかも知れないですが、現状それをやったらお店の人を驚かせてしまいますよね…。
鯨飲くんで集合駅を決めたあと、ぺこったーに予約をお願いできれば強そうです。
LINE Bot 同士の連携が難しいので、すぐに実装というわけには行きませんが。
LINE さんどうかよろしくお願いします。

Special Thanks

  • パートナー企業として駅すぱあと Web Service を提供してくださり、さらに駅すぱあと賞で鯨飲くんを救ってくれた、ヴァル研究所様
  • パートナー企業として Twilio を提供してくださった、KDDI 様
  • 私のアイデアを形にするために最善の協力をしてくれた、チームメンバーの2人
  • ハッカソンの最中に利用していた画像のぬいぐるみを販売している、ニトリ様
    • 勝手に使ってごめんなさい。宣伝しておきます!
  • ポップでかわいい鯨のイラストを提供してくださった、いらすとや様
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

obnizOS 3.0.0でobniz-nobleを使って周囲のBLEデバイスを探してみる

obnizOS 3.0.0がリリースされました。

新バージョンではBLE機能が強化されている印象を受けました。

手持ちのobnizBoardで新機能を試してみたい。

Node.jsでobniz-nobleを使ってをobnizで周辺にあるBLEデバイスを検索してみる

npm i obniz-noble
server.js
'use strict';
const express = require('express');
const PORT = process.env.PORT || 3000;
const app = express();

var obnizNoble = require('obniz-noble')
var noble = obnizNoble("OBNIZ_ID_HERE")

noble.startScanning(); // any service UUID, no duplicates

noble.on('discover', function(peripheral) {
    console.log('発見したデバイス: ' + peripheral.advertisement.localName);
    console.log('デバイスのUUID: ' + peripheral.advertisement.serviceUuids);
    console.log();

});

app.listen(PORT);
console.log(`Server running at ${PORT}`);

こんな感じに検索されてくる。
ちょうど良いところにmicrobitが上がってきた。
image.png

とりあえずここまで!
次はNode.jsでobnizとmicrobitを繋げるところの記事を書く予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む