twittbotが停止したので自力でtwitterのBotを作成する（APIv2対応）

# 1. はじめに
## 1.1. 経緯
2010年から某ゲームの登場人物のゲーム内セリフを定時ツイートするBotを運用しておりました。2023年4月頭、利用していたサービス（旧twittbot）が停止、このままBotを運用停止しようと思ったものの、折角なので自分でBotを動かすことにしました。

## 1.2. 方法
素人（自分）でもでき、運用コストが低そうな以下の方法でBotを動かすことにしました。
* PythonでTweetするスクリプトを書く
* GoogleCloudPlatform（GCP）でスクリプトを定時に動かす

タスクスケジューラでスクリプトを稼働させても良かったのですが、PCをつけっぱなしにする必要があるので、今回はGCPを利用しました。


## 1.3. 機能要件

Botの機能は必要最低限、以下の機能を満たすものとします。
2023/5/8時点のAPIの仕様では1500ツイート/月（48ツイート/日）が上限ですが、十分事足りると思います。

* 1時間に1回tweet
* ツイートリスト（csvファイル）の中からランダムにツイート
* 7時、12時、24時は参照するツイートリストを変更する
* 24時～翌朝7時は停止
* 自動フォロー/リプライは非対応

## 1.4. 必要なもの
必要なものは以下の通りです。

* Twitterアカウント（既存のものでも可能）
* Googleアカウント（2段階認証の手続きを踏むことが望ましい）
* クレジットカード

#  2. 手順
## 2.1. APIを申請する
実際に作業に移ります。まず、既存のTwitterアカウントでログインし、以下URLからAPIを申請します。この辺りは「twitter　API　申請」とかで検索したらたくさん出てくると思うのでざっくり説明します。

https://developer.twitter.com/en

有料APIを勧められますが、無料のAPIで十分です。
「Sign up for Free Account」を選択。

英語で250文字以上のAPIの利用目的を入力、submitします。
DeepLを利用して、根性で250文字書きましょう。ChatGPTに頼ってもいいです。チェックボックスは全てチェックでいいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/119c7e04-7eda-632a-c217-e11a7182d95b.png)


申請後すぐに承認され、ディベロッパーサイトに入ります。
（承認の速度は人に寄るみたいですが、遅くても数日中には承認されるようです。）
ディベロッパーサイトに入れたら、プロジェクトを立ち上げ、アプリケーション名を付けます。このアプリケーション名がツイートのvia（Twitter for Androidみたいなの）になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/10eaf918-832a-fa76-6971-08c83e3a583b.png)

「Settings」の横、「Keys and tokens」をクリックして、
以下4種をメモしてください。これらは絶対に人に公開しないでください。

* Consumer Keys
 API key
 API Key Secret
* Authentication Tokens
Access Token
Access Token Secret


## 2.2. ツイートリストを作成する

APIを申請したら、次はツイートの準備です。
ツイート内容を記載したCSVファイル（以下、ツイートリストとします）を作成します。
1ツイート分の文字が200文字を超過しないようにしてください。
エンコードはUTF-8にします（1敗）。
時間帯や日付ごとにツイート内容を参照するcsvファイルを変更したい場合はその分だけファイルを分けてください。今回は①通常②朝7時③12時④24時の4パターンでツイート内容を変えたかったので4ファイル作成しています。
このツイートリストは保存してデスクトップなりわかりやすいところに保存しておいてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/c771f84d-5d4d-f2a8-c758-4b812c2b8d33.png)
*作成したツイートリストとその内容の参考*


## 2.3. スクリプトを作成する
tweepyを利用し、先ずはpythonからツイートが出来るか確認します。
今回スクリプトの動作確認には、Google corabo（python 3.7系）を利用しましたが、任意の環境で実施していただければいいと思います。

tweepyをインストールします。
```python
Pip install tweepy
```

python経由でツイートを行うことが出来るか確認します。
tweepy.**Client**()でないと、API連携のエラーが発生するようです。

```python:tweet_test.py
import tweepy
##以下箇所は取得したAPI情報と置き換えてください。
ck = 'API_kEY'
cs = 'API_KEY_SECRET'
at = 'ACCESS_TOKEN'
ats = 'ACCESS_TOKEN_SECRET'

client = tweepy.Client(consumer_key=ck, consumer_secret=cs, access_token=at, access_token_secret=ats)
tweet = 'Hello World'
client.create_tweet(tweet)

```

上記を実行、APIを取得したtwitterアカウントを開き、ツイートされていることを確認します。
これで最低限の「pythonからツイートする」というスクリプトができました。

上記にさらに、「ツイートリストの中からランダムにセリフをピックアップ、ツイート」「時間ごとに参照するツイートリストを変更する」機能を持たせます。

```python
Pip install pandas
```

