毎日のお弁当注文FAXをTwilioで自動化する

僕の通っているオフィスでは社内の有志メンバーを募って仕出し弁当を注文していました。 毎朝10時ちょっと前までにFAXで注文しておけば、お昼休みまでにオフィスに届けてくれるという仕組みでした。 しかも、オフィスの前にやってくる弁当屋さんよりも、ちょっと安い!

しかし、毎朝注文を集計してFAXで注文するというのはなかなか大変。およそ人のやることではないということで自動化しました。

フローの設計

  • 注文の受付は Google スプレッドシート。Sheets V4 API を使って注文数を取得する。
  • HexaPDF で FAX する文章を生成
  • Twilio Programmable FAX で送信
  • Chatwork API で結果通知

今回は Ruby で実装しました。

Google スプレッドシートで注文管理

注文は Google スプレッドシートで管理していました。(横列方向に注文する人、縦列方向に注文日を記載)

Sheets V4 API を使い、毎日の注文数をこのようなスクリプトで集計していました。*1

require 'google/apis/sheets_v4'
require_relative 'google_authorize.rb'

service = Google::Apis::SheetsV4::SheetsService.new
service.authorization = google_authorize

t = Time.now
sheet_name = sprintf('%04d-%d', t.year, t.mon)

value_range = service.get_spreadsheet_values(SPREADSHEET_ID, "#{sheet_name}!A4:ZZ35", value_render_option: 'FORMATTED_VALUE')
values = value_range.values
header = values.shift
order_info = Hash.new
values.each do |row|
  day_header = row[0].scan(/^(\d+)/)[0]
  if day_header.nil?
    next
  end
  day = day_header[0].to_i
  orders = row[1]
  price = row[2]
  members = Array.new
  (3..(row.length-1)).each do |i|
    if row[i] != ''
      members << header[i]
    end
  end
  order_info[day] = {
    :day => day,
    :day_formatted => row[0],
    :orders => orders.to_i,
    :price => price.to_i,
    :members => members
  }
end

today_item = order_info[t.day]

HexaPDF で注文 FAX を生成

弁当屋さん規定のフォーマットを保存したPDFに、取得した注文数を書き込みます。この処理では HexaPDF というライブラリが使いやすかったです。

require 'hexapdf'

def generate_order_pdf(month, day, count)
  dir = File.dirname(__FILE__)
  pdf = HexaPDF::Document.open("#{dir}/order.pdf")
  
  page = pdf.pages[0]
  canvas = page.canvas(type: :overlay)
  canvas.font('Helvetica', size: 18)
  canvas.text(month.to_s, at: [178, 341]) # 月
  canvas.text(day.to_s, at: [225, 341]) # 日
  
  canvas.font('Helvetica', size: 38)
  canvas.text(count.to_s, at: [105, 245]) # Aセット普通
  canvas.text(count.to_s, at: [395, 245]) # 合計

  return pdf
end

pdf = generate_order_pdf(t.mon, t.day, today_item[:orders])

いきなりファイルには書き込まず HexaPDF の PDF ドキュメントオブジェクトを返して次の処理で使っています。

Twilio Programmable FAX を使って送信

ここで、以前午前休電話で使った Twilio の FAX サービス Twilio Programmable FAX を使います。

しかし、この API、いきなり送信する FAX の PDF を POST して受け付けてくれるわけではなく、Twilio が HTTP GET でアクセスできる URL に PDF ファイルを置いておき、その URL を指定する必要があります。なので、先に作成した PDF オブジェクトを今回は Google Cloud Storage にアップロードしておくことにしました*2

require 'google/cloud/storage'

GCS_PROJECT_ID = 'xxxxxx'
GCS_KEY_FILE = 'gcs_key.json'
GCS_BUCKET = 'xxxxxx.appspot.com'

storage_path = 'fax/' + Time.now.strftime('%Y%m%d%H%M%S') + '.pdf'

