Repl.it を使った Discord Bot の構築と運用（無料）

## はじめに
無料で使えるホスティングサービスである <a href="https://replit.com">Repl.it</a> と <a href="https://github.com/Rapptz/discord.py">Discord.py</a> を用いた、Discord Bot の構築と運用についてまとめてみました。Uptime Robot と併用することで課金せずに常時稼働してくれる Bot の運用が可能になると期待していました。しかし、検証としてダウンタイムを計測してみたところ、課金せずに常時稼働させるのは難しいという結果になりました（私の検証が正しければですが…）。ダウンタイムを気にしないのであれば Repl.it と Uptime Robot を使って Discord bot を運用してもいいでしょう。

## Repl.it とは
Repl.it とはクラウド上の IDE（総合開発環境）です。自分でサーバを用意する必要がなく、Web ブラウザさえあれば、開発、実行、デバッグが可能となります。ほとんどのセットアップは不要で、依存関係など（Python のライブラリなど）のインストールも自動で行ってくれます。また、共同開発も可能となっています。

## 環境
* <a href="https://replit.com">Repl.it</a>
    * Ubuntu 20.04.2 LTS
        * Python 3.8.12
            * Discord.py 2.1.0
* <a href="https://uptimerobot.com">Uptime Robot</a>

## 導入

### Bot アカウントの作成
<a href="https://discord.com/developers">Discord Developer Portal</a> より Bot アカウントを作成しましょう。詳細な手順については既にまとまっている記事があるため省きます。以下の記事を参考にして下さい。
実装においてアクセストークンが必要になります。メモしておきましょう。

https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf

### Repl の作成と Discord.py の導入
まずは <a href="https://replit.com">Repl.it</a> のアカウントを作成しましょう。その後、以下のように Python の Repl を作成します。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/84dd080c-9c6f-ee14-ed15-4854b9c723f7.png)
環境が立ち上がると、自動的に `main.py` ファイルが作成されます。以下のようにファイルの1行目に `import discord` と入力し、[Run]ボタンを実行してみて下さい。自動的に `discord.py` のライブラリがインストールされるはずです。
 
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/f9575757-3ccb-ca3e-419c-d17dd9c49378.png)

### アクセストークンの登録
Repl.it を用いる場合、基本的にソースコードは公開される設定になっています。そのため Bot のアクセストークンをコードに直書きすることは推奨されません。環境変数やトークンなどを設定するためには、メニュータブの [Tools] より [Secrets] を選択します。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/c37b32ab-78d5-cd06-029a-9f6d59314732.png)
すると Secrets タブが開かれると思うので、先程取得したアクセストークンを以下のように登録して下さい(`.env` ファイルは非推奨になりました)。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/c04e3ff3-2739-6cdb-9d46-dce794e9a97b.png)

### コードの作成と実行
一例として、投稿されたメッセージに自動的にリアクションする Bot を考えてみましょう。

```python:main.py
import discord
import os

client = discord.Client(intents=discord.Intents.default())

@client.event
async def on_ready():
    print('ログインしました')

@client.event
async def on_message(message):
    emoji ="👍"
    await message.add_reaction(emoji)

TOKEN = os.getenv("DISCORD_TOKEN")
client.run(TOKEN)
```
[Run] ボタンで実行し、コンソールに"ログインしました"と表示されていれば問題ありません。これでメッセージが投稿されると Bot が"👍"で反応してくれます。

## Bot の常時稼働
Repl.it ではブラウザタブを閉じてしまうと、Bot が停止してしまいます。これを防ぐためには2通りの方法があります。まず1つ目は、課金をし [Always-on] （左上から選択）を有効にする方法（1ヶ月あたり約6＄かかります）、2つ目は Web サーバを立ち上げそのサーバに ping を送り続ける方法があります。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/2b88dd68-9d50-2889-3347-a6d589dfe865.png)
2つ目の方法について詳しくみていきましょう。引用にある通り、Repl.it では最後のリクエストを受けてから1時間ほど経過すると Bot が停止してしまいます（厳密には1時間ではない気がしますが）。言い換えると、リクエストを受け取り続けさえすれば Bot が常時稼働してくれることになります。そこで Bot とは別に Web サーバを立て、そこに一定間隔でリクエストを送ってあげることで、Bot が常時稼働してくれます。
>Once deployed, the server will continue to run in the background, even after you close the browser tab. The server will stay awake and active until an hour after its last request, after which it will enter a sleeping stage. Sleeping repls will be woken up as soon as it receives another request; there is no need to re-run the repl. 

### Web サーバの立ち上げ
Web サーバの立ち上げには Flask と Thread を用います。Repl.it の左のタブから `keep_alive.py` というファイルを新規作成して下さい。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/247f94d5-9908-766d-9831-9f396addf433.png)
コードの中身は以下の通りです。**2024/01/18修正→コメント参照**
```python:keep_alive.py（変更前）
from flask import Flask
from threading import Thread

app = Flask('')

@app.route('/')
def home():
    return "I'm alive"

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

def keep_alive():
    t = Thread(target=run)
    t.start()
```
```python:keep_alive.py（変更後）
from flask import Flask, request
from threading import Thread

app = Flask('')

@app.route('/')
def home():
    url = request.base_url
    return f'このページのURLは {url} です'

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

def keep_alive():
    t = Thread(target=run)
    t.start()
```

