hawk, camphora, avocado and so on..

美術展、博物展、神社、旅行、読書、IT関連などの雑感を書いていきます。

ERC-721 トークンを発行する DApp を作ってみました(その3)

アボカドです。前回、前々回と、捨てたものをブロックチェーンに記録するアプリの作り方を書いています。

ERC-721 トークンを発行する DApp を作ってみました(その1) - hawk, camphora, avocado and so on..

ERC-721 トークンを発行する DApp を作ってみました(その2) - hawk, camphora, avocado and so on..

前回は、おおまかな作り方と、Truffle インストール ~ ERC-721 トークンの実装まで説明しました。今回は、コントラクトの動作確認とデプロイ方法について説明します。

コントラクトの動作確認

Remix を使うと、コントラクトを擬似的にデプロイして関数の動作を確認できます。デバッグ機能もあるので、バグを潰すのに役立ちます。

右ペインの [Run] タブ > Environment で "Java VM" を選ぶと Ethereum のプライベートネットなどを用意しなくても動作確認できます。("Injected Web3" を選ぶと、インストール済みの MetaMask などを経由してブロックチェーンにデプロイ等ができるみたいです。プライベートネットを自分で立てている場合は "Web3 Provider" を選択します。)

f:id:takagusu:20180816212605p:plain

ただ、ひとつ課題なのが、Truffle で作ったフォルダ階層がある .sol ファイル群をそのまま読み込めません。すべてフラットになります。そのため、必要な .sol ファイルを読み込んで、import のパスを書き換えるか、または truffle-flattener などを使ってひとつのファイルに結合して読み込む必要があります。

Loom Network が開発している EthFiddle を使うのも良さそうですが、いまのところ複数のファイルには対応していないので、同様にファイルを結合する必要があります。

コントラクトのデプロイ

コマンドと準備

Truffle を使うと比較的簡単にコントラクトをデプロイできます。

まず下記のコマンドを実行して(Windows の場合は .cmd をつけてもつけなくても良いですが、truffle.js などのファイルが実行フォルダにあるときは、それを開いちゃわないように .cmd をつけます)、コントラクトをコンパイルします。コンパイル結果は build フォルダ内に格納されます。

> truffle.cmd compile

次に下記のコマンドを実行すると、migrations フォルダに含まれる js ファイルの処理を順番に実行します。デフォルトでは、コントラクトをデプロイするための処理が書かれた 1_initial_migration.js のみ実行します。

> truffle.cmd migrate

自分で書いたコントラクトをデプロイするには、この migrations フォルダに、「2_」 から始まる js ファイル(例えば「2_deploy_contracts.js」)を作成し、下記のようなコードを書きます。コントラクトの名前は適宜書き換えてください。コントラクトが複数ファイルに分かれている場合、最終的に継承しているコントラクトだけ書けば大丈夫です。

var DiscardedRelicToken = artifacts.require("./DiscardedRelicToken.sol");

module.exports = function(deployer) {
  deployer.deploy(DiscardedRelicToken);
};
プライベートネットへのデプロイ

truffle.js にはデプロイ先の情報を入力します。プライベートネットの場合は、下記のような感じです。上述の `truffle.cmd migrate` コマンドを実行するとデプロイできます。

  module.exports = {
    networks: {
      development: {
        host: "127.0.0.1",
        port: 8545,
        network_id: "*" // Match any network id
      }
    }
  }
テストネットやメインネットへのデプロイ

テストネットやメインネットにデプロイする場合は、自分でノードを立ててそのホストと IP を指定してもいいですが、Infura というサービスのノードを間借りするほうがお手軽です。

手順としては、まず Infura にアクセスしてアカウントを作り、プロジェクトを作成します。すると、API Key、API Secret、Endpoint の情報をもらえます。これらの情報と、トランザクションに署名するための自分の秘密鍵(MetaMask で取得できるニーモニック(12個の単語)から導出します)を使って、デプロイのためのプロバイダーを作ります。具体的には、truffle-hdwallet-provider を使います。まず下記コマンドでインストールします。

> npm install truffle-hdwallet-provider

そして truffle.js に、下記のように実装します。

const HDWalletProvider = require("truffle-hdwallet-provider");
const mnemonic = ""; // デプロイに使う MetaMask ウォレットのニーモニック(12個の単語)を設定する
const accessToken = ""; // Infura の API Key を設定する

module.exports = {
  networks: {
    ropsten: {
      provider: function() {
        return new HDWalletProvider(
          mnemonic,
          "https://ropsten.infura.io/V3/" + accessToken  // Infura の Endpoint を設定(テストネット、メインネットによって異なる)
        );
      },
      network_id: 3,  // テストネットまたはメインネットのネットワーク ID を設定(Ropsten テストネットの場合は 3)
      gas: 6000000
    }
  }
}

上記で定義したネットワーク設定を使ってデプロイする場合は、下記のコマンドを実行します。"ropsten" は上記 json 内で自分で決めた要素名です。

> truffle.cmd migrate --network ropsten

デプロイに成功すると、次のような出力結果が得られます。コントラクトごとに、デプロイした結果のアドレスが表示されています。

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xd65815f0565f8fb2ec3eeebf6ecde96e8fc58185802f9183461dca6863b9f2ae
  Migrations: 0x7e6b22144e7029ffb81493bae6889e9194384ea5
Saving successful migration to network...
  ... 0xf854f9f409006dcc8b449700ef6e72ebe537014cacba573acddab6c3b55ec59b
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying DiscardedRelicToken...
  ... 0xfdc626352f3ea671b34bd156b1e9f882179b48f2d732bffd230623cc42b887fa
  DiscardedRelicToken: 0xee994142aff67e8332e2ba52ecaefdda16b71b91
Saving successful migration to network...
  ... 0xd3d8c52fd4ee99ba468826a88ea979cc840e3f4b22501ad4854cf24291cbb9e6
Saving artifacts...

Truffle の設定ファイルの詳細は、公式ドキュメントを参照してください。

今回はここまでにしたいと思います。
次の記事では、デプロイしたコントラクトを操作する Web フロントエンドの実装方法を説明します。

🥑

ERC-721 トークンを発行する DApp を作ってみました(その2)

アボカドです。前回の記事では、捨てたものをブロックチェーンに記録するアプリの概要を紹介しました。
ERC-721 トークンを発行する DApp を作ってみました(その1) - hawk, camphora, avocado and so on..

今回は、このアプリの実際の作り方や、ハマった部分などを書いていきます。

