Twitter api freeでpythonを利用してbot（ボット）を作る

# はじめに
新しくtwitter apiの無料プランが発表されましたので、botを作っていきたいと思います。
developer portalへの登録から定期投稿まで全部やります。

twitter api freeでは
- **ツイートする** (24時間に50回)
- (自分の)**ツイート削除** (24時間に50回)
- (自分の)**アカウントの情報手に入れる** (24時間に25回) 

ができます。（というか、これしかできない。ツイート取得さえできない...)

今回は、定期的にツイートをするbotをpythonで作ることを目指します。

:::note alert
**追記**(2024/08/05)
ここで紹介されている、無料で定期実行を行う方法は現在できなくなりました。
1月ごろの出来事です。
https://ask.replit.com/t/replit-24-7-and-uptimerobot/95014/4
https://uptimerobot.com/blog/replit-monitors-recent-issues/
などを見るとわかります。
:::
# developer portalへの登録
[developer portal](https://developer.twitter.com/en/portal/petition/essential/basic-info)に行きます。下の**Sign up for Free Account**をクリックします。(画像のようにMissing Phone Numberと表示される場合はVerify Phoneをクリックして確認を行ってください。)
![img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/78c0abee-5976-db29-72bb-17a8bf42cf11.png)

TwitterのデータとAPIの使用例を250文字以上で説明します。
![img1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/23f2dd4a-3925-76e8-50c7-fb8c7d1e010f.png)
私の場合こんな感じにしてみました。
`
I am currently learing programming with python. I am interested in creating a twitter bot using python and will use twitter api for the purpose of creating a simple bot that will tweet and delete tweets and their deletion. Machine translation is used. Sorry if there are any strange parts.
`

# botを動かせるようにしていく
歯車マークをクリックします。
![img2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/4b515a7e-54b7-06a3-b0ad-f886ee10d8b1.png)

下の方にある**User authentication settings**のSet upをクリックします。
![img3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/24cff5b9-085d-872d-4424-48f77e8ec416.png)

App permissionsを**Read and write**,Type of Appを**Web App, Automated App or Bot**にします。
![img4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/c9aaa200-0a8c-da2f-9552-7199858209f7.png)

あとは、入力が必須なApp infoの**Callback URI / Redirect URL**と**Website URL**を入力します。どちらも`https://twitter.com/`でokです。
![img5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/7a40c4c9-3916-e6d1-6db8-6331cc1d3fda.png)

Client IDとClient Secretがコピーできます。私の場合だと使わないのですが、一応コピーして安全な場所に保存しておくほうがいいかもです。ここまでできたら、Doneを押します。
![img6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/9dcea28a-1926-cb22-c1e8-08292fbfb059.png)

Keys and tokensを選択します。
![img7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/7aebec81-ad74-93de-fc73-4249a8517c94.png)

Consumer KeysのAPI Key and Secretの**Regenerate**と、Authentication TokensのAccess Token and Secretの**Generate**をクリックしてそれぞれコピーして保存しておきます。**これは後で使用します。**
![img8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/e7225044-31f1-0e60-fd13-3bb1e9b1f6b9.png)

これでDeveloper Portalの設定は以上です！お疲れさまでした！

# プログラムを書く準備
今回はpythonを利用する環境としてhttps://replit.com/ を利用します。初めての方は右上の**Sign Up**よりアカウントを作成してください。(それが完了したら下の画面に移行します)
![img9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/38144d37-660d-7204-d099-2ab937318491.png)

左下のToolsよりSecretsを選択します。
![img10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/b0d7abba-b07f-dc05-3997-be2acf29b3a6.png)

secretを追加していきます。追加するのは

|key|value|
|---|---|
|CONSUMER_KEY|API KEYの内容|
|CONSUMER_SECRET|API KEY SECRETの内容|
|ACCESS_TOKEN|Access Tokenの内容|
|ACCESS_TOKEN_SECRET|Access Token Secretの内容|

の4つです。
![img11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/235100c9-53b8-850d-694c-7805a4a90fff.png)

:::note
この操作を行うのはセキュリティを考えてのことです。また、ここの「API KEYの内容」や「API KEY SECRETの内容」には、「botを動かせるようにしていく」で先ほど保存したコードを入力してくださいね。
:::

これで、必要な準備は終わりです、実際にプログラムを書いていきましょう！

# プログラムを書く

main.pyにプログラムを入力したら、「**RUN**」をクリックしてプログラムを実行します。

## ツイートできるかテスト
```main.py
import os
import tweepy

#secretsで設定した値をとる
CONSUMER_KEY = os.environ['CONSUMER_KEY']
CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_SECRET = os.environ['ACCESS_SECRET']

#オブジェクト作成
client = tweepy.Client(
	consumer_key = CONSUMER_KEY,
	consumer_secret = CONSUMER_SECRET,
	access_token = ACCESS_TOKEN,
	access_token_secret = ACCESS_SECRET
)

#ツイートする
client.create_tweet(text='テスト')
```

https://twitter.com/home に行って確認すると、ちゃんとツイートされていることが分かりますね！
![img12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/d30ffef6-baaa-d654-b436-b46b8a884b18.png)

ツイートできることが確認できたら、あなたの目的に応じてプログラムを書きましょう。

:::note
メッセージを改行させたいときは、`\n`をいれてください。
例えば'おはよう\nございます'だと
```
おはよう
ございます
```
のように表示されます。
:::

:::note alert 
同じ内容をツイートしようとすると、`You are not allowed to create a Tweet with duplicate content.`(重複する内容のツイートを作成することは禁止されています。)というエラーがでてしまいます。このエラーがでたときはその内容はツイートできません。どこまでがこのエラーに引っかかるのか(何時間後でok、何個違うツイートを間にはさんだらokとか)はわかりませんが、定期投稿をさせるにあたっては、目的(行いたいプログラム)によってはこのエラーに気を付ける必要があります。
:::

## 画像(動画)付きツイート
画像(動画)付きのツイートをさせたい場合
参考: https://zenn.dev/stucci/articles/195815871110c2

```main.py
import os
import tweepy

#secretsで設定した値をとる
CONSUMER_KEY = os.environ['CONSUMER_KEY']
CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_SECRET = os.environ['ACCESS_SECRET']

#認証
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)

#オブジェクト作成
api = tweepy.API(auth)
client = tweepy.Client(
	consumer_key = CONSUMER_KEY,
	consumer_secret = CONSUMER_SECRET,
	access_token = ACCESS_TOKEN,
	access_token_secret = ACCESS_SECRET
)

image_path = "sample.png" #ファイルのパスをここに

#ツイート
media = api.media_upload(filename=image_path)
client.create_tweet(text="画像投稿テスト", media_ids=[media.media_id]);
```
![img13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/79901e7a-1643-932d-a6d5-3f1f4bdcb4ef.png)

## その他
一応、ツイート以外のこともできるので紹介。
公式ドキュメント↓

https://docs.tweepy.org/en/stable/client.html

使えるのは
- create_tweet() ツイート
- delete_tweet() ツイート削除
- get_me() 自分の情報取得
のみです。

`Client.delete_tweet(ツイートのid)`でツイート削除。
`Client.get_me()`で自分の情報が得られます。

実用性があるのはほぼcreate_tweet()だけだと思います。

一応引用ツイート(`client.create_tweet(text="test2",quote_tweet_id=00000000);`)や、
フォローしてる人だけが返信できる(`client.create_tweet(text="test2",reply_settings="following");`)
などもできました。だからどうしたって感じですが。
![img14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/cc52f6b4-e57b-2099-faf2-910b843a38b3.png)
![img15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/e028760c-6dff-8293-5232-faf23a313dd9.png)

# 定期投稿
:::note alert
**追記**(2024/08/05)
上でも書きましたが、
ここで紹介されている、無料で定期実行を行う方法は現在できなくなりました。
1月ごろの出来事です。
https://ask.replit.com/t/replit-24-7-and-uptimerobot/95014/4
https://uptimerobot.com/blog/replit-monitors-recent-issues/
などを見るとわかります。
:::動かしたいプログラムが完成したら、定期投稿をできるようにします。
scheduleモジュールを使って定期的な実行を行います。ただし、replitはアクティビティがないとスリープしてしまいそもそもプログラムが動かなくなりますので、
uptimerobotを利用します。
uptimerobotは、ウェブサイトの監視サービスです。uptimerobotが監視をできるように、のちほどサーバを作っていきます。

:::note warn
1つ注意してほしいのは、この方法はあまり褒められたものではないということです。(replit側からすると無料でサーバーを使われ続けるということですから。)
有料ですが、replit公式が提供している**Always On**を利用するのが本来であれば望ましいでしょう。
:::

## scheduleモジュールを使って定期実行
参考: https://di-acc2.com/programming/python/4574/

1時間に一回「テスト」と表示させるプログラムの例です。(実際は重複する内容はダメだよというエラーに引っかかりますよ)

```main.py
import os
import tweepy
import schedule
from time import sleep

#secretsで設定した値をとる
CONSUMER_KEY = os.environ['CONSUMER_KEY']
CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_SECRET = os.environ['ACCESS_SECRET']

#オブジェクト作成
client = tweepy.Client(
	consumer_key = CONSUMER_KEY,
	consumer_secret = CONSUMER_SECRET,
	access_token = ACCESS_TOKEN,
	access_token_secret = ACCESS_SECRET
)

#ツイートする
def tweet():
  #定期実行したい部分のプログラムをここに入力します。
  client.create_tweet(text='テスト')

#1時間に1回実行
schedule.every(1).hours.do(tweet)

while True:
  schedule.run_pending()
  sleep(1)
```

`import schedule`と`from time import sleep`を追加したら、定期実行したい部分のプログラムを関数化します。そして最後下の五行を追加します。

`schedule.every(1).hours.do(tweet)`の部分を変えることで、いろいろできます。例えば30分に一回なら`schedule.every(30).minutes.do(tweet)`。
詳しく説明するのは記事違いなので https://di-acc2.com/programming/python/4574/ を参考にお願いします。 freeプランでは24時間に50ツイートまでなので、頻度としては30分に1回程度が上限となります。


## サーバの立ち上げ
次に、replitをスリープさせないためのuptimerobotが監視をできるようにサーバを立ち上げます。

まず、左のタブより`server.py`を作成し以下のプログラムをコピペします。
![img16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/7ef31aa6-935e-e72b-fc4c-688eea4bcd65.png)
```server.py
from flask import Flask
from threading import Thread

app = Flask("")

@app.route("/")
def main():
  return "bot is alive!"

def run():
  app.run("0.0.0.0", port=8080)

def keep_alive():
  t = Thread(target=run)
  t.start()
```
次に、main.pyに戻り`from server import keep_alive`と、`keep_alive()`を追加します。
```main.py
#省略
from server import keep_alive
keep_alive()
#省略
```
ここまでできたら、replitの**Run**をクリックします。以下のようにWebviewタブが開かれると思いますので、URLをコピーしておきましょう。
![img17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/a9cb7fff-808e-1998-07fb-c3003b74b24c.png)

## UptimeRobotの設定
https://uptimerobot.com/ に行きます。アカウントを作成したら、左上の**Add New Monitor**をクリックします。
![img18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/154ab069-8305-776d-2d55-2df83107540d.png)
設定は`Monitor Type`を**HTTP(s)**、`Friendly Name`に好きな名前を入れたら`URL (or IP)`に先ほどコピーしたURLを貼り付けます。
`Monitoring Interval`を**every 20 minutes**にしたら、下のほうにあるSelect "Alert Contacts To Notify"　にチェックを入れ、Create Monitorをクリックし、完了したらClose。
![1mg19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/bdbc7ec5-c34a-80b6-a937-ac43b1cb97fa.png)

これで完成です。replitを起動させ続けられるようになりました。お疲れさまでした！

:::note alert
実際には**不安定**であり、完全に365日24時間稼働とはいかないみたいです。(つまり、1時間に1回ツイートするようなプログラムであってもたまにかなり間があいてしまったりなどがありえます。)
さらに安定させたい場合はやはりpc常時起動や、Always Onへの課金などが必要みたいですね。
:::

# おわりに
記事を書くのは初めてなので、冗長な部分が多々あったと思います。もし何か間違いなどあればコメントで教えてください。
見てくださりありがとうございました。

## おまけ
適当に思ったことを書きます。

### 順番ツイート
使い道がありそうな「順番にツイートしていく」プログラムを作ってみます(こういう単純なものだとわざわざプログラミングせずともtwittbot?みたいなのがあるのかもしれませんが)。順番にツイートするためには前何をツイートしたかという情報が必要ですが、完全に365日24時間ずっと稼働といえない状況では一度ダウンしたらまた1からということになりかねないので、それを防ぐためツイートするたびに何番目をツイートしたかをファイルに保存する方針で行きます。

まずnumber.txtを追加して、`0`と入力しておきます。
![img20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/0c9a21f0-e1eb-ed10-74a7-a7c8c5fa3697.png)
![img21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3111324/66523736-4362-8625-81cf-c373479b991f.png)

次に、main.pyにこれをコピペ
```main.py
import os
import tweepy
import schedule
from time import sleep
from server import keep_alive
keep_alive()

#secretsで設定した値をとる
CONSUMER_KEY = os.environ['CONSUMER_KEY']
CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_SECRET = os.environ['ACCESS_SECRET']

#オブジェクト作成
client = tweepy.Client(
	consumer_key = CONSUMER_KEY,
	consumer_secret = CONSUMER_SECRET,
	access_token = ACCESS_TOKEN,
	access_token_secret = ACCESS_SECRET
)

tweetlist = [ #ツイートする内容をここに。
  "ツイート内容1",
  "ツイート内容2",
  "ツイート内容3",
  "ツイート内容4",
  "ツイート内容5"
]

path = "number.txt"
n = 0

with open(path) as f:
  n = f.read()

#ツイートする
def tweet():
  #定期実行したい部分のプログラムをここに入力します。
  global n
  n = int(n)
  client.create_tweet(text=tweetlist[n])
  n += 1
  if n >= len(tweetlist):
    n = 0
  with open(path, mode='w') as f:
    f.write(str(n))

#3時間に1回実行
schedule.every(3).hours.do(tweet)

while True:
  schedule.run_pending()
  sleep(1)
```

〇時間に1回とか、ツイートする内容、その数などは自由に変えて使うことができます。ツイート内容重複エラーにはお気をつけて。

### アナグラム
元ネタ: https://twitter.com/Fal_conpunch

```main.py
import os
import tweepy
import schedule
import random
from time import sleep
from server import keep_alive
keep_alive()

#secretsで設定した値をとる
CONSUMER_KEY = os.environ['CONSUMER_KEY']
CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_SECRET = os.environ['ACCESS_SECRET']

#オブジェクト作成
client = tweepy.Client(
	consumer_key = CONSUMER_KEY,
	consumer_secret = CONSUMER_SECRET,
	access_token = ACCESS_TOKEN,
	access_token_secret = ACCESS_SECRET
)

word = "ファルコン・パンチ" #ここにアナグラム化するワードを

#ツイートする
def tweet():
  l = list(word)
  random.shuffle(l)
  result = ''.join(l)
  client.create_tweet(text=result)

#1時間に1回実行
schedule.every(1).hours.do(tweet)

while True:
  schedule.run_pending()
  sleep(1)
```

### 蛇足
一応、この方法で私が作った「およそ1時間に一度、ランダムに色とその反転色を生成し、hex codeとrgbを表示するbot」がありますので、リンクとmain.pyを貼っておきます。
https://twitter.com/color_botTw
```main.py
import os
import tweepy
import random
import schedule
import time
from PIL import Image, ImageDraw, ImageFont
from server import keep_alive
# ウェブサーバーを起動する
keep_alive()

#secretsで設定した値をとる
CONSUMER_KEY = os.environ['CONSUMER_KEY']
CONSUMER_SECRET = os.environ['CONSUMER_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']
ACCESS_SECRET = os.environ['ACCESS_SECRET']

#認証
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)

#オブジェクト作成
api = tweepy.API(auth)
client = tweepy.Client(
	consumer_key = CONSUMER_KEY,
	consumer_secret = CONSUMER_SECRET,
	access_token = ACCESS_TOKEN,
	access_token_secret = ACCESS_SECRET
)

#ファイルパス指定
path = "colortype.txt"

#rgbをhexコードに
def rgb_to_hex(r, g, b):
  return "#{:02x}{:02x}{:02x}".format(r, g, b)

#フォントの色を決める(黒か白)
def fontColor(r, g, b):
  sum = r + g + b
  if (sum>382):
    return "#484848"
  else:
    return "#EBEBEB"

#決めたトーンの色を生成
def tone(color):
  power = (random.randint(0,766)) / 765
  color = color.strip("#")
  rgb = list(int(color[i:i+2], 16)  for i in (0, 2, 4))
  R = int(rgb[0] * power)
  G = int(rgb[1] * power)
  B = int(rgb[2] * power)
  return R, G, B

with open(path) as f:
  colorType = f.read()

def change():
  global colorType
  n = random.randint(0, 10)
  if(n <= 1):
    colorType = "random"
    message = "今日は完全ランダムに色を生成します。"
  elif (n == 2):
    colorType = "light"
    message = "今日は明るい色中心に色を生成します。" 
  elif (n == 3):
    colorType = "dark"
    message = "今日は暗い色中心に色を生成します。"   
  elif (n == 4):
    colorType = "red"
    message = "今日は赤色中心に色を生成します。"
  elif (n == 5):
    colorType = "green"
    message = "今日は緑色中心に色を生成します。"
  elif (n == 6):
    colorType = "blue"
    message = "今日は青色中心に色を生成します。"
  elif (n == 7):
    colorType = "monochrome"
    message = "今日はモノクロを生成します。"
  elif (n == 8):
    colorType = "sepia"
    message = "今日はセピア色を生成します。"
  else:
    R = random.randint(0, 256)
    G = random.randint(0, 256)
    B = random.randint(0, 256)
    colorType = rgb_to_hex(R, G, B)
    message = "今日は"+rgb_to_hex(R, G, B)+"を基調とした色を生成します。"
    
  client.create_tweet(text=message)
  
  with open(path, mode='w') as f:
    f.write(colorType)

#ツイート
def tweet():
  if (colorType == "random"):
    R = random.randint(0, 256)
    G = random.randint(0, 256)
    B = random.randint(0, 256)
  elif (colorType == "light"):
    R = random.randint(128, 256)
    G = random.randint(128, 256)
    B = random.randint(128, 256)
  elif (colorType == "dark"):
    R = random.randint(0, 128)
    G = random.randint(0, 128)
    B = random.randint(0, 128)
  elif (colorType == "red"):
    R = random.randint(128, 256)
    G = random.randint(0, 128)
    B = random.randint(0, 128)
  elif (colorType == "green"):
    R = random.randint(0, 128)
    G = random.randint(128, 256)
    B = random.randint(0, 128)
  elif (colorType == "blue"):
    R = random.randint(0, 128)
    G = random.randint(0, 128)
    B = random.randint(128, 256)
  elif (colorType == "monochrome"):
    R, G, B = tone("ffffff")
  elif (colorType == "sepia"):
    R, G, B = tone("f0c891")
  else:
    R, G, B = tone(colorType)

  #反転色
  R2 = abs(R-255)
  G2 = abs(G-255)
  B2 = abs(B-255)

  #hexコードに変換する
  hex = rgb_to_hex(R, G, B)
  hex2 = rgb_to_hex(R2, G2, B2)

  #無地の画像作成
  img = Image.new("RGB", (1200, 800), (R, G, B))
  img2 = Image.new("RGB", (300, 200), (R2, G2, B2))
  draw = ImageDraw.Draw(img)
  draw2 = ImageDraw.Draw(img2)

  #フォントをいれる
  font = ImageFont.truetype('arial.ttf', 150)
  font2 = ImageFont.truetype('arial.ttf', 60)
  draw.text((310, 305), hex, fontColor(R,G,B), font=font)
  draw2.text((41, 65), hex2, fontColor(R2,G2,B2), font=font2)

  #生成した画像を一つにする
  img.paste(img2, (900,600))

  image_path = "color.png"
  
  img.save(image_path)
  
  message = hex + "("+str(R)+", "+str(G)+", "+str(B)+\
  ")\n反転色は" + hex2 + "("+str(R2)+", "+str(G2)+", "+str(B2)+")"

  media = api.media_upload(filename=image_path)
  client.create_tweet(text=message, media_ids=[media.media_id]);

#00:00時点に毎日実行(utcとは9時間のズレ)
schedule.every().day.at("15:00").do(change)

#00分時点に毎時間実行
schedule.every().hour.at(":00").do(tweet)

while True:
  schedule.run_pending()
  time.sleep(1)
```