AWS LambdaでGooglePhotosAPIから思い出の写真をTwitterに投稿

昔から動かしていた思い出を振り返るためのシステム実装を今時にしました！
機能は同じですが、GooglePhotosAPIのフィルタ機能などで追加の機能も実装予定です！！例えば、以前やりたいな〜と記事に書いていた「人が写っている写真を優先的に表示する」なども可能になります。

# 作った時の記事
- [Twitter API を使って思い出を振り返る - Qiita](https://qiita.com/paper2/items/65e07ad14520510cc472)
- [[機能拡張] 同じ日付の昔の写真をツイートさせる - Qiita](https://qiita.com/paper2/items/d2677fb4bc54353cb459)

# システムの機能概要
- １時間ごとにその日の日付の写真をランダムでツイートする。
    - 例えば2019/09/22ならxxxx/09/22の写真がランダムにツイートされる。

# 変更点
| 変更点 | Before | After |
|:--    |:--     |:--    |
| 実行環境 | ラズパイ | AWS Lambda |
| 写真保存先 | SQLite3 on 外付けHDD| Google Photo |

全部クラウド上で動きます。今時じゃないですか？笑

# pythonのコード
- 前提
  - error処理などはあまいです。。。

## Google Photos API関連のコード

```googlePhoto.py
from googleapiclient.discovery import build
import google.oauth2.credentials
import datetime
import random
import requests
import logging
import sys
import os
import time

API_TRY_MAX=3

def getService():
    '''
    service objectの取得。
    '''

    env = os.environ
    credentials = google.oauth2.credentials.Credentials("dummy", 
    refresh_token=env['REFRESH_TOKEN'],
    # AUTHORIZATION_CODEからTokenを取得したURI
    token_uri=env['TOKEN_URI'],
    client_id=env['CLIENT_ID'],
    client_secret=env['CLIENT_SECRET'])
    service = build('photoslibrary', 'v1', credentials=credentials, cache_discovery=False)
    return service

def getMediaItems(service):
    '''
    実行日と日付が同じMediaItemsを全て取得する。
    EX) 2019/09/22日に実行した場合は、xxxx/09/22に保存されたMediaItemsを取得する。
    '''

    today = datetime.date.today()
    body = {'pageSize': 100,
            # "includedContentCategories"により、PEOPELなどカテゴリでフィルタできる。面白いから機能追加で利用を検討。
            # https://developers.google.com/photos/library/reference/rest/v1/mediaItems/search#contentcategory
            'filters': {'contentFilter': {"includedContentCategories": ["NONE"]},
                        'dateFilter': {'dates': [{"year": 0, "month": today.month, "day": today.day}]}}}
    mediaItems = []
    pagesize = 0
    response = {'nextPageToken':'dummy'}
    while 'nextPageToken' in response.keys():
        # たまにエラーが発生するため、リトライ処理を書く。
        for try_num in range(API_TRY_MAX):
            try:
                response = service.mediaItems().search(body=body).execute()
                # responseがエラーなく終了したらfor文を抜ける。
                break
            except Exception as e:
                logging.error(e)
                if try_num <= API_TRY_MAX:
                    time.sleep(3)
        else:
            logging.error("Max retry out: " + str(API_TRY_MAX))
            # エラーでリトライアウトした場合は終了
            sys.exit(1)

        if 'mediaItems' in response.keys():
            pagesize += len(response['mediaItems'])
            mediaItems += response['mediaItems']
        if 'nextPageToken' in response.keys():
            body['pageToken'] = response['nextPageToken']

        logging.debug('Get pagesize Sum: ' + str(pagesize))

    return mediaItems

def getRandomMediaItemsWithImageBinary(mediaItems):
    '''
    mediaItemsからランダムに１個選択し、写真をbaseUrlにより取得し'imageBinary'のkeyに追加して返す。
    '''

    if len(mediaItems) == 0:
        logging.debug("A number of MediaItems is 0.")
        return None
    mediaItem = random.choice(mediaItems)
    response = requests.get(mediaItem['baseUrl'])
    if response.status_code != 200:
        logging.error("Request baseUrl was failed")
        sys.exit(1)
    mediaItem['imageBinary'] = response.content
    return mediaItem
```

## Twitter関連のコード

```twitter.py
#!/usr/bin/env python
# coding: utf-8

import json
import sys
import logging
import os
from requests_oauthlib import OAuth1Session

URL_MEDIA = "https://upload.twitter.com/1.1/media/upload.json"
URL_TEXT = "https://api.twitter.com/1.1/statuses/update.json"

def uploadMedia(mediaItem):
    # OAuth認証 セッションを開始
    env = os.environ
    twitter = OAuth1Session(env['TWITTER_CK'],env['TWITTER_CS'],env['TWITTER_AT'],env['TWITTER_AS'])

    # 画像投稿
    files = {"media": mediaItem['imageBinary']}
    req_media = twitter.post(URL_MEDIA, files=files)

    # レスポンスを確認
    if req_media.status_code != 200:
        logging.error("画像アップデート失敗: %s", req_media.text)
        sys.exit(1)

    # Media ID を取得
    media_id = json.loads(req_media.text)['media_id']
    logging.debug("Media ID: %d" % media_id)

    # Media ID を付加してテキストを投稿
    params = {'status': mediaItem['mediaMetadata']['creationTime'], "media_ids": [media_id]}
    req_media = twitter.post(URL_TEXT, params=params)

    # レスポンスを確認
    if req_media.status_code != 200:
        print("テキストアップデート失敗: %s", req_media.text)
        sys.exit(1)
    
    return
```

## Lambdaのコード

```labda_funtion.py
import json
from googlePhoto import getService, getMediaItems, getRandomMediaItemsWithImageBinary
from twitter import uploadMedia

def lambda_handler(event, context):
    service=getService()
    mediaItems = getMediaItems(service)
    mediaItem = getRandomMediaItemsWithImageBinary(mediaItems)
    if mediaItem == None:
        return {
            'statusCode': 200,
            'body': json.dumps('Any media does not exit')
        }
    uploadMedia(mediaItem)

    return {
        'statusCode': 200,
        'body': json.dumps('Uploaded Media')
    }
```


# Lambdaの設定

## Lambdaを作成
- 関数名
    - memoryTweetFromGooglePhoto
- ランタイム
    - python3.7

## パッケージ化してupload

必要なパッケージをzipにまとめる。

```shell
$ pip install --target ./package requests google-api-python-client google-auth requests-oauthlib                                                                                                        [~]
Collecting requests
  Using cached https://files.pythonhosted.org/packages/51/bd/23c926cd341ea6b7dd0b2a00aba99ae0f828be89d72b2190f27c11d4b7fb/requests-2.22.0-py2.py3-none-any.whl
Collecting google-api-python-client
  Using cached https://files.pythonhosted.org/packages/5e/19/9fd511734c0dee8fa3d49f4109c75e7f95d3c31ed76c0e4a93fbba147807/google-api-python-client-1.7.11.tar.gz
Collecting google-auth
  Using cached https://files.pythonhosted.org/packages/c5/9b/ed0516cc1f7609fb0217e3057ff4f0f9f3e3ce79a369c6af4a6c5ca25664/google_auth-1.6.3-py2.py3-none-any.whl
Collecting requests-oauthlib
  Using cached https://files.pythonhosted.org/packages/c2/e2/9fd03d55ffb70fe51f587f20bcf407a6927eb121de86928b34d162f0b1ac/requests_oauthlib-1.2.0-py2.py3-none-any.whl
Collecting idna<2.9,>=2.5 (from requests)
  Using cached https://files.pythonhosted.org/packages/14/2c/cd551d81dbe15200be1cf41cd03869a46fe7226e7450af7a6545bfc474c9/idna-2.8-py2.py3-none-any.whl
Collecting chardet<3.1.0,>=3.0.2 (from requests)
  Using cached https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 (from requests)
  Using cached https://files.pythonhosted.org/packages/81/b7/cef47224900ca67078ed6e2db51342796007433ad38329558f56a15255f5/urllib3-1.25.5-py2.py3-none-any.whl
Collecting certifi>=2017.4.17 (from requests)
  Using cached https://files.pythonhosted.org/packages/18/b0/8146a4f8dd402f60744fa380bc73ca47303cccf8b9190fd16a827281eac2/certifi-2019.9.11-py2.py3-none-any.whl
Collecting httplib2<1dev,>=0.9.2 (from google-api-python-client)
  Using cached https://files.pythonhosted.org/packages/60/55/3902b9f33ad9c15abf447ad91b86ef2d0835a1ae78530f1410c115cf8fe3/httplib2-0.13.1-py3-none-any.whl
Collecting google-auth-httplib2>=0.0.3 (from google-api-python-client)
  Using cached https://files.pythonhosted.org/packages/33/49/c814d6d438b823441552198f096fcd0377fd6c88714dbed34f1d3c8c4389/google_auth_httplib2-0.0.3-py2.py3-none-any.whl
Collecting six<2dev,>=1.6.1 (from google-api-python-client)
  Using cached https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl
Collecting uritemplate<4dev,>=3.0.0 (from google-api-python-client)
  Using cached https://files.pythonhosted.org/packages/e5/7d/9d5a640c4f8bf2c8b1afc015e9a9d8de32e13c9016dcc4b0ec03481fb396/uritemplate-3.0.0-py2.py3-none-any.whl
Collecting pyasn1-modules>=0.2.1 (from google-auth)
  Using cached https://files.pythonhosted.org/packages/be/70/e5ea8afd6d08a4b99ebfc77bd1845248d56cfcf43d11f9dc324b9580a35c/pyasn1_modules-0.2.6-py2.py3-none-any.whl
Collecting rsa>=3.1.4 (from google-auth)
  Using cached https://files.pythonhosted.org/packages/02/e5/38518af393f7c214357079ce67a317307936896e961e35450b70fad2a9cf/rsa-4.0-py2.py3-none-any.whl
Collecting cachetools>=2.0.0 (from google-auth)
  Using cached https://files.pythonhosted.org/packages/2f/a6/30b0a0bef12283e83e58c1d6e7b5aabc7acfc4110df81a4471655d33e704/cachetools-3.1.1-py2.py3-none-any.whl
Collecting oauthlib>=3.0.0 (from requests-oauthlib)
  Using cached https://files.pythonhosted.org/packages/05/57/ce2e7a8fa7c0afb54a0581b14a65b56e62b5759dbc98e80627142b8a3704/oauthlib-3.1.0-py2.py3-none-any.whl
Collecting pyasn1<0.5.0,>=0.4.6 (from pyasn1-modules>=0.2.1->google-auth)
  Using cached https://files.pythonhosted.org/packages/a1/71/8f0d444e3a74e5640a3d5d967c1c6b015da9c655f35b2d308a55d907a517/pyasn1-0.4.7-py2.py3-none-any.whl
Installing collected packages: idna, chardet, urllib3, certifi, requests, httplib2, pyasn1, pyasn1-modules, rsa, six, cachetools, google-auth, google-auth-httplib2, uritemplate, google-api-python-client, oauthlib, requests-oauthlib
  Running setup.py install for google-api-python-client ... done
Successfully installed cachetools-3.1.1 certifi-2019.9.11 chardet-3.0.4 google-api-python-client-1.7.11 google-auth-1.6.3 google-auth-httplib2-0.0.3 httplib2-0.13.1 idna-2.8 oauthlib-3.1.0 pyasn1-0.4.7 pyasn1-modules-0.2.6 requests-2.22.0 requests-oauthlib-1.2.0 rsa-4.0 six-1.12.0 uritemplate-3.0.0 urllib3-1.25.5
You are using pip version 19.0.3, however version 19.2.3 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

$ cd package
$ zip -r9 ${OLDPWD}/function.zip .
省略
$ cd ${OLDPWD}
$ zip -g function.zip googlePhoto.py lambda_function.py twitter.py                                                                                          [~/SDropbox/Dropbox/dev/memoryTweetGooglePhoto]
updating: googlePhoto.py (deflated 51%)
updating: lambda_function.py (deflated 54%)
updating: twitter.py (deflated 50%)
$ aws lambda update-function-code --function-name memoryTweetFromGooglePhoto --zip-file fileb://function.zip
省略
```

## Lambdaの環境変数の設定
### GooglePhotsAPI
[[追記あり] Google Photos APIsでアルバム作成と写真のアップロード - Qiita](https://qiita.com/zaki-lknr/items/97c363c12ede4c1f25d2)を参照し

- CLIENT_ID
- CLIENT_SECRET
- REFRESH_TOKEN
- TOKEN_URI
    - AUTHORIZATION_CODEを利用してTokenを取得したURI
    - EX) https://www.googleapis.com/oauth2/v4/token

を設定する。

```getFirstToken.sh
#!/bin/bash

# 実行例
# source getFirstToken.sh

# 以下記事を参考にCLIENT_IDとCLIENT_SECRETを用意。
# [[追記あり] Google Photos APIsでアルバム作成と写真のアップロード - Qiita](https://qiita.com/zaki-lknr/items/97c363c12ede4c1f25d2)
CLIENT_ID=xxxxxxxx.apps.googleusercontent.com
CLIENT_SECRET=xxxxxxxxx

# 変更不要
REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
SCOPE=https://www.googleapis.com/auth/photoslibrary.readonly

echo "以下URLにアクセスし、認証を行う。"
echo "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=$SCOPE&access_type=offline"

echo ""
echo "以下コマンドを実行し、AUTHORIZATION_CODEを変数に書き込む。"
echo 'read AUTHORIZATION_CODE'
echo "以下コマンドを実行し、tokenを取得。"
echo 'curl --data "code=$AUTHORIZATION_CODE" --data "client_id=$CLIENT_ID" --data "client_secret=$CLIENT_SECRET" --data "redirect_uri=$REDIRECT_URI" --data "grant_type=authorization_code" --data "access_type=offline" https://www.googleapis.com/oauth2/v4/token'%
```

### Twitter API 
twitter周りの設定は過去記事や[Python で Twitter API にアクセス - Qiita](https://qiita.com/yubais/items/dd143fe608ccad8e9f85)を参考に以下の環境変数を設定する。

- TWITTER_CK
- TWITTER_CS
- TWITTER_AT
- TWITTER_AS

# ClowdWatchEventsの設定
あとはこの辺を参考にして、定期実行できるようにすればおしまい。

[Lambdaを定期実行するためにCloudWatchを設定する - Qiita](https://qiita.com/RyujiKawazoe/items/64e8a65ab3e4909678fc)

# まとめ
もともとあったシステムを今時にしました。全部クラウドでできる世界。素晴らしいですね。