```python:tweet_test2.py
import tweepy
import random
import pandas as pd
import datetime

tz = datetime.timezone(datetime.timedelta(hours=9))
now = datetime.datetime.now(tz)
##時分秒のうち時を取得
hour = now.hour 

##時間によって参照するツイートリストを変更
##7時はgoodmorning.csvを参照
##12時はgoodafternoon.csvを参照
##24時はgoodnight.csvを参照
##それ以外はserifu.csvを参照
if hour == 7:
    filepath = './goodmorning.csv'
elif hour == 12:
    filepath = './goodafternoon.csv'
elif hour == 0:
    filepath = './goodnight.csv'
else:
    filepath = './serifu.csv'

##ツイートリストを読み込んで、ランダムなセリフをピックアップする
df = pd.read_csv(filepath,header=None).values.tolist()
num = random.randint(0,len(df))
##確認用
print(df[num][0])

##以下箇所は取得したAPI情報と置き換えてください。
ck = 'API_kEY'
cs = 'API_KEY_SECRET'
at = 'ACCESS_TOKEN'
ats = 'ACCESS_TOKEN_SECRET'

client = tweepy.Client(consumer_key=ck, consumer_secret=cs, access_token=at, access_token_secret=ats)

client.create_tweet(df[num][0])


```
1行データのcsvなのでデータフレームの意味がないですね。

## 2.4. Cloud Storageにcsvファイルをアップロードする

次からGoogleCloudPlatform（以下、GCP）の作業に移ります。Googleアカウントにサインインし、GCPの契約画面に入ります。GCPは従量課金制（初回90日間無料）ですが、今回の利用範囲であれば無料の範囲内で利用できる or 料金が発生しても数十円で済むと思います。

確かクレジットカードの登録が必要だった気がします。
「新しいプロジェクト」を選択、任意のプロジェクト名を命名した後、コンソールへ移動します。

![humberger](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/ba6986b5-6581-0bc7-26d1-cff6193fe9b2.png)
*3機能を利用します。ハンバーガーメニューに追加しておくと楽です。*

CloudStorageをクリック、「バケットを作成」から、新しいバケットを作成します。
バケットは、フォルダみたいなものと思えばいいと思います。

:::note warn
無料枠が設けられているロケーションタイプ「region（単一リージョン）」
リージョン「us-east1」「us-west1」「us-central1」のいずれかを選択してください。
それ以外のロケーションタイプ、リージョンは、無料枠が設けられておらず、容量に寄らずデータの出し入れに料金が発生します。
:::

![backet](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/ac4f4244-1eb4-660a-1f7b-e7ecb07cdf79.png)

 
バケット名とデータの保存場所以外はデフォルトで使用します。気になる方は適宜設定してください。
リージョンも無料枠のあるリージョンで十分です。

作成したバケットに2.2.で作成したツイートリストを納めます。
 
![backet2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/0ec6fe62-5238-e34c-fb02-bc05809c65da.png)

## 2.5. Cloud Functionsにスクリプトをアップロードする
次にGCPで「Cloud Functions」を開きます。ファンクションの作成を選択、関数名とリージョンを設定します。この時選択するリージョンも、前項で選択したリージョンと同様のリージョンを設定しておくと良いと思います。
トリガーはHTTPを選択。HTTPが必須にチェック。
この時表示されるトリガーURLはあとで使用するので、メモをとっておきます。
未認証の呼びたしを許可にチェック。「次へ」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/63339468-9d40-296a-af61-3792d94c04b9.png)

Python3.7で作成したのでランタイムはpython3.7を選択。
Main.pyに先述tweet_test2.pyを改良した以下のスクリプトを貼り付けます。
具体的には、先ほど作成したバケットに格納したツイートリストを読み込むようにしています。

```python:main.py
def tweet(request):
    import tweepy
    import random
    from io import BytesIO
    import pandas as pd
    from google.cloud import storage
    import datetime
    
    tz = datetime.timezone(datetime.timedelta(hours=9))
    now = datetime.datetime.now(tz)
    hour = now.hour 
  
    if hour == 7:
        blobpath = 'goodmorning.csv'
    elif hour == 12:
        blobpath = 'goodafternoon.csv'
    elif hour == 0:
        blobpath = 'goodnight.csv'
    else:
        blobpath = 'serifu.csv'     
    
    ##クライアントをインスタンス化
    client = storage.Client()
    ##バケットを取得
    ##CloudStorageのバケット名を指定（今回はcsvkanri）
    bucket = client.get_bucket('csvkanri')
    ##BLOB（Binary Large OBject）を構成
    blob = bucket.blob(blobpath)
    # オブジェクトのデータを取得
    content = blob.download_as_bytes()
    df = pd.read_csv(BytesIO(content),header=None).values.tolist()
    num = random.randint(0,len(df))
    print(df[num][0])

    ##以下箇所は取得したAPI情報と置き換えてください。
    ck = 'API_kEY'
    cs = 'API_KEY_SECRET'
    at = 'ACCESS_TOKEN'
    ats = 'ACCESS_TOKEN_SECRET'

    api = tweepy.Client(consumer_key=ck, consumer_secret=cs, access_token=at, access_token_secret=ats)

    api.create_tweet(text=df[num][0])
    
    return "OK"
```
エントリポイントをスクリプトの関数名（tweet）に変更します。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/e5f4bacf-f5f6-ee9c-2d15-33239863b764.png)