おおまかな作り方

  • 事前準備
    • Node をインストール
    • MetaMask ウォレットをインストール
  • スマートコントラクトを作る
    • npm で Truffle をインストールして、任意のフォルダを init
    • npm で OpenZeppelin ライブラリをインストール
    • OpenZeppelin ライブラリの ERC721Token を継承して、独自の ERC-721 トークンのスマートコントラクトを実装
    • Remix を使って、実装したスマートコントラクトの動作確認
    • Infura.io にアカウントを作って、プロジェクトを作成し、API キーを取得
    • デプロイに使う MetaMask ウォレットのニーモニックを控える
    • Truffle のデプロイ先のプロバイダーを設定して(ここで Infura.io の API キー、エンドポイント、MetaMask ウォレットのニーモニックを使う)、テストネットにスマートコントラクトをデプロイ
  • Web フロントエンドを作る
    • npm で React をインストール
    • React のサンプル実装 create-react-app をベースにして、UI を作成
    • ipfs-api を使って、分散ストレージ IPFS にファイルや文字列を書き込む
    • Web3.js を使って、デプロイしたスマートコントラクトを操作
    • Ganache をインストールして Ethereum プライベートネットを立ち上げて、動作確認
    • Firebase でプロジェクトを作って、作った Web フロントエンドを公開

実際やったこと&ソースコード紹介

Windows 10 の環境を使いました。…が、Linux のほうがいろいろスムーズかもしれません。

Truffle のプロジェクトを作る

まず Node をインストールします(バージョン 8.11.3 LTS をインストールしました)。多くのライブラリが、パッケージ管理システム npm でインストールできるようになっています。

その後、Truffle をインストールして、任意のフォルダをひとつ作って初期化します。

> npm install -g truffle
truffle@4.1.13
> mkdir myTruffleProject
> cd  myTruffleProject
> truffle init

ERC-721 トークンを実装する

Solidity 言語の模範的な実装を提供している OpenZeppelin ライブラリをインストールします。この中に、ERC-721 トークンのベースとなるコードが含まれるので、今回はこれを継承して独自のトークンを作ります。ERC-721 は、固有のプロパティを持つ代替不可能トークン (Non-Fungible Token, NFT) の規格です。

> npm install zeppelin-solidity --save
zeppelin-solidity@1.11.0

まず、truffle プロジェクトの contracts フォルダ内に .sol ファイルを作成し、下記のように、必要なコントラクトを import & 継承してコントラクトを定義します。ERC721Token.sol は ERC-721 トークンの機能の多くを実装しているコード、ERC721Holder.sol は ERC-721 のコントラクト自身がトークンを受け取れるようにする機能を実装しているコード、Ownable.sol は管理者のみ操作できる処理を書きやすくするものです。なお、今回つくったコントラクトの名前は「DiscardedRelicToken」(破棄された遺産トークン)としました。コンストラクタで、トークンの名称と単位の名称を設定します。ちなみにこれらは他のトークンとかぶっても問題ないです(エラー等は出ません)。

pragma solidity ^0.4.24;

import "zeppelin-solidity/contracts/token/ERC721/ERC721Token.sol";
import "zeppelin-solidity/contracts/token/ERC721/ERC721Holder.sol";
import "zeppelin-solidity/contracts/ownership/Ownable.sol";

contract DiscardedRelicToken is ERC721Token, ERC721Holder, Ownable {
  // コンストラクタで、トークンの名称と単位名称を設定
  constructor() ERC721Token("DiscardedRelic", "DRLC") public {
  }

  // 必要な関数を実装する
}


今回、このトークンの持つ固有のプロパティとして、次の構造体を定義しました。imageIpfsHash は捨てたものの写真の IPFS ハッシュ値(分散ストレージ IPFS にファイルをアップロードしたときに得られる、ファイルの格納場所を示す値)、metaDataIpfsHash はそのメタデータの IPFS ハッシュ値、owner はトークンの所有者のアドレスです。それから、discardedRelics は、発行したすべてのトークンを格納する配列です。

  struct DiscardedRelic {
    string imageIpfsHash;
    string metaDataIpfsHash;
    address owner;
  }

  DiscardedRelic[] public discardedRelics;

今回作るのは mintable(鋳造可能な)トークンです。ユーザーはコントラクトに対して送金することで、自分が所有者のトークンを発行できます。これを実現するための、2 つの重要な関数 mint() と burn() を実装します。継承している OpenZeppelin のコントラクトには関数 _mint() と _burn() が書かれており、内部で所有者とトークン ID を渡してこれらの関数を実行することで、トークンの管理が楽になります。なお、burn() はトークンを削除する関数ですが、必要がなければ実装しなくてよいと思います。createFee についてはこの下で述べます。

  function mint(string _imageIpfsHash, string _metaDataIpfsHash) public payable {
    require(msg.value == createFee);
    require(owner != address(0));
    DiscardedRelic memory _discardedRelic = DiscardedRelic({imageIpfsHash: _imageIpfsHash, metaDataIpfsHash: _metaDataIpfsHash, owner: msg.sender});
    uint256 newTokenId = discardedRelics.push(_discardedRelic) - 1;
    _mint(msg.sender, newTokenId);
  }

  function burn(uint256 _tokenId) external onlyOwnerOf(_tokenId) {
    _burn(msg.sender, _tokenId);
  }

上で出てくる変数 createFee は、トークンを発行する際の手数料です(型は uint128)。今回は初期値を 0 ether として宣言しましたが、下記、コントラクトをデプロイしたユーザーのみが実行できる関数で、この手数料は変更できるようにしてあります。

  uint128 private createFee = 0 ether;

  function setCreateFee(uint128 _fee) public onlyOwner {
    createFee = _fee;
  }

  function getCreateFee() external view returns (uint128) {
    return createFee;
  }


そのほか、必要になりそうな関数を定義しました。このあたりは、実現したいアプリによって異なると思います。
ownedTokens や allTokens は、継承しているコントラクトで管理している変数であり、これらを使うことで実装量を減らせます。

  function getDiscardedRelic(uint256 _tokenId) public view returns (string, string, address) {
    return  (discardedRelics[_tokenId].imageIpfsHash, discardedRelics[_tokenId].metaDataIpfsHash, ownerOf(_tokenId));
  }

  function tokensOf(address _owner) external view returns (uint256[]) {
    return ownedTokens[_owner];
  }

  function getAllTokens() external view returns (uint256[]) {
      return allTokens;
  }

  function updateDiscardedRelic(uint256 _tokenId, string _metaDataIpfsHash) public {
    require(ownerOf(_tokenId) == msg.sender);
    discardedRelics[_tokenId].metaDataIpfsHash = _metaDataIpfsHash;
  }


今回はここまで。
次の記事では、「スマートコントラクトを作る」の続きを書きたいと思います。
動作確認と、テストネットへのデプロイについて説明する予定です。

🥑

ERC-721 トークンを発行する DApp を作ってみました(その1)

アボカドです。名前も変わって、すごく久しぶりの投稿です。
ここ一年くらいブロックチェーンに興味があり勉強しています。

今回、勉強のために DApp (Decentralized Application) を作ってみました。
この記事ではまず、どんなものを作ったかを紹介します。