これで Web サーバを立ち上げる準備ができました。続いて `main.py` を以下のように変更して下さい。

```python:main.py
# 変更前
TOKEN = os.getenv("DISCORD_TOKEN")
client.run(TOKEN)

# 変更後
from keep_alive import keep_alive # 追加
# 省略
keep_alive()
TOKEN = os.getenv("DISCORD_TOKEN")
try:
    client.run(TOKEN)
except:
    os.system("kill 1")
```
この時、`os.system("kill 1")` としているのは Repl のコンテナを再起動し IP アドレスをリセットするためです。理由としては Discord API に同じ IP アドレスからアクセスし続けていると、Discord 側でスパム扱いされてしまい、アクセスがブロックされてしまうからです（おそらく Repl.it にて、同じ IP アドレスで bot を運用している人が複数いるため？）。IP アドレスをリセットしない場合、コンソールに以下のようなエラーが発生します。
`Access denied | discord.com used Cloudflare to restrict access`

`discord.errors.HTTPException: 429 Too Many Requests (error code: 0): You are being blocked from accessing our API temporarily due to exceeding our rate limits frequently. Please read our docs at https://discord.com/developers/docs/topics/rate-limits to prevent this moving forward.`

コードの修正を終えたら、Repl で [Run] を実行して下さい。以下のようにWebview タブが開かれるはずです。開かれなかった場合、F5 で Web ページの更新をするか、左下 [Tools] より [Webview] を選択して下さい（何故か "I'm alive"と表示されないことがありましたが、まあ表示されなくても URL さえ確認できれば問題ありません）。~~このとき開かれた URL アドレスに対して、後ほど Uptime Robot を用いて、定期的に ping を送ります。メモしておきましょう。~~
**2024/01/18追記**
WebViewにURLが表示されなくなりました。Flask を利用して URL を表示するようにコードを修正しました。以下のように表示されるはずですので、その URL をコピーして利用して下さい。
![screenshot2024-01-18 11.06.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/0f483e85-c922-d95f-5504-d9859abeaecb.png)


### Uptime Robot の設定
さて、これで Repl.it で Web サーバを立ち上げることができました。後は Repl の Web サーバに定期的に ping を送るだけです。ここでは <a href="https://uptimerobot.com">Uptime Robot</a> を用います。Uptime Robot とは無料で Web サイトの死活監視をモニタリングしてくれるサービスになります。<a href="https://uptimerobot.com">ここ</a>からサイトにアクセスし右上の [Register for FREE] からアカウントを作成して下さい。アカウントが作成できたら自動的に "Dashboard" ページに移動すると思います（移動しなかったらホームより [Go to Dashboard] を押して下さい）。次に左上の [Add New Monitor] から、監視対象を追加します。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/c09cae0d-1444-4afa-f47b-128d2217a74f.png)
設定は、`Monitor Type` を `HTTP(s)`に、`Friendly Name` に適当な名前、`URL` に先程メモしておいた Repl で表示された URL、`Monitoring Interval` を5分に設定（無料アカウントだと5分間隔が一番短い。アップグレードすると1分間隔に調整可能）、エラーが起きたときように `Alert Contacts To Notify` にチェックを入れ、右下の `Create Monitor` を押して完成です。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/204fce5f-2b84-3bcd-d135-a38820537473.png)
これで Bot を動かしている Repl に、5分間隔で ping を送れるようになりました。

## ダウンタイムの検証
これで完成といきたいところですが、実際に Bot が常時稼働しているのか調べたくなってしまいました。公式から以下のように説明されています。

>Repl it being just another host, won't be able to host it 24/7 so it will have a downtime of a few minutes within every 24 hours

365日24時間は稼働できず、24時間の間で数分間のダウンタイムがあるみたいです。そこで、24時間常時稼働している Raspberry Pi を用いて、Repl.it のダウンタイムを検証してみました。検証では Uptime Robot を使った場合と [Always on]（課金をした場合）を有効にした場合とを比較しました。検証に用いたコードについては詳しく説明しませんのでご了承下さい（Python に詳しくないので間違えてるかもしれません）。

### ダウンタイムの測定方法
以下の `test.py` を Raspberry Pi のサービスに登録し、起動します（サービスは途中で一切停止しないことを前提にしています）。これで約1秒間隔でリアクションの追加と削除が繰り返されます。<font color="red">注意としてダウンタイムを測定している時は、Bot が所属しているサーバにメッセージを送らないようにして下さい。</font>リアクションの追加と削除が重複して正常に測定できなくなります（メッセージ ID を指定して、特定のメッセージに対するリアクションのみを監視対象にすれば、この問題は回避できます）。使用する際は適宜 ID を自分のものに置き換えて下さい。

