Top View


Author Yuhei Okazaki

IP制限付き社内システム用のSlack App(Slash Commands)を作った

2020/01/15

仕様

Slackで /nicole というコマンドが使えるようになります。

slack app

コマンドは以下の形式で入力できます。

/nicole [good | normal | bad] [今日の一言]

例: 
/nicole good 今日は給料日なので気分が良い!
/nicole normal 普通です。
/nicole bad 朝から頭が痛いので「悪い」です。。。

例えば

このように入力すると

このように登録されます。

インフラ構成

Slackからの通知を受けるエンドポイントをAWS SAMで構築しています。

最終的に入力内容をSQSに登録している点がPOINTです。

Nicoleは社内システムのためIP制限がかかっており、エンドポイントを作ってもSlackからアクセスができません。 このため、AWS SAMで作ったエンドポイント(Api Gateway)→Lambda→SQSという順番でデータを流し、Nicole側からSQSをポーリングしています。

アプリケーション

クラウド側(Lambda)

Slackから送信されたデータは lambda_handlerevnet['body'] に格納されています。これはURL-encoded form dataなので、デコードが必要です。

デコードが完了したら token が正しいものであるかチェックした後、コマンド文字列をパースして入力内容をJSON化します。

JSONをSQSへsendしたらLambdaの処理は完了です。

require 'uri'
require 'date'
require 'json'
require 'aws-sdk'

MOOD_REGEX = /\A[a-z]*/

def lambda_handler(event:, context:)
  params = Hash[URI::decode_www_form(event['body'])]
  return result(404) unless params['token'] == ENV['SLACK_TOKEN']
  return result(200, '気分が未入力です') if params['text'].nil?

  slack_user, date, mood, message = parse_params(params)
  return result(200, '気分が正しくありません') unless ['good', 'normal', 'bad'].include?(mood)
  
  begin
    send_niko(slack_user, date, mood, message)
  rescue Aws::SQS::Errors::NonExistentQueue => e
    return result(200, "登録に失敗しました\n#{e}")
  end

  result(200, "[ 本日の気分: #{mood}, 一言: #{message} ] を登録しました。良い一日を!")
end

def result(status, message='')
  { statusCode: status, body: message }
end

def parse_params(params)
  mood = params['text'].slice(MOOD_REGEX)
  message = params['text'].gsub(MOOD_REGEX, '').gsub(/\A\s/, '')
  slack_user = params['user_name']
  date = Date.today.strftime('%Y-%m-%d')
  message ||= '未入力'
  [slack_user, date, mood, message]
end

def send_niko(slack_user, date, mood, message)
    sqs = Aws::SQS::Client.new(region: 'ap-northeast-1')
    send_message_result = sqs.send_message({
      queue_url: sqs.get_queue_url(queue_name: 'nicole-stack-app-CreateNikoQueue-xxxxxxxxxxxx').queue_url, 
      message_body: { slack_user: slack_user, date: date, mood: mood, message: message }.to_json,
    })
end

社内システム側(Shoryuken)

NicoleはRailsで実装されています。RailsからSQSを簡単にポーリングするために ShoryukenというGemを使いました。

ActiveJobのようにSQSからデータをreceiveしたときの処理を記述できます。

class CreateNikoJob < ApplicationJob
  include Shoryuken::Worker

  shoryuken_options queue: Rails.application.credentials.aws[:queue_name],
                    auto_delete: true,
                    body_parser: :json,
                    batch: true

  def perform(_message, json)
    json.each do |record|
      user = User.find_by(slack_user: record['slack_user'])
      next if user.nil?

      niko = user.nikos.find_or_initialize_by(date: Time.zone.parse(record['date']))
      niko.mood = record['mood'].to_sym
      niko.message = record['message']

      if niko.new_record?
        niko.niko_histories.build(user: user, action: :create_niko)
        niko.save!
        notify_niko(niko)
      else
        niko.niko_histories.build(user: user, action: :update_niko)
        niko.save!
      end
    end
  end

  private

  def notify_niko(niko)
    notifier.post(
      text: "気分が投稿されました",
      attachments: [niko_format(niko)]
    )
  end

  def notifier
    Slack::Notifier.new(webhook_url)
  end

  def webhook_url
    Rails.application.credentials.send(Rails.env)[:slack_webhook_url]
  end

  def niko_format(niko)
    {
      color: '#FFBE0B',
      author_name: niko.user.name,
      author_link: Rails.application.routes.url_helpers.user_url(niko.user),
      title: "#{I18n.l(niko.date.to_date)}の気分: #{niko.mood_i18n}",
      text: niko.message + "\n" + Rails.application.routes.url_helpers.niko_url(niko)
    }
  end
end

まとめ

IP制限付きの社内システムはSlack Appに対応することが難しいと思っていましたが、実際に作ってみると簡単に実現できました。小さめのAPIを作るのはやっぱりAWS SAMが便利ですね。

Fusicには他にも運用中の社内システムが多数あるので、いろいろな操作をSlackでできるようになると便利だなと思いました。

Yuhei Okazaki

Yuhei Okazaki

Twitter X

2018年の年明けに組込み畑からやってきた、2児の父 兼 Webエンジニアです。 mockmockの開発・運用を担当しており、組込みエンジニア時代の経験を活かしてデバイスをプログラミングしたり、簡易的なIoTシステムを作ったりしています。主な開発言語はRuby、時々Go。