LCL Engineers' Blog

夜行バス比較なび(高速バス比較)・格安移動・バスとりっぷを運営する LCLの開発者ブログ

LINE botの返事をカルーセル形式にしてみました

フロントエンドエンジニアの岡田です。 先日作ったLINE botを改良しましたのでご紹介します。 techblog.lclco.com

前回の状態では、botはテキストメッセージを返すだけでした。 f:id:lcl-engineer:20170227051900p:plain:w382

これでは開いてみるまでどんなヨガ動画かわかりませんし、候補が1つしかないのもいまいちです。

そこで、Template messageを使って、候補の出し方を変えてみました。 Template messageには3種類あります。

  • Buttons
  • Confirm
  • Carousel

ドキュメント

今回はCarouselを使いました。 右にスライドすると、最大5つのヨガ動画が表示されます。
f:id:lcl-engineer:20170330060532g:plain

見た目が一気に豪華になりましたね。
今回はURLの他に、動画のタイトルやdescription, thumbnail等も出しています。

ソースコードは以下のとおりです。

// -----------------------------------------------------------------------------
// 定数の設定
const LINE_CHANNEL_ACCESS_TOKEN = 'あなたのChannl Access Token';
const GOOGLE_API_KEY = 'あなたのGOOGLE API KEY';
const DOMAIN = 'あなたのサーバーのドメイン: https://xxxxxxxx.herokuapp.com/';

// -----------------------------------------------------------------------------
// モジュールのインポート
var express = require('express');
var bodyParser = require('body-parser');
var request = require('request');
var app = express();

// -----------------------------------------------------------------------------
// ミドルウェア設定
app.use(bodyParser.json());

// -----------------------------------------------------------------------------
// Webサーバー設定
var port = (process.env.PORT || 3000);
var server = app.listen(port, function() {
  console.log('Node is running on port ' + port);
});

// -----------------------------------------------------------------------------
// ルーター設定
app.get('/', function(req, res, next) {
  res.send('Node is running on port ' + port);
});

app.post('/webhook', function(req, res, next) {
  res.status(200).end();
  for (var event of req.body.events) {
    if (event.type == 'message') {
      var requestHeaders = {
        'Content-Type': 'application/json',
        'referer': DOMAIN
      }
      //オプションを定義
      var options = {
        url: 'https://www.googleapis.com/youtube/v3/search?key=' + GOOGLE_API_KEY + '&part=snippet&channelId=UCd0pUnH7i5CM-Y8xRe7cZVg&q=' + encodeURI(event.message.text),
        method: 'GET',
        headers: requestHeaders,
        json: true
      }

      //リクエスト送信
      request(options, function(error, response, responseBody) {
        //コールバックで色々な処理
        var columns = [];
        for (var item of responseBody.items) {
          columns.push({
            "thumbnailImageUrl": item.snippet.thumbnails.medium.url,
            "title": item.snippet.title,
            "text": item.snippet.description ? item.snippet.description.substr(0, 60) : ' ', // title指定時は60文字以内,
            "actions": [{
              "type": "uri",
              "label": "動画を再生",
              "uri": 'https://www.youtube.com/watch?v=' + item.id.videoId
            }]
          });
          // carouselは最大5つのため、6つ以上の候補はカット
          if (columns.length === 5) {
            break;
          }
        }

        var body = {
          replyToken: event.replyToken,
          messages: [{
            "type": "template",
            "altText": "this is a carousel template",
            "template": {
              "type": "carousel",
              "columns": columns
            }
          }]
        }
        var url = 'https://api.line.me/v2/bot/message/reply';
        var headers = {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
        }
        request({
          url: url,
          method: 'POST',
          headers: headers,
          body: body,
          json: true
        });
      })
    }
  }
});

以上です。 更なるパワーアップを目指して、改良していこうと思います。

RailsでPaperclipを利用して、画像に透かし(ウォーターマーク)を付与する

弊社で運営している「バスとりっぷ」というメディアでは、Paperclipを利用して画像のアップロードを行っています。Paperclipでは、サムネイルの作成やリサイズ・背景色の変更などを行っていましたが、透かし(ウォーターマーク)の付与にも対応しました。

www.bushikaku.net

ウォーターマークの付与イメージ

画像に透かし(ウォーターマーク)を付与するには、透かし画像を用意し元画像に重ね合わせます。

f:id:lcl-engineer:20170326200650p:plain

実装方法

paperclipを拡張してもできますが、paperclip-watermarkというgemを利用しました。

GitHub - vikewoods/paperclip-watermark: module PaperclipWatermark

gem 'paperclip-watermark'

paperclipの設定に、以下のようにwatermarkの設定を追加するだけです。これでアップロードしたファイルに、ウォーターマークが付与されます。

class SampleImage < ActiveRecord::Base
  has_attached_file : attachment,
                    :processors => [:watermark],
                    :styles => { 
                            :original => { :watermark_path => "#{Rails.root}/watermark.png",:position => "southwest"}
                     }

paperclipの利用方法については、下記のページを参照ください。

Thumbnail Generation · thoughtbot/paperclip Wiki · GitHub

ウォーターマーク付与位置の変更

ウォーターマークを付与する位置は、positionを指定することで変更できます。

:original => { :watermark_path => "#{Rails.root}/watermark.png",:position => "northwest"}

以下の9種類の位置が指定できるようになってます。

  • northwest 左上
  • north 中央上
  • northeast 右上
  • west 中央左
  • center 中央
  • east 中央右
  • southwest 左下
  • south 中央下
  • southeast 右下

さらに「geometry」を指定すれば、上記の基準位置からピクセル単位で調整できるようです。(未検証)

ウォーターマークを外す

