最新のハッカソンの開催情報を自動で集めて、お知らせするBotを作ったので頭の中を紹介

# ハッカソンポータル
新着のハッカソン・ゲームジャム・アイディアソン・開発合宿の情報を自動的にお知らせしているBotです。よかったらフォローしてください!!

 * Twitter: https://bit.ly/2O1gmYi
 * Qiita: https://bit.ly/2CyqzWH
 * Googleカレンダー: https://bit.ly/34Y359T
 * Blogger: https://bit.ly/2CuDGIw

ちなみに頭の中(ソースコード)はこちらで公開しています。
[hackathon_portal](https://github.com/TakuKobayashi/hackathon_portal)

QiitaやGoogleカレンダーの方がよりまとまった情報として紹介しています。
また、現在、日本で開催する情報を中心にお知らせしていますが、海外でこのような情報を収集できるサイトがあり、ニーズがありそうでしたら導入を検討します。

# ボードゲームマニア
こちらは上記ハッカソンポータルの技術とほとんど同じことをして、違うコンセプトのものを作りました。
こちらはボードゲーム・アナログゲームについて近日開催されるイベントを紹介しているBotです。こちらもよかったらフォローしてください!!

 * Twitter: https://twitter.com/boardgamemania

本家サイト: [BoardGameMania](https://www.boardgame-mania.click/)
頭の中(ソースコード)はこちらです。
[board_game_mania](https://github.com/TakuKobayashi/board_game_mania)

[ボードゲームマニア](https://www.boardgame-mania.click/)ではTwitterのみで紹介していますが、ご要望がありそうでしたら、QiitaやGoogleカレンダー連携したものも作成します。

技術的にはほとんど同じことをしています。
以降に技術的な解説もしていきます。

# 作成目的
## インスパイア
[2017年のハッカソン開催情報まとめ！](https://qiita.com/satoshi0212/items/178c56c3ae28dd4c3252)
こちらの記事では手動で集めてくださっていました。そこで、「これ、自動的にデータを収集して、お知らせできるようにしたらいいのでは?」と考え、作成することにしました。

## 方針・コンセプト
 1. <font color="#FF0000"><b>自動化</b></font> 手動で情報を収集、公開していたのでは膨大な労力がかかり、運用コストが大きいため、継続的に情報を発信し、続けることが難しくなってしまいます。そのため、この問題の解決のため、ほぼ自動で情報を収集、発信することができるようにしました。
 2. <font color="#FF0000"><b>定常化</b></font> 情報は定期的に更新されるため、定期的に最新の情報を取得し、お知らせし続けられるようにする。
 3. <font color="#FF0000"><b>省コスト</b></font> 個人でやっていくので、サーバーなどを持って運用すると、サーバー費用がかかてしまい継続的に運用することが難しくなります。(お金かけたくないし...)　そのため、既存のWebサービスをフル活用することで、サーバーへのコストや負荷を抑え、サーバーを立てなくても運用できる形にしました。(実際は後述しますが常時運用のためサーバーを使用しています)

# 使用しているツール
### 使用しているツール
 * Ruby
 * Rails
 * Whenever
 * Nokogiri
 * MySQL

### 情報を集めているサービス

 * [Connpass](https://connpass.com/)
 * [Doorkeeper](https://www.doorkeeper.jp/)
 * [Peatix](https://peatix.com/)
 * [Twitter](https://twitter.com/search?q=hackathon%20OR%20%E3%83%83%E3%82%AB%E3%82%BD%E3%83%B3%20OR%20gamejam%20OR%20%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A3%E3%82%A2%E3%82%BD%E3%83%B3%20OR%20%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A2%E3%82%BD%E3%83%B3%20OR%20ideathon%20OR%20%E9%96%8B%E7%99%BA%E5%90%88%E5%AE%BF%20OR%20%E3%81%AF%E3%81%A3%E3%81%8B%E3%81%9D%E3%82%93&src=typed_query&f=live)
 * [Devpost](https://devpost.com/hackathons)
 * [Eventbrite](https://www.eventbrite.com/)
 * [Itch.io](https://itch.io/jams)

このほかに追加で収集してほしいサービスとかありましたら、issueなどで述べてください。また、管理ツールもあります。管理ツールを使ってみたい!!など、希望の方は個別にご連絡ください。

### 告知しているサービスおよびAPI
 * Twitter API
 * Qiita API
 * Google Calendar API
 * Google Blogger API

# ランニングコスト
## 使用環境
 * AWS LightSail $5/月 プラン環境
 <img width="1333" alt="aws_lightsail_price.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/31a876e4-f56f-a6cc-33fe-6b67dacd7365.png">
 * OS: Ubuntu 18.04 LTS

## 解説
### サーバーを立てる経緯
上記、ハッカソンポータルとボードゲームマニア他と共通のサービスを1つのサーバーで運用していまします。
サーバーを利用した理由として、常駐させる場合、自宅のデスクトップPCの電源を常にONにするので、電気代がかさむのと以下のルーチンのバッチ処理を実行中の時、CPU負荷が高くなり、一時的にPCがもっさりすることがあるので専用のサーバーを立てることにしました。

### 環境について
AWS Lightsailの5/月ドルにした理由は余計なデータをMySQLに保存しすぎてしまったためストレージが20GB SSDでは足りなかったためです。今後データを整理すれば、 3.5/月ドルのプランに置き換えるかもしれません。
(なお、3.5/月ドルで十分に稼働して、運用できることはすでに確認しました。)
以前はさくらのVPS、CentOS6の環境で運用していましたが、AWS Lightsailの方が安いのと、CentOSよりもUbuntuの方がほかの各種ライブラリとの親和性(Dockerとか)が高いため置き換えました。

### Ubuntu 18.04 LTSの導入方法
AWS Lightsailでは初期設定ではUbuntu 16.04 LTSのみ導入することができます。AWS Lightsailでインスタンスを作成した後、sshでログインし、

```
do-release-upgrade 
```
と入力することでUbuntuのOSをUbuntu 18.04 LTSにアップグレードすることができます。

# 技術的な部分の紹介
## 基本的なルーチン
基本的に2種類のバッチを毎日定時に実行しています。

 1. 主にイベント告知サイトで新しく募集されたイベント情報をWebAPIまたはHTMLスクレイピングして情報をデータベースに貯める(Input)
 2. データベースに貯まっているデータの中から未来に開催され、Twitter, Qiita, Google Calendar(今後発信先が増える場合があります)において、まだ情報を発信していないイベント情報のみ、新たに発信する(Output)

これらのバッチを1 → 2の順番に毎日一定時刻になったら実行するようにしています。
現在、11時に1.のバッチを実行し(バッチが全て実行し、終わっているはずの)19時に2.のバッチを実行し、告知しています。

## ハッカソンイベントなのかどうかの判別
### 表記ゆれのカバー
一言で「ハッカソン」と言っても表記は様々でした。「ゲームジャム」や「アイディアソン」もハッカソンの一種のため、集めた情報を発信する場合に精査して、発信するものを選別するようにしました。今回では、```"hackathon", "ッカソン", "jam", "ジャム", "アイディアソン", "アイデアソン", "ideathon", "合宿"```を精査対象とする条件としました。(詳しくは[こちら](https://github.com/TakuKobayashi/hackathon_portal/blob/master/app/models/event.rb))
全角、半角問題の区別は、[charwidth](https://github.com/labocho/charwidth)を使い置き換えることでカバーしています。ひらがな表記は意図しないものが多く取れるため除外しています。
使い方についてはこちらを参照してください。
[Ruby / Rails で全角・半角をいい感じに変換しよう!](http://penguinlab.jp/blog/post/2327)

### ハッカソンなどと関係なさそうなイベントは除去
タイトルだけで判別するとハッカソンイベントであってもハッカソンでないと判別されるケースが多く発生し、本文で判別すると、明らかにハッカソンイベントではないものをお知らせしてしまうケースが多くありました。そこで、本文の中に含まれるキーワードでスコアを算出し、一定のスコア以上のものをイベントをハッカソンイベントとするようにしました。
(詳しくは[こちら](https://github.com/TakuKobayashi/hackathon_portal/blob/master/app/models/event.rb))

## 自動投稿
情報を投稿するにはAPIを利用する必要があります。基本的には投稿するにはOAuth認証が必要で、そのためにはサーバーを立ててエンドポイント作る必要があります。しかし、今回、使用しているサービスではエンドポイントを作ることなく自動的に投稿するBotの形にしました。このような形を実装する方法を以下に紹介していきます。

### Twitterへの自動投稿
 * [Twitter App](https://apps.twitter.com/)へとアクセスし、Twitterアプリを開発するための登録をする
 * 作成されたアプリを選択し、「Keys and Access Tokens」を選択する。
<img width="1326" alt="KeyAndAccessToken.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/35934338-72af-a2b8-6828-b2dec39f0f75.png">
 * 作成されたアプリからAccessToken(とAccessTokenSecret)を生成する。
<img width="1335" alt="twitter_accesstoken.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/e5822dac-3d11-1a69-f9fa-b36e24840b57.png">

ここで生成された```Consumer Key, Consumer Secret, Access Token, Access Token Secret```を用いて```Twitter::REST::Client```を生成し、```update```メソッドを呼び出すことで、ツイートすることができます。
詳しくはこちらを参照してください。

 * [Twitter Gem](https://github.com/sferik/twitter)
 * [Rubyで簡単なTwitter クライアントをつくる](https://qiita.com/shimisunet/items/c3a0b93fb13fa82ed8be)

### Qiitaへの自動投稿
こちらで詳しく、記載があります。[Qiita gemを使ってみる！](https://qiita.com/Rock22/items/452f46bb0d09dc0a8077)

 * Qiita → 「設定」を開き、「アプリケーション」を選択する。
 * 個人用アクセストークンの中の「新しくトークンを発行する」を選択して、アクセストークンを発行する
 * 発行したアクセストークンを用いて、Qiitaに投稿する。

詳しくは[こちら](https://github.com/TakuKobayashi/hackathon_portal/blob/master/app/models/qiita_bot.rb)を参照してください。

### Googleサービス(Googleカレンダー)への自動投稿
ハッカソンの開催情報をGoogleカレンダーに投稿するときに使用しました。
Googleのサービスに投稿するためには、上記のようなものとは別に本来ならエンドポイントが必要なOAuth認証が必要です。今回はエンドポイントを作らないで[OAuth 2.0 Playground](https://developers.google.com/oauthplayground/)(Google APIにおけるOAuth認証を試し打ちするサイト)を使ってGoogle APIにPOSTする方法を紹介します。

 * [Google Cloud Console](https://console.cloud.google.com) → 「APIの概要に移動」
<img width="1336" alt="google_api_console.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/0a3188df-cedb-72ae-8e20-506f2c6c5c5f.png">
 * 「APIとサービスの有効化」を選択し、利用したいGoogleのサービスのAPIを有効にする。そして、「認証情報」を選択する
<img width="1335" alt="auth_and_api.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/a6838fc9-3c03-1c36-08e2-b6dc90b4460b.png">
 * 「認証情報を作成」  → 「OAuth クライアントID」を選択
<img width="1336" alt="create_oauth.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/418cd399-b769-897b-a1a0-02a411673480.png">
 * 「承認済みのリダイレクトURI」に```https://developers.google.com/oauthplayground```と入力し保存する
<img width="1335" alt="redirect_uri.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/98e7cdb5-366e-ff5e-ae75-f3f63568d2f1.png">
 * [OAuth 2.0 Playground](https://developers.google.com/oauthplayground/)にアクセスする。設定アイコンを選択し、商事するプルダウンの中の「OAuth Client ID」と「OAuth Client Secret」の中に先ほど作成したことでできた、「クライアントID」と「クライアントシークレット」をそれぞれ入力し、OAuth認証する、Googleのサービスを選択して、チェックを入れる。そうすることで、「Authrize APIs」のボタンが有効になるのでそのボタンを押してOAuth認証を開始する。
<img width="1292" alt="oauth_service.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/4bc8446b-8b62-443c-c532-03153d8c4a06.png">
 * OAuth認証が無事成功すると[OAuth 2.0 Playground](https://developers.google.com/oauthplayground/)に戻ってきてSTEP2の項目が自動的に埋まっている。この状態で、「Exchange authorization code for tokens」のボタンを押すと、Refresh tokenとAccess Tokenの項目に文字列が入力されます。<font color="#FF0000">Googleの場合、Access Tokenは時間とともに使えなくなるため、Refresh tokenの値をコピーして控えます。</font>
<img width="1336" alt="refresh_token.png" src="https://qiita-image-store.s3.amazonaws.com/0/72596/2c515d04-4809-249d-d06f-aea59d853da5.png">

以降はrubyにて[google-api-ruby-client](https://github.com/googleapis/google-api-ruby-client) を使った認証の処理になりますが、他の言語でも似たような形で実装できます。
rubyでは```Signet::OAuth2::Client```インスタンスの中に認証に必要な情報を詰め込みます。
以下のように、OAuth認証に必要なものは```クライアントID```,```クライアントシークレット```,そして先ほど控えた```refresh token```の値を指定することで、エンドポイントなしでOAuth認証することができます。<font color="#FF0000">(refresh tokenは名前の通り、新しいaccess tokenを作るためのものなので、期限内のaccess tokenがあるのなら、それを指定した方がいいです。)</font>

```ruby
oauth_client = Signet::OAuth2::Client.new
oauth_client.client_id = ENV.fetch("GOOGLE_OAUTH_CLIENT_ID", "")
oauth_client.client_secret = ENV.fetch("GOOGLE_OAUTH_CLIENT_SECRET", "")
oauth_client.authorization_uri = "https://accounts.google.com/o/oauth2/auth" 
oauth_client.token_credential_uri = "https://accounts.google.com/o/oauth2/token"
oauth_client.refresh_token = refresh_token
```
詳しいソースコードは[こちら](https://github.com/TakuKobayashi/hackathon_portal/blob/master/app/models/concerns/google_oauth2_client.rb)
そして、上記で情報を詰め込んだ```Signet::OAuth2::Client```インスタンスを以下のように```Google::Apis::CalendarV3::CalendarService```インスタンスの認証情報に指定します。

```ruby
require 'google/apis/calendar_v3'

service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = oauth_client
```
こうすることで、この場合、```Google::Apis::CalendarV3::CalendarService```インスタンスである```service```から、Googleカレンダーの各種APIを呼び出して利用することができるようになります。
あとはGoogleカレンダーにハッカソンの日程情報を書き込んで反映させていくことで、自動的にGoogleかれんだーに情報が書き込まれます。
詳しい処理は[こちら](https://github.com/TakuKobayashi/hackathon_portal/blob/master/app/models/event_calendar_bot.rb)を参照してください。


## 非公式API(HTML)へのアクセスと情報の取得と解析
[Peatix](https://peatix.com/)からイベント情報を取得するときに使用しました。
[Connpass](https://connpass.com/)や[Atnd](https://atnd.org/)など、
どのようなWebサイトにも当てはまるのですが、Webブラウザで表示できている以上、通信状況を解析して、通信を送る場合の法則性を見つけることができれば、非公開APIやHTMLを解析して情報を取得することができます。
こちらでやり方などを解説しています。
[画像クローラー](https://qiita.com/taptappun/items/051e7d9d57770c7cb718)
こちらと同様にブラウザのDeveloperToolのNetworkを開き、送られているリクエストの一覧を調べます。
![network_profile.png](https://qiita-image-store.s3.amazonaws.com/0/72596/056fa0ec-fb08-7a4e-da30-91636851eeaa.png)
このリクエストの中から該当のリクエストを探し当て、試し打ちを繰り返して、法則性を見つけ出すことで、非公式APIやHTMLからの情報を取得できるようにしています。
(具体的な処理については[こちら](https://github.com/TakuKobayashi/hackathon_portal/blob/master/app/models/peatix.rb)を参照)

# 今後の展望
 * 海外のハッカソンなど、インプット先のソースを増やしていきたい
 * ハッカソンやボードゲーム以外の情報においても、この技術を応用して、特定のジャンルの情報を自動的に発信し続けるものを作成したい
 * 囃しなどイベントが盛り上がるようなことも自動的にできるようにしてみたい

など、ほか、ご要望がありましたら申し付けてください。