RubyでQiitaの投稿をSlackに通知する


概要

社内メンバーのQiita投稿をSlackに通知する仕組みをRubyとAWS lambdaで実装する機会があったので、その手順をまとめます。

おおまかな仕組みとしては、


  1. 会社のQiitaページのフィードを解析し、1時間以内の投稿を取得する

  2. Slackに通知する

  3. それを1時間ごとに実行する

という形式をとりました。


環境

Ruby 2.5

slack-notifier 2.3.2


ディレクトリ構成

.

├── Gemfile
├── Gemfile.lock
├── lambda_function.rb
└── vendor
└── bundle


1時間以内の投稿を取得する

投稿の取得にはフィードを利用します。フィードには記事の著者や投稿日、更新日といった情報が記載されているので、フィードを解析することで、その投稿が1時間以内になされたものかを判定することができます。


フィードの取得

QiitaではAtomという形式でフィードを配信しており、Organizationのフィードは、OrganizationのURL に/activities.atom を加えることで取得できます。

http://qiita.com/organizations/<NAME_ORGANIZATION>/activities.atom

参考:Qiita 記事やユーザのフィード URL(フォローしたいユーザーやタグのXML/ATOMのURL)


フィードの解析

フィードの解析にはRubyの標準ライブラリであるrssライブラリが利用できます。

RSS::Parser.parseにパースしたいフィードを渡すことで、解析した結果のオブジェクトを返してくれます。

require 'rss'

atom_organization = "http://qiita.com/organizations/<NAME_ORGANIZATION>/activities.atom"
atom = RSS::Parser.parse(atom_organization, false)

parseメソッドの第二引数にfalseを指定していますが、これはパースする際にバリデーションを行わないことを意味します。現状Qiitaが提供しているAtomはAtomの構文に完全に即していないのかバリデーション付きでパースを行うとRSS::MissingAttributeErrorが発生します。

entriesにフィードの全ての投稿が入るので、eachで1件ずつ取り出し、投稿日時が1時間以内である記事のURLを全て取得します。

require 'rss'

require 'time'

links = []
current_time = Time.now

atom.entries.each do |entry|
published_time = entry.published.to_s.gsub(/<.+>(.+)Z<.+>/, '\1')
published_time = Time.parse(published_time)
links.push(entry.link.href) if published_time >= current_time - 3600
end

entry.published.contentとしてやれば、gsubやTime.parseを使わなくても直で日時を取得できるのですが、タイムゾーンがUTCになります。そのため、正しい時刻から(日本の場合)9時間ずれることになるので、私は上記のよう方法をとりました。

(ここら辺に関してはもっとスマートな方法がありそうです…)


Slackに通知

slack-notifierというgemを使うことで、簡単にSlackへ通知をすることができます。

https://github.com/stevenosloan/slack-notifier

まずは事前準備として、以下の2つが必要です。


  • gem slack-notifierのインストール


Gemfile

gem 'slack-notifier'


下記のようにすることで、先ほど取得したQiita記事のリンクをSlackに通知することができます。

require 'slack-notifier'

notifier = Slack::Notifier.new('<取得したWebhook URL>')
notifier.ping('<Qiita記事のリンク>', unfurl_links: true)

unfurl_links: trueはリンクのプレビューを表示するための引数です。無くても問題ありません。

参考:slack-notifierでrailsアプリからslackへ通知


定期実行

ここまでで①会社のQiitaページのフィードを解析し、1時間以内の投稿を取得すること②その投稿のURLをSlackに通知することができました。最後に①と②を1時間ごとに実行できれば、社内メンバーのQiita投稿をSlackに通知する仕組みの完成です。


AWS lambda

定期実行の方法としては、概要でも述べたようにAWS lambdaを使用しました。AWS lambdaとは、関数を登録しておくことで、クラウド上で自動的にコードを実行してくれるサービスです。それ以上の深い説明は私にはできません...


関数の作成

まずは、ローカルでAWS lambdaに登録するSlack通知の関数を作成します。

関数を書くのはAWS lambdaのコンソールで行うこともできますが、Lambdaに標準でインストールされていないライブラリ(今回の場合slack-notifier)を使うには、ローカルでパッケージングしたものをデプロイする必要があります。

私の場合、以下のようなプログラムにしました。


lambda_function.rb

require 'slack-notifier'

require 'rss'
require 'time'

def get_recent_links(atom, current_time)
links = []

atom.entries.each do |entry|
published_time = entry.published.to_s.gsub(/<.+>(.+)Z<.+>/, '\1')
published_time = Time.parse(published_time)
links.push(entry.link.href) if published_time >= current_time - 3600
end

return links
end

def slack_notifier(message)
#環境変数SLACK_WEBHOOK_URLはlambdaのコンソールで設定します
notifier = Slack::Notifier.new(ENV['SLACK_WEBHOOK_URL'])
notifier.ping(message, unfurl_links: true)
end

def lambda_handler(event:, context:)
#同じくTARGET_ATOM_URLはlambdaのコンソールで設定します
atom_organization = ENV['TARGET_ATOM_URL']
atom = RSS::Parser.parse(atom_organization, false)
current_time = Time.now
qiita_links = get_recent_links(atom, current_time)
return if qiita_links.empty?

message = <<~EOS
[Qiita新着投稿]
Qiitaに新着投稿がありました。
#{qiita_links.join("\n")}
EOS

slack_notifier(message)
end



デプロイ

デプロイ方法については以下の2つが参考になりました。私と同じくAWSに慣れていない方は、zipに圧縮してコンソールからアップロードするというやり方が一番わかりやすいかと思います(以下はこの方法をとった前提で進めます)。

Ruby support for AWS Lambdaを使ってみる

AWS Lambda の Ruby ランタイムを試す

リンク先にも書かれていますが、AWS LambdaのRubyバージョンは2.5です(2019年6月現在)。開発側のRubyのバージョンが2.5でないとデプロイを行ってもrequireするときにエラーになります。


環境変数の設定

デプロイ後は環境変数の設定をします。

関数のエディタの下に設定できる欄があるので、プログラム中で用いている2つのURLに加え、タイムゾーンをJSTに変更するため、TZの値をAsia/Tokyoに設定します。

参考:AWS Lambdaのタイムゾーン変更

image.png


トリガーの設定

トリガーは画像中の左側のリストからCloudWatch Eventsを選択します。

image.png

すると下側にトリガーの設定が表示されるので、新規ルールの作成を選択し、1時間ごとに実行するよう設定します。

image.png

設定が完了したらトリガーを追加し、関数を保存して完成です。

参考

【AWS】lambdaファンクションを定期的に実行する