Qiita記事をTwitterに投稿しSlackに通知するスクリプトをAWSで定期実行してみた

# 概要

以下の記事で__Pythonスクリプト__から各APIを操作する方法をを紹介しました。

- [__PythonでTwitter API を利用していろいろ遊んでみる__](https://qiita.com/bakira/items/00743d10ec42993f85eb)
- [__PythonでSlackのIncoming Webhookを試してみる__](https://qiita.com/bakira/items/8fa06ab10edf1f42ff97)

今回は、

1. __QiitaAPIを使って記事一覧からランダムで取得__
2. __TwitterAPIを使っていい感じに投稿する__
3. __Incoming Webhookを使って実行結果をSlackに通知する__
4. __AWS lambdaに乗せて実行をする__
5. __AWS CloutWatchで定期実行する__

という部分を実装してみようと思います。

# 手順

## 環境

- __Python 3.6.1__
- __requests-oauthlib==0.8.0__
- __requests==2.18.4__

## 前提

以下のものは__事前__に用意しておいてください。

- __Python関連__
    - requestsのインストール
    - requests-oauthlibのインストール
- __Twitter関連__
    - Twitterアカウント
    - Consumer Key
    - Consumer Secret
    - Access Token
    - Access Token Secret
- __AWS関連__
    - AWSアカウント
- __Qiita関連__
    - Qiitaアカウント
    - Qiita記事
- __Slack関連__
    - Incoming Webhookの設定
    - 通知先Chanel

また、今回は__リファクタリング等__はしていないので__<font color="red">冗長的なコーディング</font>__になってしまっている部分がありますが、__本質ではないので今回はそのままとさせていただきますorz__

## Twitterへの投稿

まずは__QiitaAPI__を利用して、__ランダムで投稿記事を取得__し、__Twitterに投稿__するところまで実装してみましょう。

```py:config.py
CONSUMER_KEY = "{コンシューマーキー}"
CONSUMER_SECRET = "{コンシューマーシークレット}"
ACCESS_TOKEN = "{アクセストークン}"
ACCESS_TOKEN_SECRET = "{アクセストークンシークレット}"  
```

```py:script.py
import json, config, requests, random, sys  #必要なライブラリの読み込み 
from requests_oauthlib import OAuth1Session  #OAuthのライブラリの読み込み

CONSUMER_KEY = config.CONSUMER_KEY
CONSUMER_SECRET = config.CONSUMER_SECRET
ACCESS_TOKEN = config.ACCESS_TOKEN
ACCESS_TOKEN_SECRET = config.ACCESS_TOKEN_SECRET
twitter = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) #認証処理

TWITTER_API_HOST = "https://api.twitter.com/1.1"  #TwitterAPIホスト
STATUSES_UPDATE_ENDPOINT = "/statuses/update.json"  #ツイートエンドポイント

TARGET_USER_NAME = "{Qiitaユーザー名}"  #取得対象のQiitaユーザー名
QIITA_API_HOST = "https://qiita.com/api/v2"  #QiitaAPIホスト
USER_ENDPOINT = f"/users/{TARGET_USER_NAME}"  #ユーザー情報取得エンドポイント
USER_ITEMS_ENDPOINT = f"/users/{TARGET_USER_NAME}/items"  #ユーザー記事一覧取得エンドポイント
PAGE_PER_COUNT = 20  #1ページ辺りの記事取得件数

response = requests.get(
    QIITA_API_HOST + USER_ENDPOINT
).json()  #対象Qiitaアカウント情報をJsonで取得

totalItemCount = response['items_count']  #投稿総数を取得
pageCount = totalItemCount // PAGE_PER_COUNT  #1ページ辺りの記事取得件数で取得出来たページ数を取得
hasSurplus = totalItemCount % PAGE_PER_COUNT != 0  #余りの記事数を取得
totalPageCount = pageCount + 1 if hasSurplus else pageCount #余りの記事数があった場合、端数分のページ数を追加

params = {
    "page": random.randint(1,totalPageCount),  #ページ番号をランダム生成
    "per_page": PAGE_PER_COUNT
}  #記事一覧をページ番号と１ページ辺りの表示件数を指定するためのパラメータを生成

response = requests.get(
    QIITA_API_HOST + USER_ITEMS_ENDPOINT,
    params=params
).json()  #対象Qiitaアカウントの記事一覧を取得

if( response == [] ):  #記事一覧が空だった場合
    print("アイテムを取得出来ませんでしたので終了します。")
    sys.exit()  #処理終了
    

item = random.choice(response)  #取得した一覧からランダムで記事を取得
url = item["url"]  #記事URLを取得
title = item["title"]  #記事タイトルを取得
tags = map(
    lambda element: ("#" + element["name"]), 
    item["tags"] 
)  #タグ一覧をTwitterハッシュ化して配列として再生成
tagsStr = ' '.join(list(tags))  #タグ一覧を文字列結合

params = {
    "status" : "【Qiita】" +title + "\n" + tagsStr + "\n" + url  #ツイート内容を生成
}  #Postするパラメータを生成

res = twitter.post( 
    TWITTER_API_HOST + STATUSES_UPDATE_ENDPOINT ,
    params = params
)  #APIにPost通信

if res.status_code == 200: #正常投稿出来た場合
    print("Success.")
else: #正常投稿出来なかった場合
    print("Failed. : %s"% vars(res))
```

## 確認

```shell:command
python script.py
```

<img width="601" alt="screencapture- 2018-01-15 22.57.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/f9a957cc-676b-c06c-3a61-a7a5b32bc478.png">

__それっぽくツイート出来てますね！__

ただ、Qiitaの__タグ__で「__.__」や「__-__」が含まれてると、__<font color="red">Twitterのハッシュタグがうまく動きません</font>__が、それは今回は__妥協__しましょうorz

## Slackへの通知

次に__処理結果をSlackに通知__するところを実装してみましょう。

```py:script.py
import json, config, requests, random, sys  #必要なライブラリの読み込み 
from requests_oauthlib import OAuth1Session  #OAuthのライブラリの読み込み

CONSUMER_KEY = config.CONSUMER_KEY
CONSUMER_SECRET = config.CONSUMER_SECRET
ACCESS_TOKEN = config.ACCESS_TOKEN
ACCESS_TOKEN_SECRET = config.ACCESS_TOKEN_SECRET
twitter = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) #認証処理

INCOMING_WEBHOOK_URL = config.INCOMING_WEBHOOK_URL

TWITTER_API_HOST = "https://api.twitter.com/1.1"  #TwitterAPIホスト
STATUSES_UPDATE_ENDPOINT = "/statuses/update.json"  #ツイートエンドポイント
TWITTER_USER_NAME = "Bakira_Tech_Bot"


QIITA_USER_NAME = "bakira"  #取得対象のQiitaアカウント名
QIITA_API_HOST = "https://qiita.com/api/v2"  #QiitaAPIホスト
USER_ENDPOINT = f"/users/{QIITA_USER_NAME}"  #ユーザー情報取得エンドポイント
USER_ITEMS_ENDPOINT = f"/users/{QIITA_USER_NAME}/items"  #ユーザー記事一覧取得エンドポイント
PAGE_PER_COUNT = 20  #1ページ辺りの記事取得件数

response = requests.get(
    QIITA_API_HOST + USER_ENDPOINT
).json()  #対象Qiitaアカウント情報をJsonで取得

totalItemCount = response['items_count']  #投稿総数を取得
pageCount = totalItemCount // PAGE_PER_COUNT  #1ページ辺りの記事取得件数で取得出来たページ数を取得
hasSurplus = totalItemCount % PAGE_PER_COUNT != 0  #余りの記事数を取得
totalPageCount = pageCount + 1 if hasSurplus else pageCount #余りの記事数があった場合、端数分のページ数を追加

params = {
    "page": random.randint(1,totalPageCount),  #ページ番号をランダム生成
    "per_page": PAGE_PER_COUNT
}  #記事一覧をページ番号と１ページ辺りの表示件数を指定するためのパラメータを生成

response = requests.get(
    QIITA_API_HOST + USER_ITEMS_ENDPOINT,
    params=params
).json()  #対象Qiitaアカウントの記事一覧を取得

if( response == [] ):  #記事一覧が空だった場合
    requests.post(
        INCOMING_WEBHOOK_URL, 
        data = json.dumps(
        {
           "attachments":[
              {
                 "fallback":"Qiita Article is Empty.",
                 "pretext":"Bot Result.",
                 "color":"#FF0000",
                 "fields":[
                    {
                       "title":"Qiita Item is Empty",
                       "value":"There was no Article"
                    }
                 ]
              }
           ]
        } 
    ))
    sys.exit()  #処理終了
    

item = random.choice(response)  #取得した一覧からランダムで記事を取得
url = item["url"]  #記事URLを取得
title = item["title"]  #記事タイトルを取得
tags = map(
    lambda element: ("#" + element["name"]), 
    item["tags"] 
)  #タグ一覧をTwitterハッシュ化して配列として再生成
tagsStr = ' '.join(list(tags))  #タグ一覧を文字列結合

params = {
    "status" : "【Qiita】" +title + "\n" + tagsStr + "\n" + url  #ツイート内容を生成
}  #Postするパラメータを生成

res = twitter.post( 
    TWITTER_API_HOST + STATUSES_UPDATE_ENDPOINT ,
    params = params
)  #APIにPost通信
if res.status_code == 200: #正常投稿出来た場合
    resDic = json.loads(res.text)
    id = resDic["id"]
    requests.post(
        INCOMING_WEBHOOK_URL, 
        data = json.dumps(
        {
           "attachments":[
              {
                 "fallback":"Tweet Success.",
                 "pretext":"Bot Result.",
                 "color":"#00FF00",
                 "fields":[
                    {
                       "title":"Tweet Success.",
                       "value":f"Tweet URL : https://twitter.com/{TWITTER_USER_NAME}/status/{id}"
                    }
                 ]
              }
           ]
        } 
    ))
else: #正常投稿出来なかった場合
    requests.post(
        INCOMING_WEBHOOK_URL, 
        data = json.dumps(
        {
           "attachments":[
              {
                 "fallback":"Tweet Failed.",
                 "pretext":"Bot Result.",
                 "color":"#FF0000",
                 "fields":[
                    {
                       "title":"Tweet Failed.",
                       "value":f"Error.[{res.status_code}]"
                    }
                 ]
              }
           ]
        } 
    ))
```

<img width="853" alt="screencapture- 2018-01-15 23.35.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/b744c73f-c8bc-2781-b08d-f23229e5a38f.png">

細かい部分は__今後の課題__として、一旦しっかり__Slackへの通知__が出来ていそうです。

## AWS-lambdaへ登録

### Scriptをlambda向けに修正

先ほど作ったスクリプト__はコマンドから実行可能__ですが、__lambda__上では動かないので__少し手を加えます。__

修正点としては、

- __ファイル名を`lambda_function.py`に変更__
- __全体を`lambda_handler(event, context)`関数にする__
- __設定値を環境変数化__
    - __※環境変数の指定の仕方は後述します。__

修正後のファイルは以下となります。

```py:lambda_function.py
import json, os, requests, random, sys  #必要なライブラリの読み込み 
from requests_oauthlib import OAuth1Session  #OAuthのライブラリの読み込み

def lambda_handler(event, context):
    CONSUMER_KEY = os.environ.get('CONSUMER_KEY')
    CONSUMER_SECRET = os.environ.get('CONSUMER_SECRET')
    ACCESS_TOKEN = os.environ.get('ACCESS_TOKEN')
    ACCESS_TOKEN_SECRET = os.environ.get('ACCESS_TOKEN_SECRET')
    twitter = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) #認証処理

    INCOMING_WEBHOOK_URL = os.environ.get('INCOMING_WEBHOOK_URL')

    TWITTER_API_HOST = "https://api.twitter.com/1.1"  #TwitterAPIホスト
    STATUSES_UPDATE_ENDPOINT = "/statuses/update.json"  #ツイートエンドポイント
    TWITTER_USER_NAME = os.environ.get('TWITTER_USER_NAME')

    
    QIITA_USER_NAME = os.environ.get('QIITA_USER_NAME')  #取得対象のQiitaアカウント名
    QIITA_API_HOST = "https://qiita.com/api/v2"  #QiitaAPIホスト
    USER_ENDPOINT = f"/users/{QIITA_USER_NAME}"  #ユーザー情報取得エンドポイント
    USER_ITEMS_ENDPOINT = f"/users/{QIITA_USER_NAME}/items"  #ユーザー記事一覧取得エンドポイント
    PAGE_PER_COUNT = 20  #1ページ辺りの記事取得件数

    response = requests.get(
        QIITA_API_HOST + USER_ENDPOINT
    ).json()  #対象Qiitaアカウント情報をJsonで取得

    totalItemCount = response['items_count']  #投稿総数を取得
    pageCount = totalItemCount // PAGE_PER_COUNT  #1ページ辺りの記事取得件数で取得出来たページ数を取得
    hasSurplus = totalItemCount % PAGE_PER_COUNT != 0  #余りの記事数を取得
    totalPageCount = pageCount + 1 if hasSurplus else pageCount #余りの記事数があった場合、端数分のページ数を追加

    params = {
        "page": random.randint(1,totalPageCount),  #ページ番号をランダム生成
        "per_page": PAGE_PER_COUNT
    }  #記事一覧をページ番号と１ページ辺りの表示件数を指定するためのパラメータを生成

    response = requests.get(
        QIITA_API_HOST + USER_ITEMS_ENDPOINT,
        params=params
    ).json()  #対象Qiitaアカウントの記事一覧を取得

    if( response == [] ):  #記事一覧が空だった場合
        requests.post(
            INCOMING_WEBHOOK_URL, 
            data = json.dumps(
            {
               "attachments":[
                  {
                     "fallback":"Qiita Article is Empty.",
                     "pretext":"Bot Result.",
                     "color":"#FF0000",
                     "fields":[
                        {
                           "title":"Qiita Item is Empty",
                           "value":"There was no Article"
                        }
                     ]
                  }
               ]
            } 
        ))
        sys.exit()  #処理終了
        

    item = random.choice(response)  #取得した一覧からランダムで記事を取得
    url = item["url"]  #記事URLを取得
    title = item["title"]  #記事タイトルを取得
    tags = map(
        lambda element: ("#" + element["name"]), 
        item["tags"] 
    )  #タグ一覧をTwitterハッシュ化して配列として再生成
    tagsStr = ' '.join(list(tags))  #タグ一覧を文字列結合

    params = {
        "status" : "【Qiita】" +title + "\n" + tagsStr + "\n" + url  #ツイート内容を生成
    }  #Postするパラメータを生成

    res = twitter.post( 
        TWITTER_API_HOST + STATUSES_UPDATE_ENDPOINT ,
        params = params
    )  #APIにPost通信
    if res.status_code == 200: #正常投稿出来た場合
        resDic = json.loads(res.text)
        id = resDic["id"]
        requests.post(
            INCOMING_WEBHOOK_URL, 
            data = json.dumps(
            {
               "attachments":[
                  {
                     "fallback":"Tweet Success.",
                     "pretext":"Bot Result.",
                     "color":"#00FF00",
                     "fields":[
                        {
                           "title":"Tweet Success.",
                           "value":f"Tweet URL : https://twitter.com/{TWITTER_USER_NAME}/status/{id}"
                        }
                     ]
                  }
               ]
            } 
        ))
    else: #正常投稿出来なかった場合
        requests.post(
            INCOMING_WEBHOOK_URL, 
            data = json.dumps(
            {
               "attachments":[
                  {
                     "fallback":"Tweet Failed.",
                     "pretext":"Bot Result.",
                     "color":"#FF0000",
                     "fields":[
                        {
                           "title":"Tweet Failed.",
                           "value":f"Error.[{res.status_code}]"
                        }
                     ]
                  }
               ]
            } 
        ))
```


### ライブラリを同階層にインストール

__lambda__では__<font color="red">pipコマンドでモジュールをインストールすることが出来ない</font>__ので、__サードパーティ製のモジュール__が必要な場合は__zip化してアップロード__する必要があります。

今回は

- __requests__
- __requests_oauthlib__

を__zipに含める__必要があります。

__作業ディレクトリ__で以下のコマンドを実行してください。

```shell:command
pip install requests -t .
pip install requests_oauthlib -t .
```

そうすると以下のように__展開__されます。

```shell:tree
.
├── certifi
├── certifi-2017.11.5.dist-info
├── chardet
├── chardet-3.0.4.dist-info
├── idna
├── idna-2.6.dist-info
├── oauthlib
├── oauthlib-2.0.6.dist-info
├── requests
├── requests-2.18.4.dist-info
├── requests_oauthlib
├── requests_oauthlib-0.8.0.dist-info
├── lambda_function.py
├── urllib3
└── urllib3-1.22.dist-info
```

### zip化

次にlambdaにアップロードするために__zip化__しましょう。
__作業ディレクトリ__で以下を実行してください。

```shell:command
zip -r upload.zip *
```

そうすると、__upload.zip__というファイルが出来ていると思います。
これを__アップロード__します。

### AWS Lambdaの設定

#### Lambda関数の作成

次にAWSに__ログイン__し、__コンピューティング__メニューの__Lambda__を選択します。

<img width="518" alt="screencapture- 2018-01-15 23.53.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/28d27b12-c247-e42e-ce67-e7b2c4e44cc8.png">

__関数の作成ボタン__をクリックします。

<img width="1187" alt="screencapture- 2018-01-15 23.53.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/fcbc6acb-c876-85a6-1f73-292306453699.png">

__一から作成__を選択し、以下を設定の上、

- __名前　：　任意の名称__
- __ランタイム　：　Python 3.6__
- __ロール　：　テンプレートから新しいロールを作成__
- __ロール名　：　任意の名称__
- __ポリシーテンプレート　：　プルダウンからBasic Edge Lambda アクセス権限を選択__

__関数の作成ボタン__をクリックしてください。

<img width="1224" alt="screencapture- 2018-01-16 0.27.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/deddbcb4-2762-ceee-62ad-5ed187c78afe.png">

そうすると、Lambda関数が作成されるので__詳細__を設定していきます。

<img width="1185" alt="screencapture- 2018-01-16 0.27.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/b45dbbfa-82d5-ed85-0288-3cb919c0125b.png">

#### 関数コード

コードエントリータイプを__.ZIPファイルをアップロード__、ランタイムを__Python 3.6__、ハンドラを__lambda_function.lambda_handler__、先ほど作成した__zipファイル__を選択します。

<img width="1219" alt="screencapture- 2018-01-16 0.29.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/0d486481-cad3-439b-6fd2-e2d3fdd17865.png">

#### 環境変数

Lambda関数単位で__環境変数__を指定することが出来ます。
今回は先ほど__config.py__に記載していたものを環境変数として設定してみましょう。

<img width="1204" alt="screencapture- 2018-01-16 0.32.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/8e3785a5-7051-2116-b0ee-2764d7ba0c26.png">

#### テスト

設定が出来たら__テスト実行__してみましょう。

<img width="1134" alt="screencapture- 2018-01-16 0.45.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/c82d00eb-604a-1f33-bbfc-16d3886d5412.png">

このような表示がされたら__無事にスクリプトが動いた__という事になります。

<img width="596" alt="screencapture- 2018-01-16 0.45.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/a6197e74-2ea1-e92f-1285-bee7865daee3.png">

Slackにも__通知__が来ていますね。

#### Lambda関数を使うメリット

__サーバーを用意する必要がない__ので__サーバーレス構成__を実現することが可能です。
Lambdaは他にも__AWSサービス群への操作をトリガーに処理を実行することが出来る__ので非常に便利です！！
また、__ログファイル__についても__デフォルト__で__CloudWatch Logsに吐き出してくれる__のでとても助かりますね。

<img width="1103" alt="screencapture- 2018-01-16 17.53.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/74fdfc7d-6601-45a6-2988-7a0e1f06cd8b.png">


### AWS CloudWatchの設定

#### スケジューリング登録

次はこのスクリプトを__定期的に実行__するように仕込みます。

今回は__毎日00:00__に投稿するようにスケジューリングしてみましょう。
実現するには__CloudWatch Events__を利用します。

__管理ツールメニュー__の__CloudWatch__を選択します。

<img width="646" alt="screencapture- 2018-01-16 0.50.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/5ba6722c-150c-4754-764c-eb32a0c0c1e4.png">

イベントから__ルールの作成__ボタンをクリックします。

<img width="551" alt="screencapture- 2018-01-16 0.51.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/c6c5eb78-9c71-c385-76c3-a30812af6b8b.png">

__イベントソース__の__スケジュール__を選択し、Cron式に`0 0 * * ? 0`を入力します。
__ターゲット__は__Lambda関数__にし、機能は__先ほど作ったLambda関数__を選択します。

そして、__設定の詳細ボタン__をクリックします。

<img width="1272" alt="screencapture- 2018-01-16 8.33.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/0a6694dd-68ea-940d-99c5-23c465d49e78.png">

名前を__任意の名称__にして__ルールの作成ボタン__をクリックします。

<img width="1268" alt="screencapture- 2018-01-16 0.52.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/9712d6c6-c6fd-a1fc-79ab-4a26b76d5db8.png">

これでルールが作成されたので__スケジューリングは完了__です。

<img width="885" alt="screencapture- 2018-01-16 0.52.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/2a579376-899e-52fa-f75b-d2a96972cd8e.png">

#### JSTとUTCの違い

ここですっかり__次の日の00:00__にLambda関数が実行されると思って油断していたら、、、

<img width="656" alt="screencapture- 2018-01-16 11.21.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/5c78db0a-5d30-2e4f-6761-c6f46ebf1c44.png">

__むむ。。。__

__<font color="red">9:00</font>__にLambda関数が__実行__されていました。

CloudWatchで指定するCronは__UTC(GMT)__で実行されるので、__JST__に置き換えた時間指定をする必要があります。

厳密には__9時間__のズレがあるので__-9時間__をすれば良いのです。
今回の場合__00:00__に実行したいので、`0 15 * * ? 0`と指定します。

<img width="1267" alt="screencapture- 2018-01-16 11.36.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/f0a62377-e816-7609-f60a-7061a3cbca4c.png">

これで正常に__毎日00:00__にLambda関数が実行されるようになりました♪
※__処理のラグ__で多少通知時間がズレてますが良しとしましょう！！笑

<img width="663" alt="screencapture- 2018-01-17 0.09.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/e570ef62-9e43-f5a0-9f93-6fbcd6e9944a.png">


# 参考にさせていただいた記事

- [Qiita API v2 の概要(非公式)](https://qiita.com/tag1216/items/b0b90e30c7e581aa2b00)
- [SlackのIncoming Webhooksを使い倒す](https://qiita.com/ik-fib/items/b4a502d173a22b3947a0)
- [【AWS】Lambdaでpipしたいと思ったときにすべきこと](https://qiita.com/Hironsan/items/0eb5578f3321c72637b4)