バイク乗り向けハンズフリー音声ツイートシステムについて(iOS+AWS)

## はじめに

いらっしゃいませ。わたしは@shnskfjwrです。
わたしは毎日すごい量のserverless.ymlを書いていますが、だれにも読ませるつもりはありません。

ところで、わたしの趣味はおバイクとTwitterです。
基本はツーリングの休憩中にツイするのですが、運転中に面白いものを見つけたらその場でツイしたくなることもありますよね？
ですが、おバイクは車と違って基本的に両手が塞がってる上にグローブを装着してるので、その場ツイは難しいですね？
だからといって、わざわざ停車してツイする？ ~~そのネタ、そこまでするほど面白い？~~

というわけで、それをどうにかしました。


## 何をするか

Siriの音声入力を利用したTwitter投稿


## 必要なもの

- iOS12以上がインストールされたiPhone
- ヘルメット内でsiriを起動できる環境(airpods2がおすすめ)
- AWSアカウント
- Twitterアカウント

## TwitterAPI

Twitterへの投稿はTwitterAPIを利用します。
TwitterAPIをコールするにはキー＆トークンが必要となるため、下記サイトを参考に開発者登録を行います。

[2020年度版 Twitter API利用申請の例文からAPIキーの取得まで詳しく解説](https://www.itti.jp/web-direction/how-to-apply-for-twitter-api/)

利用目的などは正直どう書いたか忘れてしまいましたが、正直に書いたら通った記憶があります。
なんやかんやで以下のキー情報を入手します。

- API key
- API key secret
- Access Token
- Access Token Secret

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/97927f65-a6e3-b28e-9b1d-bf64768ae693.png)


これらのキー＆トークンを使って認証を行い、TwitterAPIをコールします。

## AWS

### Secrets Manager

Twitterキー情報はSecrets Managerに登録しておきます。今回は以下のように格納しました。

- twitter_api_keys
    - api_key
    - api_secret_key
    - access_token
    - access_token_secret

![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/f9ce7b3d-aacf-59c0-5401-b64059f5626e.png)




### Lambdaのコード

認証情報をSecrets Managerから取得し、
リクエストに記載されたテキストをTwitterに投稿するシンプルな例です。


```python
import json
import os
import boto3
import base64
import requests
from botocore.exceptions import ClientError
from requests_oauthlib import OAuth1Session
 
SECRET_NAME = os.environ.get('SECRET_NAME')

STATUS_URL = "https://api.twitter.com/1.1/statuses/update.json"

def get_secret():

    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager'
    )

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=SECRET_NAME
        )
    except ClientError as e:
        raise e
    else:
        if 'SecretString' in get_secret_value_response:
            secret = get_secret_value_response['SecretString']
        else:
            secret = base64.b64decode(get_secret_value_response['SecretBinary'])

    return secret


def handler(event, context):
    try:
        secret = json.loads(get_secret())
        twitter = OAuth1Session(
            secret['api_key'],
            secret['api_secret_key'],
            secret['access_token'],
            secret['access_token_secret']
        )
        body = json.loads(event['body'])

        tweet = body['tweet']
        params = {
            'status': tweet[:140]
        }

        res = twitter.post(STATUS_URL, params = params)

        return {
            'statusCode': res.status_code
        }

    except Exception as e:
        print (e)
        raise e

```

公開されているコードを参考にさせていただきました。


### API Gateway & Lambda

使い慣れてる Serverless Framework を使ってサクッとデプロイします。

```yaml
service: tourin_tweeting
frameworkVersion: '>=2.0.0 <3.0.0'

plugins:
  - serverless-python-requirements

custom:
  defaultStage: alpha
  pythonRequirements:
    layer: true

provider:
  name: aws
  runtime: python3.7
  region: ap-northeast-1
  profile: ${opt:profile, ''}
  stage: ${opt:stage, self:custom.defaultStage}
  environment:
    TZ: Asia/Tokyo
    STAGE: ${self:provider.stage}
    SECRET_NAME: twitter_api_keys
  logRetentionInDays: 7
  iamRoleStatements:
    - Effect: Allow
      Action:
        - 'secretsmanager:GetSecretValue'
      Resource:
        - '*'
  apiKeys:
    - defaultApiKey
  usagePlan:
    quota:
      limit: 1000
      period: DAY
    throttle:
      burstLimit: 500
      rateLimit: 250

functions:
  tweet:
    handler: src/tweet.handler
    layers:
      - !Ref PythonRequirementsLambdaLayer 
    events:
      - http:
          path: /v1/tweet
          method: post
          cors: true
          integration: lambda-proxy
          private: true
```