次に、「requirements.txt」を選択、以下の通りに書き換えます。

```text:requirements.txt
tweepy
pandas
google-cloud-storage
google-cloud-firestore==1.2.0

```
その後、「デプロイ」をクリック。デプロイが完了したら、控えておいたトリガーのURLをブラウザに入力し、「OK」と表示されること、ツイートが行われていることを確認します。

## 2.6. Cloud Schedulerでスケジューリングする
次にGCPで「Cloud Scheduler」を開きます。
ジョブを作成から、新しいジョブを作成。リージョンは前項と同様のリージョンに設定します。
「説明」には、任意の説明を追加。
「頻度」は、分、時間、日、月、曜日を空白区切りで入力します。Cronの設定と一緒です。
今回は「7時～23時の間、毎時00分にプロンプトを実行」したいので、「00 0,7-23***」と入力します。タイムゾーンは日本標準時（JST）を選択します。「続行」をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/08b59ac0-57a2-e28f-a4b7-b7fbe9675707.png)

ターゲットタイプは「HTTP」を選択。URLには2.5でメモしたトリガーURLを入力。
HTTPメソッドは「POST」を選択。あとはデフォルトです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/ad82be2a-d045-67f8-dc44-db2574734401.png)

スクリプトの記載内容が正しくてもたまにエラー等で実行されないことがあるので、確実にスクリプトを実行するよう、再試行回数を設定します。適当です。「作成」をクリック。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/d6980c9d-5317-e74c-9152-83d02e5a94f3.png)


設定を作成後、「強制実行」をクリック、無事ツイートされていることを確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320139/ec632747-e5a0-f7a0-56a0-6b52b6579ebb.png)

最後にしばらく様子を見て、定時にbotがツイートを行っているか確認します。

# 3. 最後に
ざっくりとした概要だけ、駆け足で記載しましたが、Bot管理人の方々の参考になれば幸いです。
以前利用していたサービスでは@メンションに反応できていたのですが、無料APIはTLの読み取りに対応しているのかちょっと把握していないので、現状有料APIを申し込む or それに対応したbotサービスの出現を待つしか手段はなさそうです。
余談ですが、この記事を書いている間、メインのアカウントが凍結されました。幸い計3回の異議申し立て、期間にして1週間ほどでアカウントは復活しましたが、いきなりアカウントが使えなくなる危険性が今のtwitterにはあるので、せっかくBotを作成はしましたが、misskey等他のSNSへの移動も本格的に考えなくてなりませんね。


# 4.参考ページ
下記ページの記事を参考にさせていただきました。ありがとうございました。

[tweepy + Twitter API V2でツイート](https://qiita.com/penguinprogrammer/items/b220be0c203eaaad015a "tweepy + Twitter API V2でツイート")
[Pythonでtwitterのbotを作成する方法（後半）](https://kakedashi-engineer.appspot.com/2020/02/16/gcp-bot/ "Pythonでtwitterのbotを作成する方法（後半）")
[Cloud Storage に存在する CSV ファイルを pandas.DataFrame として取得する方法](https://zenn.dev/ryo_kawamata/articles/mysteriously-increasing-storage "Cloud Storage に存在する CSV ファイルを pandas.DataFrame として取得する方法")

# 5.追記
2023/5/9　
とても丁寧な先駆者様の記事
Replitを利用し、pythonのscheduleモジュールで定期的に実行されているようです。スマートですね。

https://qiita.com/nekocat777/items/965a85195c4c7438e2be

2023/7/25
何故か7月20日ごろから投稿が止まりました。
Cloud Functionのエラーは以下の通り。
```error.txt
packages/tweepy/client.py", line 115, in request raise TooManyRequests(response) tweepy.errors.TooManyRequests: 429 Too Many Requests
```
API利用の上限を超えたエラーメッセージのようですが、直前まで変わらず1時間に1回ツイートをしていること、ローカル環境とGoogle Colabで同様のスクリプトを実行した所ちゃんとツイートが投稿できることから、原因は他にあると思います。
API v1.1の廃止に伴ってOAuth1.0の認証も廃止されたのかな？とも思いましたがよくわかりません。
解決したらまたメモ書き程度に投稿します。
→スクリプトの末尾にreturn "OK"を追加したら治りました。最後にreturnを入力するのが、Cloud Functionのご作法らしいです。