無視されたリプライをPython+AWS Lambdaで毎日自動削除する

# メンヘラすぎて無視されたツイートは消したい
これは私ではないが、こういうツイートを消したい
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">自分がモテないのも、もしかして瀧さんのせいですか？</p>&mdash; ねっとのひと (@_tanosikuikouze) <a href="https://twitter.com/_tanosikuikouze/status/1246981628699660288?ref_src=twsrc%5Etfw">April 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- 無視されるよりは無視したほうが良いという、被害者より加害者よりの思考へシフトし、陽キャよりのコミュニケーションにコミットを行う
  - これは[セキュリティ・オートメーション](https://aws.amazon.com/jp/solutions/aws-waf-security-automations/)に対してのコミュニティ・オートメーションであり、ある意味革命である
  - もちろん例にあげた公式垢にクソリプを送るのは対象外で、日常のTwitterコミュニケーションにおける無視されたツイートが対象である

## 削除対象はこれ
- 無視されたツイート
  - リプライをしたが、リプライが返ってこないか、ふぁぼられていないツイート
  - 具体的には[こんなの](https://twitter.com/trysail_staff/status/1248423399036092427)の下のほうにある、ふぁぼられてないしリプライもされてないツイート

## 簡単な解説
- ツイにはリプライされたフラグが存在しない
![Tweetが保持しているID.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354194/3d44b8ed-651a-cbb5-e20e-a5f68e01cbe6.png)

- Tweet1がTweet2にリプライした場合、図の通りTweet2にはリプライされたというフラグが存在しない為、下記手順で抽出する必要がある
  - user_timelineからふぁぼ・RTされていないツイートを抽出
  - mentions_timelineから直近のリプライを取得し、in_reply_to_status_id(リプライされたツイート)の一覧を取得する
  - 2つを突合してリプライされていない、かわいそうなツイートを抽出する
  - かわいそうだが、それらを消す

- API取得上限の問題
    - user_timelineは一度の取得上限が3200件
    - mentions_timelineは一度の取得上限が800件
        - そのままぶつけるとuser_timelineが圧倒的に多く、問題がある
        - user_timeline =< mentions_timelineにしたい

- できるなら毎日ジョブで消したいが、面倒なのでサーバーは立てたくない
  - そこでAWS Lambdaなら簡単だし、無料

## 前提条件
- TwitterのAPIキーを発行済み
  - 英作文はググればいくらでも出てくるから頑張る 
- AWSのアカウントを作成済み
  - AWS CLIを使えるようにしておく 


  


## インフラの構築
### フォルダ構造
```cmd
DeleteNoReactionTweet
|   lambda.yml
|   README.md
|
\---code
        DeleteNoReactionReply.py
```

### パラメーターは外だしして再利用できるようにする
- `NoEcho`をつけるとセキュアだが、Lambdaのコンソールで環境変数は見えてしまうので、あまり意味がない
- SSMパラメータストアやKMSを使うとよりにセキュアにできるみたいだが、今回はそこまでしない

```yaml
AWSTemplateFormatVersion: '2010-09-09'
# Twitter API Key
Parameters:
  ConsumerKey:
    Type: String
    NoEcho: true
  ConsumerSecret:
    Type: String
    NoEcho: true
  AccessKey:
    Type: String
    NoEcho: true
  AccessSecret:
    Type: String
    NoEcho: true
```
### Lambda,IAM Role,Rule,Permissionの設定

```yaml

Resources:
# Lambda
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code: code # カレントディレクトリ下のcodeフォルダを読み込む
      Environment: # Lambda環境変数
        Variables:
          ConsumerKeyVar: !Ref ConsumerKey
          ConsumerSecretVar: !Ref ConsumerSecret
          AccessKeyVar: !Ref AccessKey
          AccessSecretVar: !Ref AccessSecret
      Description: DeleteNoReactionReply
      FunctionName: DeleteNoReactionReply
      Handler: DeleteNoReactionReply.lambda_handler
      MemorySize: 128
      Role: !Sub 
        - arn:aws:iam::${AWS::AccountId}:role/${roleName}
        -  { roleName: !Ref LambdaRole }
      Runtime: python3.8
      Timeout: 180
  # CloudWatchEvents Rule
  Rule:
    Type: 'AWS::Events::Rule'
    Properties:
      Description: DeleteNoReactionReplyRule
      Name: DeleteNoReactionReplyRule
      ScheduleExpression: 'cron(0 18 * * ? *)' # 夜中三時に消す
      State: ENABLED
      Targets:
        - Arn: !GetAtt Lambda.Arn
          Id: lambda
  # Lambda IAM Role
  LambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: DeleteNoReactionReplyRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
  # CloudWatchEvents Lambda excute enable
  LambdaEvent:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !Ref Lambda
      Principal: 'events.amazonaws.com'
      SourceArn: !GetAtt Rule.Arn
```

### Pythonで削除スクリプトを書く

```Python
import tweepy
import os

# API Key デプロイ時にCLIで指定する
consumer_key = os.environ['ConsumerKeyVar']
consumer_secret = os.environ['ConsumerSecretVar']
access_key = os.environ['AccessKeyVar']
access_secret = os.environ['AccessSecretVar']

# Tweepy Auth
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_key, access_secret)
api = tweepy.API(auth)

myReplyTweet = []
mentionInReplyTweet = []
noDeleteTweet = []
deleteTweet = []

# Get My Reply
print("==========Get My Reply==========")
for tweet in tweepy.Cursor(api.user_timeline,exclude_replies = False,include_rts = False).items(200): # あふれない程度に
    if "@" in tweet.text and tweet.favorite_count == 0 and tweet.retweet_count == 0:
        print(tweet.id,tweet.created_at,tweet.text.replace('\n',''),tweet.favorite_count,tweet.retweet_count)
        myReplyTweet.append(tweet.id)

# Get in_reply_to_status_id in mentions
print("==========Reply Tweet==========")
for mentions in tweepy.Cursor(api.mentions_timeline).items(400): # 800まで
    print(mentions.id,mentions.created_at,mentions.text.replace('\n',''))
    mentionInReplyTweet.append(mentions.in_reply_to_status_id)

# Extraction Delete Tweet
print("==========Extraction Delete tweet==========")
for mntnrptw in mentionInReplyTweet:
    for myrptw in myReplyTweet:
        if mntnrptw == myrptw:
            deleteTweet.append(myrptw)
preparateDeleteTweet = set(myReplyTweet) ^ set(deleteTweet)
print(list(preparateDeleteTweet))

# Delete Tweet
print("==========delete tweet==========")
for deltw in preparateDeleteTweet:
    print(api.get_status(deltw).id,api.get_status(deltw).created_at,api.get_status(deltw).text)
    api.destroy_status(deltw)
```

### AWSへのデプロイ
- Tweepyは外部モジュールなので同梱する

```Powershell
pip install tweepy --target ./code
```
- パッケージにしてS3に転送

```Powershell
aws cloudformation package --s3-bucket $YourBucketName `
--template-file lambda.yml `
--output-template-file lambda-packaged.yml
```

- インフラ構築とデプロイ
  - ここでTwitterのAPI KEYをLambdaの環境変数に指定する 

```Powershell
aws cloudformation deploy `
--template-file lambda-packaged.yml `
--stack-name $YourStackName `
--parameter-overrides AccessSecret=$YourAccessSecret `
ConsumerKey=$YourConsumerKey ` 
ConsumerSecret=$YourConsumerSecret ` 
AccessKey=$YourAccessKey `
--capabilities CAPABILITY_NAMED_IAM
```

- マネコンから確認する

## GitHubにあげた完全版
張っておきます。
https://github.com/natsukawadaisuki/DeleteNoReactionReply

# そもそも
- フォロワーが無視しなければいい