使った技術

  • Solidity:Ethereum 上で動くプログラム(スマートコントラクト)を書く言語
  • OpenZeppelin:Solidity 言語による模範的な実装ライブラリ
  • ERC-721:固有のプロパティを持つ代替不可能なトークンの規格
  • Truffle:Solidity で開発するときにデプロイなど簡単にできる開発ツール
  • Remix:ブラウザでスマートコントラクトの動作確認を行えるツール
  • Ganache:テスト用に簡単に Ethereum プライベートネットを作れるツール
  • React:Web フロントエンドの JavaScript ライブラリ
  • Web3.jsJavaScript からスマートコントラクトを操作できるライブラリ
  • IPFS (js-ipfs-api):分散ストレージとその JavaScript ライブラリ
  • Firebase:静的 Web のホスティングをしてくれるサービス

作ったもの概要

こんなものができた

トップ画面はこんな感じ。Ropsten テストネットで動いています。表示するには MetaMask が必要です。

f:id:takagusu:20180813212508p:plain

トークンの発行画面。捨てるもの(捨てたもの)の写真、タイトル、いつ、どこで、誰から、なぜゲットしたか、そして、なぜ捨てるのかを入力します。

f:id:takagusu:20180813212612p:plain

[Add] ボタンをクリックすると、MetaMask のトランザクション送信画面が開きます。承認してトランザクションを送信すると、ERC-721 トークンが発行されます。

f:id:takagusu:20180813213117p:plain

[MyDiscardedRelics] ボタンをクリックすると、自分(ログインしているアドレス)が持っているトークンが表示されます。

f:id:takagusu:20180813213150p:plain

[photo] [metaData] は、それぞれ IPFS データへのリンクとなっています。メタデータは、このように JSON で保存されています。

f:id:takagusu:20180813213353p:plain


ちなみに、モバイルのウォレット対応ブラウザ(Trust や Chiper)からも利用できます。こんな感じ。

f:id:takagusu:20180813213740j:plain

f:id:takagusu:20180813213756j:plain

f:id:takagusu:20180813213944j:plain


今回は、とりあえず動くものをつくることを目標にしたので、細かい部分は全然できていませんし、例外処理もままならず酷いもんですが・・・だいぶ勉強にはなりました。
やっぱり手を動かしてみるのは大事ですね。

実際の作り方や、ハマった部分などは次の記事で書きたいと思います。

🥑

粘菌の LINE スタンプを作ってみました。

鷹楠です。

「粘菌スタンプ」という LINE スタンプを作ってみました。一度、LINE スタンプを作ってみたかったのと、粘菌という生き物の魅力を広めたかったのが、つくった理由です。(…最近、LINE 関係ばっかりですが、けっして LINE の回し者ではありません。笑)

LINE はいまや、日本ではかなりの利用率を占めるコミュニケーションツールであり、LINE Bot しかり、何かを広めるために有用なプラットフォームだなあと思うわけです。…まあ、LINE スタンプの数はすでに膨大になっていて、その中で突出するのは難しいのも事実ですが。

さて、ここでは、粘菌スタンプの宣伝も兼ねて、私がどういうやり方で LINE スタンプを作ったのか、制作過程を紹介したいと思います。


まず、完成形はこんな感じ。40 個のスタンプです。

https://store.line.me/stickershop/product/1397048

f:id:takagusu:20170311190322p:plain
f:id:takagusu:20170311190327p:plain

ところで、みなさんは粘菌という生き物をご存知ですか?その字のごとく「粘る菌」…と説明できればよかったのですが、そうではありません。「粘る」は部分的にあってますが、これはライフサイクルの一期間のあいだだけです。それから、「菌」ではありません(!)。粘菌は、アメーバみたいに動き回る時期と、キノコみたいに動かなくなる時期をもつ、動物でも植物でも菌類でもない不思議な生き物です。南方熊楠がこの粘菌の研究者だったことは有名です。なお、現在では粘菌の一部(真正粘菌)のことを「変形菌」と呼びます。
粘菌の注目すべきところはたくさんあるのですが、そのひとつが、見た目の美しさです。画像検索すると、カラフルで綺麗な姿に驚くと思います(気持ち悪さも多少ありますが 笑)。
ひとまず、粘菌の話はここまでとして、制作過程を説明します。


まず、スケッチブックに、アイデアスケッチ。
粘菌はいろいろ種類があるので、その調査・選別から始めました。スケッチはこんな感じ。

f:id:takagusu:20170311184451j:plain

f:id:takagusu:20170311184524j:plain

f:id:takagusu:20170311184601j:plain


上の画像では、どんなメッセージにすればよいか、案をいろいろ書いてみてます。


次に、実際に個々のスタンプをシャーペンで下書き。40 個は、意外と骨が折れます。

f:id:takagusu:20170311184921j:plain


f:id:takagusu:20170311185030j:plain


f:id:takagusu:20170311185109j:plain


f:id:takagusu:20170311185119j:plain


南方熊楠の名言。このスタンプの主題と言ってもいいでしょう。

f:id:takagusu:20170311185125j:plain


そして、これらの画像をスキャン…したかったのですが、スキャナーを持ってないので、スマホで写真に撮って PC に送りました。

Windows フォトで、必要な範囲だけクロップして、こんな感じ。

f:id:takagusu:20170311191530j:plain


次に、これらの画像を、線画に変換します。スキャンした画像をベースにして、ペイントソフトで本書き…してもいいんですが、めんどうだったので、エッジ検出で下書きをそのまま線画にすることにしました。写真加工.com というサイトのエッジ検出 http://www.photo-kako.com/edge.cgi を使いました。ブラウザ上で利用できるので Good です。

変換した結果は下記のような感じ。けっこう汚いですが…、それはこれからどうにかしていきます。

f:id:takagusu:20170311192227j:plain


次に、色付けです。どうやって色を塗ろうか考えていたところ、ちょうど、興味深い記事をみつけました。

日本発のPaintsChainerはAIで線画を自動着色―ニューラルネットワークが驚異の能力 | TechCrunch Japan

この技術、本当にすごいです。


「これを使って、色塗りをさせてみよう!!
世界初の AI 自動着色の LINE スタンプや!!」


…と思いました。世界初かどうかは、わかりません。


実際に、先ほどの線画をインプットして、色を塗ってみたら次のような感じ(だいぶ色を指定して、補正はしました)。
どうですか、かなりいい感じじゃないですか。

f:id:takagusu:20170311193405j:plain


こんな感じで、他の画像もすべて色を塗りました。
水彩画みたいで、優しいイメージです。私が思う粘菌のイメージにぴったりです。


続いて、GIMP を使って、必要なイラスト部分だけ切り取り、それ以外は透明化しました。このへんの詳しいやり方は、「GIMP 切り取り」や「GIMP 透明化」でググると出てくると思います。