storage = Google::Cloud::Storage.new(project: GCS_PROJECT_ID, keyfile: GCS_KEY_FILE)
bucket = storage.bucket(GCS_BUCKET)
storage_file = nil
Tempfile.create() do |temp_file|
  pdf.write(temp_file)
  temp_file.seek(0)
  storage_file = bucket.create_file(temp_file, storage_path)
  storage_file.acl.public!
end

## ここで FAX 送信処理をする

# 送信が終わったらファイルを削除する
storage_file.delete

ファイルをアップロードした後はいよいよ、Twilio Programmable Fax で FAX を送信します。

まず、twilio.fax.faxes.create で FAX オブジェクトを作成し、その sid を取得します。以後、twilio.fax.faxes(sid).fetch で定期的に FAX オブジェクトを取得して結果を確認します。

require 'twilio-ruby'

TWILIO_ACCOUNT_SID = 'xxxxx'
TWILIO_AUTH_TOKEN = 'xxxxx'
TWILIO_FAX_FROM = '+8150xxxxxxxx'
TWILIO_FAX_TO = '+81xxxxxxxxx'

# 送信開始
twilio = Twilio::REST::Client.new(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
twilio_credential
fax = twilio.fax.faxes.create(from: TWILIO_FAX_FROM, to: TWILIO_FAX_TO, media_url: "https://storage.googleapis.com/#{GCS_BUCKET}/#{storage_path}")
fax_sid = fax.sid

# 結果待ち
while true do
  status = fax.status.downcase
  if status == 'delivered'
    chatwork.send("[info][title]FAX送信結果[/title]正常に終了しました[/info]")
    break
  elsif status == 'no-answer'
    chatwork.send("[info][title]FAX送信結果[/title]応答がありませんでした(puke)[/info]")
    break
  elsif status == 'busy'
    chatwork.send("[info][title]FAX送信結果[/title]電話中でした(puke)[/info]")
    break
  elsif status == 'failed'
    chatwork.send("[info][title]FAX送信結果[/title]失敗しました(puke)[/info]")
    break
  elsif status == 'canceled'
    chatwork.send("[info][title]FAX送信結果[/title]キャンセルされました(puke)[/info]")
    break
  end
  sleep 15
  fax = twilio.fax.faxes(fax_sid).fetch
end

送信結果の通知

送信結果は ChatWork に通知しました。以下のような簡易的なクライアントオブジェクトを作って使いました。

require 'faraday'

CHATWORK_ROOM_ID = 'xxxxx'
CHATWORK_TOKEN = 'xxxxx'

class ChatworkClient
  def initialize(credential)
    @credential = credential
    @conn = Faraday::Connection.new(url: 'https://api.chatwork.com') do |builder|
      builder.use Faraday::Request::UrlEncoded
      builder.use Faraday::Response::Logger
      builder.use Faraday::Adapter::NetHttp
    end
  end

  def send(message)
    @conn.post do |request|
      request.url "/v2/rooms/#{CHATWORK_ROOM_ID}/messages"
      request.headers = {
        'X-ChatWorkToken' => CHATWORK_TOKEN
      }
      request.body = {
        :body => message
      }
    end
  end
end

完成

これらの要素を元に組み上げたスクリプトを用意して、毎朝 8:45 に FAX を送信することができるようになりました。

最後に

残念ながら、届くお弁当が冷たい上にレンジできない容器に入っているというところで利用率が下がってしまい、この仕組みはもう運用が終了してしまいました。

しかし Programmable FAX は簡単に FAX ができて面白く、安いので、今後また何かに使えれば良いなと思っています。

*1:google_authorize.rb には https://developers.google.com/sheets/api/quickstart/ruby#step_3_set_up_the_sample の authorize メソッドが google_authorize という名前で書かれています

*2:空のGAEアプリをアップロードして、デフォルトバケットを作成して無料枠におさめています