デプロイ完了後、apiKeyがログに表示されますので控えておきます。
他の人に勝手にTwitterAPIを叩かれてしまうと ~~面白い~~ 困るので、apiKeyを有効にして最低限のセキュリティは担保しています。

ここまでの工程でTwitterAPIをwrapするAPIが完成しましたが、これだけではなんの意味もありません。
音声投稿を行うためには、iPhoneのパワーを借りる必要があります。


## ショートカット(ios)について

音声入力にはiOSに搭載されているSiriと[ショートカット](https://support.apple.com/ja-jp/guide/shortcuts/welcome/ios)を使います。
ショートカットは、iOS上で動作させられる簡易スクリプトのようなものです。
ショートカットはsiriから起動できるため、「**HeySiri！ {ショートカット名}！！**」と話しかければ起動できます。

つまり、Twitterに投稿するためのショートカットを用意し、それをSiri経由で起動することで
完全音声制御のツイが実現するというわけです。

そしてネタばらしですが、今回の企画は以下の記事の内容を真似してみることから始まりました。
[AirPodsだけでTwitter投稿も！SiriからTwitterにバックグラウンドで投稿する](http://hitoriblog.com/?p=56960)

こちらの記事を読んでええやん！！と真似してみたところ、ちょっとした問題が発生しました。
それは、[IFFFT](https://ifttt.com/)からのTwitter投稿は24時間で25件までに制限されていること！！(2020/9時点)

![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/a195712a-5531-258b-7ed4-09df11b140d2.png)


そんなんじゃぜーんぜん足りないYO！！というわけで、わたし専用のTwitterAPI基盤を構築する必要があったのです。

## ショートカットのインストール

ショートカットはicloudを経由して自由に公開・配布が可能になっています。
先述の記事で配布されていて、わたしがカスタムした「バックグラウンドでツイート」と「ツイート」をiPhoneにインストールします。
うまく行かない場合は先述記事を参考に、「信頼されていないショートカットを許可」してください。

[バックグラウンドでツイート](https://www.icloud.com/shortcuts/716cfcd014534efca396fcfa7ca3f400)
[ツイート](https://www.icloud.com/shortcuts/569bdbb2e6dc436e82c7182425247bb0)

インストール後、「バックグラウンドでツイート」の最上部のテキスト(変数URL)をAPI Gatewayの実行URLに書き換えてください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/e538e58d-5b57-b926-9c5e-ba0f15801bb1.png" width=50%>

これで準備完了です。

## 動作確認
HeySiriに対応しているイヤホンをヘルメットに装着し、心の赴くままに叫びます。

「**HeySiri！ ツイート！！**」


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/a6912233-68f3-e15d-1871-758cca47ce04.png" width=50%><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/698209b4-73c0-83d4-a874-db68311bf5ab.png" width=50%>

※iOS14以降、「StringAnswerは何にしますか？」と聞いてくるようになったので若干テンポが悪くなった。早く修正してほしい。

「**テスト投稿！！**」


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/a0d04be6-433d-adfc-f7f2-375aaa6f5003.png" width=50%>

「**はい！！**」

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/aca5c181-45fc-6df3-5913-57a0ed0577c3.png" width=50%>


## 結果

![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501670/62358fb1-77d8-0115-8385-226d38426901.png)


## 実際におバイクで使ってみた感想	

ゆっくり流してるときはきちんと聞き取ってくれます。
しかし、60km/h超えたあたりからは風切り音のせいでマイクの精度が低下してしまい微妙、
高速道路は絶望的という結果でした。それでも結構楽しい。

実は、写真を音声シャッターで撮影しそれを添付してツイする機能も開発済なのですが、それはまたの機会に……。