必要な部分だけ切り取った結果はこんな感じ。

f:id:takagusu:20170311193928p:plain


そして最後に、所定のサイズに合わせて、文字を入れて完成!…までを GIMP でやりたかったんですが、GIMP だとなかなか操作が煩わしい…とくに、文字を入れたりフォントを選んだりするところ。これを 40 個分やる気にはならず、…どうしたかというと、だいぶ普通じゃないと思いますが、LibreOffice の Impress(パワーポイントみたいなやつ)を使って、文字入れと PNG 出力を行いました。

フォントは「ぎゃーてーるみねっせんす」を使用させていただきました。

スタンプに使える画像は PNG 形式で、サイズは最大 幅 370 × 高さ 320 ピクセルです。
LibreOffice を使って PNG 形式に出力した画像は、こんな感じ。なお、文字はすべて回りを白で囲っています(背景が白なのでわからないかもしれませんが)。

f:id:takagusu:20170311195210p:plain


LINE スタンプとして申請するには、40 個のスタンプ画像の他に、メイン画像(240×240 ピクセル)とトークルームタブ画像(96×74 ピクセル)が必要なので、これらも用意しました(…と言っても、作ったスタンプ画像からよさげなものをピックアップして、文字を消したりして、サイズを変えて出力しただけ)。
詳細は、ガイドラインを参照。

さて、LINE スタンプには、審査があります。
今回の粘菌スタンプの場合は、申請してから承認されるまで、1 週間ちょっとかかりました。幸運にも、リジェクトされずに一発 OK でした。

今回の反省点として…

  • 完成したスタンプの文字が意外と小さい。シミュレータで確認してたつもりですが、もっと大きくしたほうがよかったです。
  • なんか画質が悪い。PNG の画質の設定がよくなかったかもしれません。
  • 使いどころがわからないスタンプがいくつかある。

…などなど


…ということで、粘菌スタンプの制作過程のご紹介でした。

気に入った方は、ご購入いただけると嬉しいです。
こちらからどうぞ:https://store.line.me/stickershop/product/1397048
または、スマホの LINE アプリから、スタンプで「粘菌スタンプ」と検索すれば出てくると思います。


ここで紹介したのはあくまで作り方の一例ですが、
LINE スタンプを作ってみたいという方は、参考にしてみてください。

では。

近くの神社を教えてくれる LINE Bot を作ってみました。

鷹楠です。前回前々回と、LINE Bot を作ってきましたが、今回もまた新しい LINE Bot を作ってみました。

機能はシンプル。近くの神社を教えてくれて、参拝済みの記録をつけられる LINE Bot です。

今回は、LINE APIカルーセル型のテンプレートメッセージを使ってみました。それから、Google Apps Script で無料で簡単に使えるデータベース Google Fusion Tables も使ってみました。

きっかけ&こんなものが作りたい

  • 神社が好きで、有名どころはけっこうまわってる。
  • とはいえ、まだまだ知らない神社はいっぱい。
  • 近所や、ちょっと出かけた先で、まだ行ったことがない神社をみつけたい。暇つぶしや、ウォーキングがてら。
  • 行ったことがある神社は、何かしらの形でわかるようにしたい。
  • 社格なんかをすぐ知りたい。
  • 御朱印を管理したい。

…とまあ、常日頃からこういう想いがあって、とりあえず作れるものを作ってみました。ちなみに、最後の 2 つの機能(社格なんかの表示、御朱印の管理)は、残念ながらこの記事を書いている時点では実現できていません。今後の課題です。

こんなものができた

  • 位置情報を送信すると、そこから半径 3 km 以内の神社の情報を、最大 5 件教えてくれる。
  • 神社の情報には、「名前」「そこまでの直線距離」「住所」が含まれる。
  • 「その神社を Google 検索するためのボタン」「その神社までのルート(by Google マップ)を検索するためのボタン」もいっしょに表示される。詳細が知りたかったら、ここから検索できる。神社名だけでなく住所もいっしょに検索ワードになってるので、一意に神社を特定して検索できる。
  • 「行ったことがある」ボタンもいっしょに表示される。これを押すと、次回からこの神社の情報には「参拝済み」マークがつく。


外観を紹介します。

Bot の名前は「近くの神社」。

f:id:takagusu:20170307193911j:plain


友だち追加時のメッセージ:

f:id:takagusu:20170307193934j:plain


位置情報を送信すると、

f:id:takagusu:20170307194130j:plain


そこから半径 3 km 以内の神社の情報を、最大 5 件教えてくれます。
今回は、カルーセル型のテンプレートメッセージでメッセージを送ってます。横にスライドするやつです。最大 5 件というのは、LINE API の制限だったり…。

f:id:takagusu:20170307194014j:plain


タップできるボタンが 3 つありまして、

f:id:takagusu:20170307194458j:plain


「この神社を検索」ボタンをタップすると、「神社の名前」+「住所」で Google 検索します。(LINE 内のブラウザで開かないで欲しいんですけどね…)

f:id:takagusu:20170307194602j:plain


「ここからのルート」ボタンをタップすると、Google マップのルート検索結果を表示します。徒歩の場合のルートにしてます。

f:id:takagusu:20170307195250j:plain


最後に、「行ったことがある」ボタンついて。タップすると、内部で持ってるデータベースにユーザーと神社の ID が保存されて、次に神社が表示されたときには、「参拝済み」という文字がいっしょに表示されます。

f:id:takagusu:20170307195723j:plain

大まかな作り方

  • 前回と同じく、LINE Bot アカウントを作り、Google Apps Script で LINE Bot 用の web アプリケーションを作成した。
  • 位置情報(緯度・経度)の近くにある神社を探すために、Yahoo!ローカルサーチAPI を利用した。この API の優れているところは、「コンビニ」「カフェ」「動物園」「歯科」などかなり具体的な施設のカテゴリ(業種コード)で検索できるところ。嬉しいことに、このカテゴリの中に「神社」があった。業種コードの一覧はここを参照(一覧の csv をダウンロードできる)。なお、利用する際はクレジット表示が必要。
  • 神社の情報を伝えるために LINE APIカルーセル型のテンプレートメッセージ を利用した。タップしたときに URL リンクを開けるように uri アクションを使い、神社に行ったことがあるという情報を受け取るために postback アクションを使った。
  • ユーザーごとの参拝済み情報(神社に行ったことがあるかどうか)は、Google Fusion Tables というデータベースで管理。Google DriveFusion Tables(試験運用) のアプリを追加して、Google API Console で有効化すると、Google Apps Script のコードから呼び出せるようになる。Google Fusion Tables は試験運用中らしく、今後どうなるかちょっと不安なところはあるけど、とりあえず気にしない。

ソースコードの紹介

GitHub に格納しています。

Google Apps Script(.gs ファイル)で実装してます。Google Apps Script を使った LINE Bot 用の基本的なコードについては、前回の記事も参照。