ウォーターマークを付与したくない画像に対して付与してしまった場合など、ウォーターマークを外したい場合もあります。その時は、以下の手順で復元します。

  • ウォーターマーク付与時に、元画像を別名で保存する
  • ウォーターマークを外す場合は、元画像を付与済みの画像に上書きする。

別名保存は、paperclipの指定で簡単にできます。

:styles => { 
                   :original_file_backup => {},
                   :original => {
                         :processors => [:watermark],
                   ・・・
                 }
}

別名で保存した画像が、インターネット上で誰にでもアクセスされてしまうと、結局ウォーターマークが付与されていない画像を取得することが可能になってします。paperclipでは、S3の権限も簡単に制御可能なので、以下のようにアクセス制限を行っています。

:s3_permissions => {:original_file_backup => :private},

既にアップロード済み画像に付与する

既にアップロード済みの画像に対しても、paperclipで再処理することが可能です。処理対象のmodelに対して、reprocessを実行します。

sample_image = SampleImage.find(123)
sample_image.reprocess!

まとめ

実際には、ウォーターマークを付与する・しないを画像によって振り分けているため、細かい判定をロジックを行っています。 その辺は結構手間がかかってますが、ウォーターマークの付与は、paperclip-watermarkを使って簡単に実現できました。

GitHubへのpush時に、featureブランチ環境を自動作成する

弊社では、作業中ブランチの動作を誰でも確認できる仕組みを用意しています。今回の記事では、導入背景から運用方法までを紹介したいと思います。

解決したかった課題

弊社のWebサービス開発では、GitHub Flowを採用しています。 GitHub Flowでは、masterへマージ後にすぐプロダクション環境へデプロイするため、masterへのマージ後には問題が発見されないのが理想です。その為、他のメンバーにfeatureブランチの動作を早い段階で確認してもらう仕組みが必要でした。

開発者のローカル環境へアクセスすれば確認することはできるのですが、以下のような課題もあったためサーバ上にブランチ専用の環境を作成するようにしました。

  • 複数Issueの開発が並行で走るので、featureブランチ毎に独立した環境が必要
  • リモートワークをしているメンバーもいるため、インターネット上に環境が必要
  • 開発者の手間をかけず、自動での構築が必要

仕組みの概要

GitHubへfeatureブランチをpushすると、WebhookでJenkinsのJOBを実行し、EC2上のテストサーバにfeatureブランチ用のアプリをデプロイします。 アプリは、Nginx + Rails(Unicorn)で構成しています。デプロイ時には、baseとなるNginx/Unicornの設定ファイルをコピーし、featureブランチ用に一部を書き換えます。(DBは共通にしています)

f:id:lcl-engineer:20170326154533p:plain

2回目以降のPUSHは既に環境が作成されているため、新たに環境は作成されず、Gitからコードをpullし既存の環境を最新化するようにしています。

仕組みの構築にあたっては、以下の記事を参考にさせて頂きました。

URL

あらかじめ、AWS Route53に「 *.yyy.zzz 」というワイルドカード利用したドメインを割り当てておき、テストサーバのIPを設定しておきます。 ワイルドカード部分にブランチ名を設定し、ブランチ毎に以下のようなURLとなるルールにしています。

https:// branch-123.yyy.zzz/

なお、ドメインにアンダースコアは利用できないため、ブランチ名にアンダースコアが含まれている場合は、ハイフンなどの別の文字へ置換する必要があります。

Nginx設定ファイル

ベースとなる設定ファイルをコピーし、server_nameには各ブランチのドメインを設定します。

server_name  base.yyy.zzz;
↓
server_name  branch-123.yyy.zzz;

Nginx -> Unicornとのsocket通信のパスもブランチ毎に固有にするため書き換えます。

server unix:/tmp/base.sock
↓
server unix:/tmp/branch-123.sock

Unicorn設定ファイル

Unicornもベースとなる設定ファイルをコピーし、nginxとのソケットのパスを書き換えます。

listen "/tmp/base.sock", :backlog => 64
↓
listen "/tmp/branch-123.sock", :backlog => 64

Railsアプリ

該当のブランチからコードをcloneし、bundle install / assets precompileなどの通常のRailsアプリのデプロイ手順通りにデプロイします。

全ての環境が揃った段階で、Nginx/Unicornを起動すると環境へのアクセスが可能となり、ブランチ専用URLをチャットへ通知します。

環境作成のタイミング

当初は、hubotで任意に指定した場合のみ、環境を作成することも考えましたが、一手間かかってしまうためGitHubへのpushで環境を作成するようにしました。 GitHubへのpushで環境を作成すると、要不要問わず全てのブランチの環境が作成されてしまうので、後述する環境削除が重要となります。

環境削除のタイミング

この仕組みの場合は、環境がいくつかもできてしまうので、放置しているとサーバリソースをすぐに圧迫してしまいます。当初は、最大N個の環境のみ保持し、更新されていないものを自動削除するという運用にしていました。ただし、レビュー期間が長い案件の場合は「テストURLが見れなくなっている」とう状態になり、その都度もう一度環境を作り直す必要がありました。

現在では、該当ブランチがmasterへマージされたことをトリガーに、環境を削除するようにしています。masterへのマージ待ちブランチが多いと、リソースを圧迫してしまうため、N個を超えたらチャットへ通知し、人が不要な環境を判断して削除します。この運用も完璧ではないですが、人であればレビュー中のブランチは削除しないなど、精度の高い判断ができるので当初の問題は解消できました。

まとめ

現在の仕組みでうまく周っているため満足度は高いのですが、実装がシェルを駆使した泥臭い仕組みになってしまってまい柔軟性に欠けています。Docker等を利用すれば、もう少しシンプルな仕組みにできそうなので、今後取り組んでみたいと思っています。