Mattermostにページの更新情報を飛ばすBot的なのを作った

### はじめに
最近、職場でOSSの環境を乱立しまくっているのですが、そのひとつとしてMattermostも立ち上げました。釈迦に説法かとは思いますが、Mattermostは早い話が、オンプレミスで動くSlackです。うちもセキュリティが厳しいので、流行りのツールを使おうにもオンプレで動くものでないと使えなかったりします。
[SlackクローンのMattermostを使ってみる - 導入、初期設定編](https://hatenablog-parts.com/embed?url=https://qiita.com/terukizm/items/4a4016d8ec5a21856e4f)
で、立ち上げたのはいいのですが、早速言われたのが、「これ何に使うの？」。うちは独自のチャットツールがあるので、わざわざMattermostを使わなくてもいいじゃんという考えの人が多いようです。そこで、よくチェックする記事を自動で呟いてくれるbotを作れば需要があるんじゃなかろうかということで作りました。※botではなく、単にスクリプトを組んだだけです。
使っている環境はMattermostとJenkinsとpythonです。構成としてはこんな感じになっています。</p>
<img class="hatena-fotolife" title="f:id:T-N-Clark:20180614000213p:plain" src="https://cdn-ak.f.st-hatena.com/images/fotolife/T/T-N-Clark/20180614/20180614000213.png" alt="f:id:T-N-Clark:20180614000213p:plain" width="498" />
即興で作ったのでガバガバなのはご愛嬌ということで。Jenkinsはただのスケジューラーとして使っているので、こうして見返してみるとcronで十分だと思います(使ったことがないので確証はありませんが)のでJenkinsである必要ないですね。すでにJenkinsの環境は構築してあったし、Mattermostと繋げてジョブに失敗したときに通知が飛ばせるから多少なりともメリットはあるはず(震え声)。
今回はこちらのTwitterアカウントの更新情報を取得させていただくことにします。機械学習などの最新の論文情報をまとめてくださっているサイトです。アブストの日本語訳などもあるので、さっとチェックしたいときにとても参考になります。

[arXivTimes (@arxivtimes) | Twitter](https://hatenablog-parts.com/embed?url=https://twitter.com/arxivtimes)

### 実装
#### RSS情報の取得 
では、早速実装していきます。まずは、Twitterの更新情報を取得します。色々方法はあると思うのですが、今回はこちらを利用しました。
<a href="http://twitrss.me/">TwitRSS.me - rss of twitter user feeds by screenscraping with perl</a>
#### Mattermostの設定
次に、Mattermost側で情報を受け取るURLを用意しておきます。今回は情報を受け取るだけなので、内向きウェブフックを使います。
[SlackクローンのMattermostを使ってみる - 外部連携編 -（WebHooks、Hubot） - Qiita](https://hatenablog-parts.com/embed?url=https://qiita.com/terukizm/items/4524249dd7f1298fdc06)
#### pythonでの加工/Mattermostへの送信
いよいよ、RSSをpythonで取得、データベースに保存、json形式に変換、Mattermostに送信部分を組んでいきます。長いので処理内容毎に分けて説明します。とりあえず、動くことだけしか考えてないので出来の悪い実装は適宜修正してください。ベースはこちらのサイトを参考にさせていただきました。
[Python+pandasを使ってRSSフィードを取得→Mattermostに投稿＆DBに保存 - Qiita](https://hatenablog-parts.com/embed?url=https://qiita.com/some-nyan/items/e35461129a480e0caede)
こちらを参考にTwitterのRSSの仕様を読み解いて、必要情報を抜き出します。
[【データサイエンスの基礎】pythonでRSSからデータ収集 - Qiita](https://hatenablog-parts.com/embed?url=https://qiita.com/shunsuke227ono/items/da52a290f78924c1f485)
今回必要なのは、タイトルとリンクURLと(各ツイートに対してユニークな情報)です。プロキシの設定が必要な場合はこちらが参考になります。
[PythonのWebスクレイピングでproxy経由でのhttpsアクセスがrequestsを使ったら簡単だった - Qiita](https://hatenablog-parts.com/embed?url=https://qiita.com/koharite/items/731fcf5146c7b0c4e800)
requestライブラリについては、こちらをどうぞ
[Requests の使い方 (Python Library) - Qiita](https://hatenablog-parts.com/embed?url=https://qiita.com/sqrtxx/items/49beaa3795925e7de666)
取ってきたRSSの情報をデータベースに格納します。今回はそこら辺はサボってCSVに保存しています。DBを使う場合は先ほども上げたこちらのサイトに書かれています。
[Python+pandasを使ってRSSフィードを取得→Mattermostに投稿＆DBに保存 - Qiita](https://hatenablog-parts.com/embed?url=https://qiita.com/some-nyan/items/e35461129a480e0caede)
全体通して書くとこうなります。

```Python
import requests,feedparser
import pandas as pd
import os
import json

def getNewFeed(rss_data,already_printed_feeds,filepath):
    feeds_info = []
    feed = feedparser.parse(rss_data.content)
    entries = pd.DataFrame(feed.entries)
    if already_printed_feeds.empty:
        #全て新着Feed
        new_entries = entries
    else:
        #既出のFeedは除く
        new_entries = entries[~entries['id'].isin(already_printed_feeds['id'])]
    if not new_entries.empty: #新着Feedがあれば
        for key, row in new_entries.iterrows():
            title = row['title'].split('http')[0]
            #Mattermostに投稿されるメッセージ.ここではmarkdown形式でリンクになるように書いている.
            feedinfo = '[**%s**](%s)' % (title, row['link'])
            feeds_info.append(feedinfo)
        #新着データがあれば既存のリストに追加する
        already_printed_feeds = already_printed_feeds.append(new_entries)
        #データベース(csv)に保存
        if os.path.exists(filepath):
            new_entries.to_csv(filepath,encoding='utf-8',mode='a',header=False)
        else:
            new_entries.to_csv(filepath,encoding='utf-8')
    else: #新着Feedが無ければ
        print('not found new entries')
    return feeds_info

#既出のfeed情報の取得
def getAlreadyPrintedFeeds(filepath):
    if os.path.exists(filepath):
        already_printed_feeds = pd.read_csv(filepath)
    else:
        already_printed_feeds = pd.Series()
    return already_printed_feeds

def setPostMessage(feedinfo,username):
    payload = {
        'text':feedinfo,
        'username':username,
    }
    return payload

#データベース(csv)へのパス
filepath = 'entries.csv'
#proxyの設定
proxies = {
    'http':'http://id:passward@proxyadress:port',
    'https':'http://id:passward@proxyadress:port'
}
#RSSFeed取得先のURL
url = 'http://twitrss.me/twitter_user_to_rss/?user=arxivtimes'
rss_data = requests.get(url,proxies=proxies)
already_printed_feeds = getAlreadyPrintedFeeds(filepath)
feedinfo = getNewFeed(rss_data,already_printed_feeds,filepath)

#Mattermostの内向きウェブフック
mattermosturl = 'http://localhost/hooks/***'
#Mattermostのつぶやき時に表示される名前(好きな名前をつける)
username = 'FeedBot'
header = {'content-Type':'application/json'}
#新着Feedを順にMattermostに投げる
for i in range(len(feedinfo)):
    payload = setPostMessage(feedinfo[i])
    resp = request.post(mattermosturl,
                        header=header,
                        data=json.dumps(payload))
```

#### Jenkinsの設定
最後にこれを定期的に実行するためにJenkinsで実行スケジュールを組みます。と言ってもやることは単純で1時間に1回ジョブを実行させるだけです。確か今回採用しているRSSの取得方法が1時間単位に更新された気がするのでこんなもんでいいでしょう。こちらを参考にさせていただきながら、スケジュールを設定していきます。
[[Jenkins] ビルドトリガ（定期的に実行）設定についてのまとめ - Qiita](https://hatenablog-parts.com/embed?url=https://qiita.com/koara-local/items/79cb9c08e77ac9d94b1d)
ということで最低限飛ばせるようになりました。
#### 複数ページのRSSに向けて改良
ただ、このままじゃ複数ページから取ってこれないのでもうちょっとだけいじります。といってもurlをリストにして順々に実行するだけです。

```Python
import requests,feedparser
import pandas as pd
import os

def getNewFeed(rss_data,already_printed_feeds,filepath):
    feeds_info = []
    feed = feedparser.parse(rss_data.content)
    entries = pd.DataFrame(feed.entries)
    if already_printed_feeds.empty:
        #全て新着Feed
        new_entries = entries
    else:
        #既出のFeedは除く
        new_entries = entries[~entries['id'].isin(already_printed_feeds['id'])]
    if not new_entries.empty: #新着Feedがあれば
        for key, row in new_entries.iterrows():
            title = row['title'].split('http')[0]
            #Mattermostに投稿されるメッセージ.ここではmarkdown形式でリンクになるように書いている.
            feedinfo = '[**%s**](%s)' % (title, row['link'])
            feeds_info.append(feedinfo)
        #新着データがあれば既存のリストに追加する
        already_printed_feeds = already_printed_feeds.append(new_entries)
        #データベース(csv)に保存
        if os.path.exists(filepath):
            new_entries.to_csv(filepath,encoding='utf-8',mode='a',header=False)
        else:
            new_entries.to_csv(filepath,encoding='utf-8')
    else: #新着Feedが無ければ
        print('not found new entries')
    return feeds_info

#既出のfeed情報の取得
def getAlreadyPrintedFeeds(filepath):
    if os.path.exists(filepath):
        already_printed_feeds = pd.read_csv(filepath)
    else:
        already_printed_feeds = pd.Series()
    return already_printed_feeds

import json
def setPostMessage(feedinfo,username):
    payload = {
        'text':feedinfo,
        'username':username,
    }
    return payload

def postForMattermost(feedinfo):
    #Mattermostの内向きウェブフック
    mattermosturl = 'http://localhost/hooks/***'
    #Mattermostのつぶやき時に表示される名前(好きな名前をつける)
    username = 'FeedBot'
    header = {'content-Type':'application/json'}
    #新着Feedを順にMattermostに投げる
    for i in range(len(feedinfo)):
        payload = setPostMessage(feedinfo[i])
        resp = request.post(mattermosturl,
                            header=header,
                            data=json.dumps(payload))

def main():
    #proxyの設定
    proxies = {
        'http':'http://id:passward@proxyadress:port',
        'https':'http://id:passward@proxyadress:port'
    }
    #データベース(csv)へのパス
    filepath = 'entries.csv'
    already_printed_feeds = getAlreadyPrintedFeeds(filepath)
    
    #RSSFeed取得先のURL
    urls = ['http://twitrss.me/twitter_user_to_rss/?user=arxivtimes',
            'http://twitrss.me/twitter_user_to_rss/?user=a_i_news',
            'http://twitrss.me/twitter_user_to_rss/?user=ai_m_lab'
    ]

    for i in range(len(urls)):
        #RSS情報の取得
        rss_data = requests.get(urls[i],proxies=proxies)
        #データベースへの登録
        feedinfo = getNewFeed(rss_data,already_printed_feeds,filepath)
        #Mattermostへの送信
        postForMattermost(feedinfo)

if __name__ == "__main__":
    main()
```

多少整理もしたのでさっきよりは見やすくなったのではないでしょうか。Cの頃の癖なのか気づくと思考停止でインデックスでforループ回してますね。これはひどい。
長いわりに稚拙な内容でしたが参考になれば幸いです。
### 後半で追加したサイト
- [人工知能・機械学習ニュース(@A_I_News)さん | Twitter](https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftwitter.com%2FA_I_News%3Flang%3Dja)
- [人工知能,機械学習関係ニュース研究所(@AI_m_lab)さん | Twitter](https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftwitter.com%2FAI_m_lab%3Flang%3Dja)