位置情報から、近くの神社を検索する

今回の LINE Bot で最も重要な部分です。送信された位置情報から、近くにある神社の情報を取得するための、getNearShrines() という関数を作りました。このコードでは、Yahoo!ローカルサーチAPI の仕様に従って、指定した 緯度 (latitude) と経度 (longitude) から、半径 3 km 以内にある「神社」(業種コード: 0423002)を最大 5 件、近い順に検索するための HTTP リクエストを投げています。結果は JSON 形式でもらうように指定してます。なお、事前に Yahoo! の アプリ ID を取得しておく必要があります。この Web API のレスポンスとして、神社の名前・住所・緯度・経度などを取得できます。

var YAHOO_APP_ID = 'XXXXX' // Yahoo! のアプリ ID
var YAHOO_SEARCH_URL = 'https://map.yahooapis.jp/search/local/V1/localSearch';

function getNearShrines(latitude, longitude) {
  var url = YAHOO_SEARCH_URL
          + '?appid=' + YAHOO_APP_ID
          + '&dist=3'     // 3 km 以内
          + '&gc=0424002' // 業種コード: 神社
          + '&results=5'  // 最大 5 件
          + '&lat=' + latitude
          + '&lon=' + longitude
          + '&output=json&sort=dist';
  var response = UrlFetchApp.fetch(url);
  
  var shrines = [];
  var features = JSON.parse(response.getContentText('UTF-8'))['Feature'];
  for (i = 0; i < features.length; i++) {
    var uid = features[i]['Property'].Uid;         // 場所の ID
    var name = features[i].Name;                   // 場所の名前
    var address = features[i]['Property'].Address; // 場所の住所
    var coords = features[i]['Geometry'].Coordinates.split(',');
    var shrine_longitude = coords[0];              // 経度
    var shrine_latitude = coords[1];               // 緯度

    // 神社の情報を配列に入れる処理

  }
  return shrines;
}

2 点の場所の距離を求める

Yahoo!API を使うことによって、近くの神社の緯度・経度などの情報が取れました。ほんとはこの中に、検索した場所からの距離が含まれていてほしかったのですが、どうもこの情報は含まれてない。そこで、検索した場所から、取得した神社の場所までの直線距離を、2点間距離API(これも Yahoo! 提供)を使って求めました。…まあ、ほんとうは道なり距離が知りたかったんですけどね。ぱっと探した感じだと、無料でたくさんリクエストできる API が見つかりませんでした。直線距離でも、ないよりはマシな情報かな、と。

コードは下記のような感じ。2 つの 緯度・経度 から、直線距離(地球の楕円体に合わせた距離)を求めます。パラメータ coordinates に、「coordinates=139.73091159286,35.665662327613 135.49513388889,34.701974166667」のように、2 つの位置情報をスペースで繋げて渡すみたいなんですが、当然といえば当然ながら、パーセントエンコードしないとうまく動きません(ハマりました)。Google Apps Script 標準の関数を使って、encodeURIComponent(' ') とやってます(「%20」と直に書いてももちろん OK)。最後の Math.round(distance * 10) / 10 は、小数点以下 1 桁の距離 (km) にするためです。

function getDistanceInKilloMeters(latitude1, longitude1, latitude2, longitude2) {
  var url = YAHOO_DIST_URL
          + '?appid=' + YAHOO_APP_ID
          + '&coordinates=' + longitude1 + ',' + latitude1 + encodeURIComponent(' ') + longitude2 + ',' + latitude2
          + '&output=json';
  var response = UrlFetchApp.fetch(url);
  var distance = JSON.parse(response.getContentText('UTF-8'))['Feature'][0]['Geometry'].Distance;
  return Math.round(distance * 10) / 10;
}

Google 検索の URL と Google マップのルート検索の URL

Google 検索の URL は、次のコードで生成しています。https://www.google.co.jp/search という URL に、パラメータ q を渡します。検索する文字列がマルチバイト文字の場合は、「入力文字のエンコードUTF-8 ですよ」と指定する ie=UTF-8 もつけます(説明として正確ではないかも)。

function getGoogleSearchUrl(query) {
  return 'https://www.google.co.jp/search?q=' + encodeURIComponent(query) + '&ie=UTF-8';
}


Google マップのルート検索の URL は、次のコードで生成しています。http://maps.google.com/maps という URL に、2 つの緯度経度を saddr と daddr で渡します。dirflg=w をつけると、徒歩でのルート検索になります。ただ、このへんの URL 生成は、やってみたらできた、って感じなので、そのうち動かなくなるかもしれません。…いやー、しっかし、このような URL にアクセスするだけで一瞬でルートを示してくれるって、かなり凄いことだと思います。

function getGoogleMapRouteUrl(srcLatitude, srcLongitude, destLatitude, destLongitude) {
  return 'http://maps.google.com/maps'
         + '?saddr=' + srcLatitude + ',' + srcLongitude
         + '&daddr=' + destLatitude + ',' + destLongitude
         + '&dirflg=w';
}

ルーセル型のテンプレートメッセージと、ポストバック機能

横にスライドするやつです(下記画像のようなかんじ)。LINE APIリファレンスはこちら

f:id:takagusu:20170307194014j:plain


リファレンスに書いてあるとおりに送れば実現できるのですが、リファレンスは、なんだかリンク飛ばされまくって、どこに何をいれたらいいのかわかりづらいと感じます。結果的に、下記のような JSON を送ればカルーセル型のメッセージになりました。

// ユーザーがタップするボタンになるもの. 最大 3 個まで作成可能.
var actions = [
  {
    'type': 'postback',          // ポストバックの場合は postback
    'label': '行ったことがある',
    'data': 'action=visited&uid=<場所のID>'  // ポストバックする任意の文字列
  },
  {
    'type': 'uri',               // リンクの場合は uri
    'label': 'この神社を検索',
    'uri': googleSearchUrl       // 神社の 検索 URL
  },
  {
    'type': 'uri',               // リンクの場合は uri
    'label': 'ここからのルート',
    'uri': googleMapRouteUrl     // 神社の ルート検索 URL
  }
];

// 横にスライドされる 1 単位. 最大 5 個まで作成可能.
var columns = [
  {
    'title': title,
    'text': 'ここから ' + distance + 'km ― ' + address,
    'actions': actions
  },
  
  ...

]

// ユーザーに返信するメッセージ. 最大 5 個まで作成可能.
var messages = [
  {
    'type': 'template',
    'altText': '代替のテキストメッセージ',  // 通知窓なんかにもこれが表示される
    'template': {
      'type': 'carousel', // カルーセル型のテンプレートメッセージの指定
      'columns': columns
    }
  }
];


アクションの type に postback を指定した場合、ユーザーがそれをタップすると、サーバーにポストバックが送信されます。今回、この機能を使って、ユーザーが行ったことがある神社を記録する処理に繋げてます。