```python:test.py
#!/usr/bin/env python3
import discord
import time

TOKEN = 'YourAccessToken'

client = discord.Client(intents=discord.Intents.default())

@client.event
async def on_ready():
    print('ログインしました')
    CHANNEL_ID = 1063089928042
    channel = client.get_channel(CHANNEL_ID)
    await channel.send("test")

@client.event
async def on_message(message):
    emoji ="👍"
    id = "1065906219715"
    member = await message.guild.fetch_member(id)
    while True:
        await message.add_reaction(emoji)
        time.sleep(0.7)
        await message.remove_reaction(emoji,member)
        time.sleep(0.7)

client.run(TOKEN)
```

続いて Repl.it で稼働させるコードになります。リアクションを追加した時間と削除した時間を `time.log` ファイルに保存してくれます。

```python:test2.py
import discord
import os
import time 
import datetime
from keep_alive import keep_alive

client = discord.Client(intents=discord.Intents.default())

@client.event
async def on_ready():
    print('ログインしました')

@client.event
async def on_raw_reaction_add(payload):
    global t_add
    t_add = time.time()
    diff = t_add - t_remove
    func(diff)

@client.event
async def on_raw_reaction_remove(payload):
    global t_remove
    t_remove = time.time()

def func(diff):
    dt = datetime.datetime.now()
    f = open("./time.log","a")
    f.write("logout:" + str(dt - datetime.timedelta(seconds=diff)) + "\n")
    f.write("login :" + str(dt) + "\n")
    f.close()

keep_alive()
TOKEN = os.getenv("DISCORD_TOKEN")
try:
    client.run(TOKEN)
except:
    os.system("kill 1")
```

続いて、出力された `time.log` ファイルを整形していきます。以下がそのシェルスクリプトになります（bash で書いているので性能的に遅いかもしれません。~~Python で書けばいいのですが~~）。

```bash:chk.sh
#!/bin/sh

file="time.out"
echo -n > $file
date=`cat time.log|cut -c 8-|rev|cut -c 8-|rev`
declare -a ary=(); declare -a ary=($date)

size=`expr ${#ary[*]} - 1`

for i in `seq 0 2 $size`
do
  echo $i $size
  j=`expr $i + 1`
  logdate=`echo ${ary[$i]} ${ary[$j]}`
  k=`expr $j + 2`
  t_old=`date -d "${ary[$i]} ${ary[$j]}" +%s`
  t_new=`date -d "${ary[$i]} ${ary[$k]}" +%s`
  diff=`expr $t_new - $t_old`

  echo $logdate 1 >> $file
  for l in `seq 2 $diff`
  do
    t=`expr $t_old + $l - 1`
    datetime=`date -d @$t +"%Y-%m-%d %H:%M:%S"`
    if [ $diff -gt 10 ]; then
      echo $datetime 0 >> $file
    else
      echo $datetime 1 >> $file
    fi
  done
done
```

Repl にてリアクションの追加と削除を検知した時間差が10秒より大きい場合、ダウンタイムと見なします。例えば以下のような時にダウンタイムと見なされます。

```file:time.log
logout:2023-02-01 23:06:48.258909
login :2023-02-01 23:06:49.480307
logout:2023-02-01 23:06:50.695027
login :2023-02-01 23:06:51.878834
logout:2023-02-01 23:07:39.316589
login :2023-02-01 23:07:40.555776
logout:2023-02-01 23:07:41.863756
login :2023-02-01 23:07:43.094347
```

### ダウンタイムの検証結果

Repl.it で24時間程 Bot を動かしてみました。以下が Uptime Robot を使った場合のダウンタイムの図になります。頻繁にダウンタイムがあることに加えて、最長で25分程のダウンタイムがありました。~~これでは使い物になりませんね。~~
![sample-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/05f5ae01-a704-5473-0208-464d6e07868b.png)

続いて [Always on] （課金した場合）のダウンタイムの図になります。Uptime Robot を使った場合と比較して、ダウンしている回数は少なく、ダウンタイムもかなり短いことがわかります（平均38秒）。
![sample-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/4d41904e-d26b-d643-402f-c5bdc8d74670.png)

確かに公式から説明があった通り、24時間中、数分はダウンタイムがあるみたいですね。
>Repl it being just another host, won't be able to host it 24/7 so it will have a downtime of a few minutes within every 24 hours

## 最後に
Repl.it と Uptime Robot を組み合わせれば、無料で Discord Bot の運用ができるじゃんと期待していましたが、期待した結果にはなりませんでした（私の検証が間違えてるのかもしれません）。潔く課金するか、他のホスティングサービスを探してみます。ちなみに Uptime Robot を使わない方法もあるみたいです。気になる方は試してみて下さい（常時稼働してくれるなら教えてください）。

https://replit.com/talk/learn/Discord-Bot-247-Without-UpTimeRobot/116372


## 参考
<a href="https://qiita.com/eureyuri/items/c5f041773c93a54b9f92">Pythonとrepl.itを使って無料でDiscord Botを運用してみた - Qiita</a>
<a href="https://docs.replit.com/tutorials/build-basic-discord-bot-python#keeping-our-bot-alive">Building a Discord bot with Python and Replit</a>
<a href="https://stackoverflow.com/questions/54722596/keep-discord-bot-online-on-repl-it">Keep Discord Bot online on repl.it</a>