送信されてきたデータを JSON に変換して(変数名 json)、json.events[0].postback.data でポストバックデータを取得できます。データの設計・使い方は自由です。この LINE Bot では、「action=visited&uid=<場所のID>」という文字列を渡して、これを受け取ってデータベースを操作するようにしています。

var LINE_BOT_CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_BOT_CHANNEL_ACCESS_TOKEN'); // Google Apps Script の [プロジェクトのプロパティ] > [スクリプトのプロパティ] で値を設定
var LINE_REPLY_URL = 'https://api.line.me/v2/bot/message/reply';


function doPost(e) {
  var json = JSON.parse(e.postData.contents);

  // 中略

  if ('postback' == json.events[0].type) {
    var data = json.events[0].postback.data;

    // 送信されてきたデータを使った処理
    // 今回の場合、「visited」or「unvisited」の文字列と、神社の場所 ID が送信される。

    messages = [{'type': 'text', 'text': '行ったことがある神社を更新しました'}]; 
  }

  UrlFetchApp.fetch(LINE_REPLY_URL, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_BOT_CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

Google Apps Script でデータベースを利用する

Google Apps Script で無料で簡単に使えるデータベース Google Fusion Tables を使って、ユーザーごとの神社参拝記録を行うための、ごく簡単なデータベースを作りました。

上の方でも少し書きましたが、Google DriveFusion Tables(試験運用) のアプリを追加して、Google API Console で有効化すると、Google Apps Script のコードから呼び出せるようになります。

まず、Google Drive で任意の場所に Fusion Tables のファイルを作成します。そしてこれを開いて、あらかじめ、カラムを定義しておきます。下記のような感じです(※これはデータがいくつか入ってるとき)。Text 型とか Date 型とか使えます。ちなみに、Location 型という特殊な型もあって、これを使うと地図上に簡単にマッピングできるらしいです(今回使ってませんが)。

f:id:takagusu:20170308221114p:plain


さて、こうして作ったデータベースに Google Apps Script からアクセスするには、下記のようにします。あんまり綺麗じゃないですが(undefined で判断してるとことか特に…)、「場所の ID」(uid) と「ユーザー ID」(userId) と「参拝の有無」(visited, 1 or 0) が引数で、指定した docId の Fusion Tables データベースに対して、select や insert、update の操作を行っています。既に「場所の ID」と「ユーザー ID」の組み合わせがある場合には、その rowId を取得して、これを使って update を実行します(rowId を指定しないと update ができないという、少し特殊なつくりになってます)。「場所の ID」と「ユーザー ID」の組み合わせがない場合には、新規に行を insert します。

// Google Apps Script の [プロジェクトのプロパティ] > [スクリプトのプロパティ] で値を設定
var GOOGLE_FUSION_TABLES_DOC_ID = PropertiesService.getScriptProperties().getProperty('GOOGLE_FUSION_TABLES_DOC_ID');

function changeVisited(uid, userId, visited) {
  var sql = "SELECT ROWID FROM " + GOOGLE_FUSION_TABLES_DOC_ID
          + " WHERE uid = '" + uid + "' and userId = '" + userId + "'";
  var result = FusionTables.Query.sqlGet(sql);
  if (typeof result.rows === 'undefined') {
    sql = "INSERT INTO " + GOOGLE_FUSION_TABLES_DOC_ID
        + " (uid, userId, visited)"
        + " VALUES ('" + uid + "', '" + userId + "', " + visited + ")";
    FusionTables.Query.sql(sql); 
  } else {
    var rowid = result.rows[0];
    sql = "UPDATE " + GOOGLE_FUSION_TABLES_DOC_ID
        + " SET visited = " + visited
        + " WHERE ROWID = '" + rowid + "'";
    FusionTables.Query.sql(sql);
  }
}

簡単なデータベースであれば、この Fusion Tables で十分ですね。


長くなってしまいましたが、とりあえずソースコードの紹介はこんな感じで。
最終的な .gs ファイルは、GitHub に格納しているので、よければ参考にしてください(あんまりいいコードじゃないですが)。

では。

アイデアを記録・想起する LINE Bot を作ってみました。

鷹楠です。
思いついたアイデアやメモを記録できて、さらに、過去のアイデアを思い出させてくれる LINE Bot を作ってみました。前回作った LINE Bot では、IBM の Bluemix でサーバーを用意しましたが、今回は、もっとお手軽な Google Apps Script を使ってみました。

コンセプト(こんなものが作りたい)

  • 思いついたアイデアやメモを、LINE を使って記録したい
  • 記録したアイデアは、LINE 以外からでもアクセス、活用したい
  • 過去に記録したアイデアを、あとから思い出したい

ちなみに、私は普段、LINE に自分一人だけのグループを作っていて、メモ帳代わりに使ってます。今回は、それをもっと活用できるように拡張したかった…という感じです。

こんなものができた


Bot の名前は「アイデアの泉」(…幹並みですが)。




友だち追加時のメッセージはこんな感じ。


イデアを入力すると、自動的に Google スプレッドシートに記録します。


「アイデア」と入力すると、これまでに入力したアイデアを、日時とともにランダムに 2 つ表示します。この機能によって、なにか新しいアイデアの組み合わせが見つかればいいなあ。


スプレッドシート」と入力すると、アイデアが記録されている Google スプレッドシートの URL を教えてくれます。ブラウザやスプレッドシートからアクセスできるので、アイデアやメモを活用しやすくなります。


スマホGoogle スプレッドシートのアプリで開いた様子。
入力したアイデアやメモが、投稿日時とともに記録されてます。


スプレッドシートはユーザーごとに 1 つ、自動で作成されます。とりあえず、権限は閲覧のみとしました。権限は、どうするのがベストか、悩ましい。…というのも、LINE のユーザーと Google のユーザーが紐付けられないので、Everyone が閲覧可能 or 編集可能 のどちらかを選ぶしかないのです。

おおまかな作り方

  • LINE Bot のアカウントを作成する。
  • Google Drive に、Google Apps Script のプラグインを入れて、Google Apps Script のファイルを新規作成する。
  • Google Apps Script のコードをブラウザ上で書いて、web アプリケーションとして公開する(メニューから選ぶだけ)。このとき URL が決まるので、コピって、LINE Bot の Webhook URL に設定する。

Google Apps Script では、とても簡単に web アプリケーションを公開できます。この手軽さは、本当に素晴らしいです。しかも無料。何かアプリを作るときにいちいちサーバーを立てるのが、ばかばかしく思えてきます。使えるのは JavaScript をベースにした Google 独自の言語であり、なんでもやれるというわけではないですが、それでも、かなりのことが実現できます。

ソースコードの説明

ソースコードは、下記の GitHub リポジトリに格納してます。
GitHub - avcdsld/IdeaLogger: This is a LINE Bot to record your ideas input in Google SpreadSheet. By using this bot, you can also recall past ideas randomly.


Google Apps Script の拡張子は .gs となります。
web アプリを作成するには、.gs ファイルに下記の関数を定義します。LINE Bot に対してメッセージが送信されると、この関数が実行されます。

function doPost(e) {
  // Post送信されてきたときの処理
}


どんな HTTP リクエストが来るかは、LINE の REST API のリファレンス で確認します。
e.postData.contents を JSON としてパースして、必要なパラメータを取得します。Bot からメッセージを返信するために必要となるトークンや、ユーザーから送信されたメッセージなど。

今回の場合、LINE Bot へのメッセージに返信する処理は、下記のようになります(下記のコードだと未実装部分があるので注意。完成版は GitHub のコード を参照)。

var CHANNEL_ACCESS_TOKEN = 'dummy';  // LINE Bot のアクセストークンを入力してください
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';

function doPost(e) {
  // JSONをパース
  var json = JSON.parse(e.postData.contents);

  // 送信されてきたメッセージを取得
  var user_message = json.events[0].message.text;  
  
  // 返信するためのトークンを取得
  var reply_token= json.events[0].replyToken;
  if (typeof reply_token === 'undefined') {
    return;
  }

  // 返信するメッセージを配列で用意する
  var reply_messages;
  if ('ヘルプ' == user_message) {
    // 「ヘルプ」と入力されたときの返信メッセージ
    reply_messages = ["スプレッドシートにアクセスしたい場合は「スプレッドシート」と入力してください。\n\nアイデアを思い出したくなったら「アイデア」と入力してください。あなたの過去のアイデアをランダムにお伝えします。\n\n使い方がわからなくなったら「ヘルプ」と入力してみてください。"];

  } else if ('スプレッドシート' == user_message) {
    //「スプレッドシート」と入力されたときの処理
    // Google スプレッドシートの URL を返信メッセージとする
    // reply_messages = ...

  } else if ('アイデア' == user_message) {
    //「アイデア」と入力されたときの処理
    // 過去のアイデアからランダムに取得して、返信メッセージとする
    // reply_messages = ...

  } else {
    // アイデアやメモが入力されたときの処理
    reply_messages = ['アイデアが追加されました'];
  }

  // メッセージを返信
  var messages = reply_messages.map(function (v) {
    return {'type': 'text', 'text': v};    
  });    
  UrlFetchApp.fetch(line_endpoint, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}


Google スプレッドシートを操作する処理について説明します。
SpreadsheetApp.create(<任意のタイトル>) で、スプレッドシートを新規に作成できます。
書き込むためには、シートを指定する必要があります。下記のコードの変数名だと、spreadSheet.getSheets()[0] で最初のシートを取得でき、spreadSheet.appendRow(<追加する行の配列>) で行を追加できます。楽でいいです。権限は、DriveApp でファイルを取得して setSharing() で設定できます。

  // スプレッドシートの作成と、ヘッダ行の追加
  var spreadSheet = SpreadsheetApp.create("スプレードシートのタイトル");
  var sheet = spreadSheet.getSheets()[0];
  sheet.appendRow(['日時', 'メッセージ']);

  // 権限の設定(全員が閲覧可能)
  var file = DriveApp.getFileById(spreadSheet.getId());
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  

なお今回、ひとりのユーザーにただ 1 つのスプレッドシートを作成するために、LINE のユーザー ID(API 用の ID)とスプレッドシート ID の組み合わせを保管することにしました。保管には ScriptProperties を使っています(PropertiesService.getScriptProperties() で取得可能)。

スプレッドシートからランダムにセル値を取得する方法については割愛します。

雑ですが (笑)、ソースコードの説明はこんな感じで。

LINE Bot のしくみで、何か別のものも作ってみたいです。

では。

毎朝 天気を通知する LINE Bot を作ってみました。

鷹楠です。今年は「思いついたらとりあえずやってみる」精神でいきたいです。
クオリティはひとまず気にしない。既存のものがあるとかも気にしない。

きっかけ&こんなものをつくりたい

朝、傘を持っていくのを忘れる。
天気予報をみればいいんだけど、それをみるのを忘れる。
毎朝、LINEに通知が来れば嬉しい。特に知りたいのは降水確率。
地域の設定はなるべく手間をかけずにやりたい。
※ちなみに、毎日天気を知らせてくれる、似たようなサービスは既にあるようです(「LINEお天気」など)。

こんなものができた

名付けて、「毎朝お知らせ!天気予報」


f:id:takagusu:20170124194725j:plain


使い方の表示はこんな感じ(友だち追加時と、何か話しかけたときに表示)。


f:id:takagusu:20170124194802j:plain


位置情報を送信すると、地域を設定できる。


f:id:takagusu:20170124194917j:plain


f:id:takagusu:20170124200046j:plain


「天気」と入力すると、その日の天気予報をお知らせ。

そして、「スタート」と入力すると、毎朝7時に天気が通知されるようになる。


f:id:takagusu:20170124200317j:plain


朝、通知が届いている様子。これで傘を忘れることもなくなりそう。


f:id:takagusu:20170124195216j:plain


おおまかなつくりかた

  • LINE Bot 自体は、LINE Business Center に登録すれば無料で作成できる。ためしにということで、Messaging API Developer Trial でアカウントを作成した。返信だけでなく、Bot 側からユーザーにメッセージを送信する Push API が利用できる。ただし、追加可能友だち数は50人という制限がある。ブラウザ上で、Bot のプロフィールなどを設定する。
  • IBM Bluemix にアカウントを作成し、LINE Bot の返信のためのサーバーアプリを作る。Bluemix は Amazon Web Service や Google Cloud Platform のような、開発者向けクラウドサービスで、工夫すれば無料で利用できる。
  • 今回、サーバーアプリは、Bluemix が提供している Ruby + Sinatra のテンプレートをベースにして、LINE が提供している Ruby 用の API を使って実装した。LINE Business Center から辿れる LINE Developers > Channels ページで、作成した LINE Bot の Webhook URL に、サーバーアプリの URL を設定することで、Bot の返信がサーバーアプリに任される。
  • 天気の情報は、JP Weather Forecast という天気予報 API を利用させてもらった。無料で降水確率が取得できる API はこれ以外にほとんどなかった。ありがたい。(ただ、欲を言うともっと細かい地域の情報が知りたい。)日本の主要な地域の天気予報が XML または JSONP 形式で取得できるので、これを Ruby でパースする。
  • どこの天気をお知らせするかという 地域 の設定は、LINE の位置情報を利用できるようにした(これは LINE のかなり売りな機能だと思う)。ユーザーが位置情報を送信すると、サーバーアプリでは 緯度・経度 が取得できるので、天気予報 API から取得できる最も近い観測地点を自動的に選択する。各都道府県ごとに、数個の地点があるようである。詳細な位置情報は、プライバシーの観点から、保存しない。
  • ユーザーごとの ID と地域を保存するために、Bluemix のサービスのひとつ ClearDB (中身は MySQL)を利用した。5 MB までなら無料で使える。
  • 毎朝メッセージを送信するために、Bluemix のサービスのひとつ Workload Scheduler を利用した。Windows のタスクスケジューラのようなことができる(というより、UI そのまんま)。月 50 回までなら無料で使える。

ソースコードの紹介

LINE Bot の受け口と返信

/callback という URL を受け口にすることにします(名前は任意です)。ユーザーからメッセージが来た場合、この URL が呼ばれます。Sinatra のコードだとこんな感じ。

post '/callback' do

  # 処理

  "OK"
end


LINE の Ruby 用の API を利用するために、require に下記を書く(Gemfile にも)。

require 'line/bot'


Ruby 用の API のサンプルコードに倣って、まず client を定義。ここで、LINE_CHANNEL_SECRET と LINE_CHANNEL_TOKEN は、Bluemix に設定した環境変数です。値は LINE Developers で取得・確認できます。

def client
  @client ||= Line::Bot::Client.new { |config|
    config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
    config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
  }
end


client.parse_events_from でリクエスト body をパースして、

  body = request.body.read

  events = client.parse_events_from(body)


events.each で回して、イベントの種類ごとに処理を書きます。
文字列が入力された場合は event.type の値が Line::Bot::Event::MessageType::Text に、位置情報が入力された場合は Line::Bot::Event::MessageType::Location になるようです。
メッセージを返信するには、まず message オブジェクトを作成し、client.reply_message(event['replyToken'], message) を実行します(下記のコード参照)。一定時間(記事作成時点で30秒)以内に返事を返さないといけないようです。

  events.each { |event|
    case event
    when Line::Bot::Event::Message
      case event.type

      when Line::Bot::Event::MessageType::Text
        # 文字列が入力された場合

        case event.message['text']
        when 'スタート'
          # 「スタート」と入力されたときの処理
        when 'ストップ'
          # 「ストップ」と入力されたときの処理
        when /.*天気.*/
          # 「天気」を含む文字列が入力されたときの処理

        end

      when Line::Bot::Event::MessageType::Location
        # 位置情報が入力された場合
        latitude = event.message['latitude'] # 緯度
        longitude = event.message['longitude'] # 経度

        # 経度・経度を使った処理
      end

      message = { type: 'text', text: "デフォルトの返信メッセージ"}
      client.reply_message(event['replyToken'], message)
    end
  }

天気の取得

必要なモジュール(Ruby標準です)

require 'net/http'
require 'uri'
require 'rexml/document'


JP Weather Forecast という天気予報 API で、天気を XML 形式で取得し、パースします。Ruby だと、とても簡単に書けます。これは東京都の天気を取得する例です。

    uri = URI.parse('http://www.drk7.jp/weather/xml/13.xml')
    xml = Net::HTTP.get(uri)
    doc = REXML::Document.new(xml)


各情報を取得します。doc.elements[<xpath>].text で値を取得、doc.elements[<xpath>].attributes[<属性名>] で属性値を取得できます。この簡便さ。RubyXML パーサは神ですね。上記のコードに引き続き、東京都 東京地方 の天気を取得する例です。

    xpath = 'weatherforecast/pref/area[4]'

    weather = doc.elements[xpath + '/info/weather'].text # 天気(例:「晴れ」)
    max = doc.elements[xpath + '/info/temperature/range[1]'].text # 最高気温
    min = doc.elements[xpath + '/info/temperature/range[2]'].text # 最低気温
    per00to06 = doc.elements[xpath + '/info/rainfallchance/period[1]'].text # 0-6時の降水確率
    per06to12 = doc.elements[xpath + '/info/rainfallchance/period[2]'].text # 6-12時の降水確率
    per12to18 = doc.elements[xpath + '/info/rainfallchance/period[3]'].text # 12-18時の降水確率
    per18to24 = doc.elements[xpath + '/info/rainfallchance/period[4]'].text # 18-24時の降水確率

位置情報から地域の選定

LINE の位置情報を利用して、天気を取得できる地域のうち、最も近いものを選定します。
天気を取得できる地域の緯度・軽度はあらかじめ天気予報 API から取得し、データベースに格納しておきます。

先ほど、LINE でユーザーから位置情報が送信された場合に 緯度・経度 を取得するコードを紹介しましたので、その情報を使って、下記の SQL を実行します。area_info は、地域の緯度・経度を格納しているテーブル名です。

select * from area_info order by abs(latitude - 緯度) + abs(longitude - 経度) asc

Bot 起点でユーザーにメッセージを送る

返信ではなく、Bot 起点でユーザーにメッセージを送るには、下記のように書きます。Push API というらしいです。
こちらは、Developer Trial だと無料で利用できます(ただし先ほども書いたように、追加可能友だち数は 50 人まで)。フリー版だと Push API は利用できません。

message = { type: 'text', text: '送信したいメッセージ' }
client.push_message(row['user_id'], message)

今回、毎朝この処理を実行したいのですが、Bluemix の無料枠では、Sinatra 上で定期実行するのは難しそう…(できるのかな?)。Sinatra 上でこのようなことをする方法をどなたか知ってれば教えてほしいです。

ということで、処理を実行するための URL をひとつ用意することにして、Bluemix の Workload Scheduler から HTTP リクエストを投げることで実現しました。
サーバー側の受け口は下記のような感じ。/send という URL にしました。

get '/send' do
  protected! # basic auth

  # メッセージ送信処理

  "OK"
end

さすがにセキュリティが何もないとまずいと思うので、Basic 認証を入れました。上記、protected! を呼んでいるところの定義は下記です。こちらは、Sinatra の Q&A に載っていたものをそのまま使いました(まあ、Basic 認証もセキュリティ上よくないですが…)。BASIC_AUTH_USERNAME と BASIC_AUTH_PASSWORD は Bluemix の環境変数で、ユーザー名とパスワードです。

helpers do
  def protected!
    return if authorized?
    headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
    halt 401, "Not authorized\n"
  end

  def authorized?
    @auth ||=  Rack::Auth::Basic::Request.new(request.env)
    @auth.provided? and @auth.basic? and @auth.credentials and @auth.credentials == [ENV['BASIC_AUTH_USERNAME'], ENV['BASIC_AUTH_PASSWORD']]
  end
end

所感

LINE Bot は、いろんなAPIと組み合わせることができるとても良いプラットフォームだなと思います。しくみは単純で、それゆえに奥が深い。LINE BOT AWARD なるものもあり、APIを提供しているいろんな企業が協賛になってるみたいです。他のプラットフォームやAPIも活用して、何か作ってみたいですね。

では。