Railway を使った Discord Bot の構築と運用

## はじめに
無料で使えるホスティングサービスである [Railway](https://railway.app) と [Discord.py](https://github.com/Rapptz/discord.py) を用いた、Discord Bot の構築と運用についてまとめてみました。

**2023/09/17追記**
Railway のホスティングサービスは有料化されることになりました。内容は以下サイトを参考にしてください。スペックがいいのでどこに乗り換えようか迷っています。そのまま 5$ を払い続けることも視野に入れています。

https://blog.railway.app/p/pricing-and-plans-migration-guide-2023

## Railway とは
[Railway](https://railway.app) とは、クラウドベースでアプリケーションのデプロイができるプラットフォームになります。いわゆる PaaS（Platform as a Service）と呼ばれるサービスに分類されます。非常にシンプルな UI をしており、GitHub と連携することですぐデプロイすることが可能です。また、様々な言語を使用することができ、小規模なプロジェクトから大規模なプロジェクトまで対応しています。データベースは PostgreSQL, MySQL, Redis, MongoDB の4つを提供しています。サーバがある場所は US-West ですが公式の FAQ にもある通り、そのうち追加される予定だそうです。

>Do you have a EU/APAC region?
Not yet but it's coming soon! Our servers are located in GCP's US-West.

料金体系は月 5$ 分だけ無料でそれ以降、使った分が課金されていきます。Discord Bot を運用する程度であれば、無料枠で十分でしょう。さらにデプロイしたアプリが停止することはなく、24/7 でホスティングすることが可能だと明言されています。
>No, Railway applications do not sleep unlike other platforms. If you need to have 24/7 hosting, you can attach a card and unlock the Developer Plan. 

## 環境
* Raspberry Pi 4 Model B
    * CentOS Stream 8
        * Python 3.11.0
            * discord.py 2.1.1
* [Railway](https://railway.app)

## 導入

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

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

### Railway アカウントの作成
[Railway](https://railway.app) にアクセスし右上の [Login] からアカウントを作成します。

<img width="500" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/fd252cf5-681b-a9c3-b766-7c2693decd60.png">

登録にはメールアドレスで登録する方法と GitHub 連携で登録する方法の2種類があります。GitHub アカウントを持っている方は GitHub で連携したほうが便利だと思いますが、メールアドレスで登録しても問題ありません。ですが、メールアドレスで登録するとログインする時に、いちいちメールでの認証が必要になります。面倒くさいので GitHub 連携を選んだほうが楽だと思います（GitHub アカウントを持っていない方は、これを機に作成してみて下さい。）。

<img width="300" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/d20bcbc2-eb81-b2b8-d980-cb071d041a6f.png">

[GitHub] を選択すると、`Railway App by Railway would like permission to:`と聞かれるので、[Authorize Railway App] を選択して下さい。
アカウントが作成できたら、`Your account needs to agree to the Terms of Service.Please agree to the new terms to keep on using Railway
.`というように利用規約に同意して下さいと言われるので同意して下さい。
<img width="300" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/1b2e51f6-464d-fb92-50a0-37a7cbc99aa6.png"><img width="300" alt="キャプチャ2.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/767ccbe2-a45d-a1dd-67dd-cd723fa8a801.png">

まだこのままの状態だと使えないので、右上から [Verify Account] を選択します。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/6bbcae6b-d0ee-7888-45ad-578e52f0f91d.png">

この時、作成したばかりの GitHub アカウントだと `Your GitHub might be too new or there might be risky repos on your account that means you can't verify with your GitHub.` のように警告が吐かれます。Discord コミュニティに質問してみたところ、1年くらい経過している GitHub アカウントでないと認証できないそうです。その代わりとしてクレジットカードを登録することで認証する方法があります。こちらの方法では、1\\$ チャージして、 1\\$ 返金するという処理を行うそうですが、私の場合、いくら経っても特に請求が来ることはありませんでした。また、最終的に 24/7 で Bot を動かす場合クレジットカードの登録が必要なので、ここでしてしまっても問題ないでしょう。
<font color="red">2023/04/12 追記
クレカ決済にて認証する方法が無くなった模様。ここでの認証は飛ばし、そのまま"Remove Resource Limits"を選択して下さい。</font>
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/6b347d87-7578-0864-d6d0-30be64871afc.png"><img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/284c9da1-5104-fd23-f496-78217c7b1833.png">
クレジットカードを入力し、ページを更新した後、以下のように表示されていれば問題ありません。このとき、クレジットカードの認証が上手くいかなかった時がありましたが、日を置いたら認証できたので、日を改めて試してみて下さい。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/db632a0f-eb1e-98fc-844e-e0241b0dc1a4.png">
ちなみに、私は 3 年くらい放置していた GitHub アカウントがあったので、そこに適当なリポジトリを追加するだけで認証できました。`Your Account is Verified` と表示されていれば問題ありません。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/b2976930-1313-6cd5-a627-2ddcdebd2177.png">

アカウントの認証ができたら、右上に `Starter Plan $5.00 500Hrs` と表示されていると思います。このプランでは月 5\\$ までの利用と、500 時間までの利用制限が設けられています（月初めから 21 日程経過すると停止してしまいます）。この利用制限を解除するために右上より [Remove Resource Limits] を選択し、クレジットカード番号を入力し [Subscribe to Developer Plan] を選択して下さい（~~PayPal に対応しろ~~）。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/cd2f6463-ef93-d96c-b51c-3d21e517df41.png"><img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/344f39ec-d448-7235-698c-61ded80bce66.png">

これで Developer Plan への移行が完了しました。Developer Plan の性能は以下の通りです。比較的高性能なんじゃないかなと思います。
* 最大 8GB の RAM
* 最大 8 コアの仮想 CPU
* 最大 100GB のディスク容量
* 最大 5 人のプロジェクトメンバー
* データ転送量：アウトバウンドは 100GB まで/インバウンドは無制限

また、料金体系は以下になります。毎月 5\\$ の無料枠があるため、軽い Discord Bot の運用であれば無料枠を超えることはないでしょう。ダウンタイムの検証のために 3 日間フルで Bot を動かし続けても 0.0767\\$（月換算で約 0.8\\$）だったので、負荷が少ない Bot なら約 10 個くらいホストできると思います。
```console:料金体系
サービス数 ×（稼働時間（分）× （メモリ使用量（GB））＋ 仮想 CPU 使用量（コア数））
```
詳細な金額については以下を参照して下さい。

https://docs.railway.app/reference/pricing

ちなみに[こちら](https://railway.app/pricing)から月額使用料の見積もりを見ることができます。Discord Bot の項目では、月額 3\\$ と表示されていますが、これにはデータベースとして `Postgres` と `Redis` を利用しているため、高めに表示されています。利用しなければ 3\\$ もかからないと思います。1ヶ月 Bot を2つほど運用してみたところ、 \\$0.77 の請求でした。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/84f3b90d-2b48-00c1-11e1-6145ec265e1e.png">

### デプロイ
アカウントの準備ができたのでいよいよデプロイに移っていきます。デプロイの仕方には GUI と CLI でデプロイする2種類の方法があります。Railway の UI が非常にシンプルなのもあって、GUI でデプロイしたほうが楽だと思います。また CLI はコマンドがあまり充実していないイメージがあるので、少し使いにくいかもしれません。~~わざわざ CLI を使う必要はないかなと思います。~~

#### GUI からデプロイする方法
必要なファイルを GitHub のリポジトリに追加します。必要なファイルは `main.py`, `requirements.txt`, `Dockerfile` になります。今回使った `main.py` ファイルについてはダウンタイムの検証で説明するので省略します。各々動かしたい Bot のソースコードを作成して下さい。
`requirements.txt` ファイルについては `pip freeze` コマンドより必要なライブラリを記載して下さい。

```text:requirements.txt
discord.py==2.1.1
```

最後に `Dockerfile` になります。以下のように作成して下さい。`Dokerfile` ではなく `Procfile` と `runtime.txt` ファイルを使うやり方もありますがどちらでもいいでしょう（お好みで）。

```text:Dockerfile
FROM python:3.11
WORKDIR /bot
COPY requirements.txt /bot/
RUN pip install -r requirements.txt
COPY . /bot
CMD python main.py
```

```text:Procfile
worker: python main.py
```
```text:runtime.txt
python-3.11.0
```
必要なファイルを GitHub にプッシュする方法は、記事にまとめているので参考にしてみて下さい。

https://qiita.com/Pumila/items/b90b7647f39ed3ca2069

GitHub にリポジトリを用意できたら [Railway Dashboard](https://railway.app/dashboard) にアクセスし、右上から [New Project] を選択します。次に、[Deploy from GitHub repo] を選択しデプロイしたいリポジトリを選択します。今回は `Downtimetest` を選択しました。
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/eab25810-efe8-aefb-d207-1dc71ba864f5.png"><img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/fd9ac3c8-11c5-97a7-db0a-207b237b6712.png"><img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/e5a6a23c-0104-f6b1-2870-305d1cb5f303.png"> 
このとき、GitHub アカウントと連携していない場合何も表示されていないと思います。[Deploy from GitHub repo] を選択した後、[Configure GitHub App] を選択して下さい。新しいウインドウが立ち上がり `Install & Authorize Railway App` と表示されるので、[Install & Authorize] を選択して下さい。これで連携完了です。ウインドウは閉じて問題ありません。

リポジトリを選択すると以下のように、今すぐデプロイするか、環境変数を追加するか選ぶことができます。しかし、[Add variables] を選択しても勝手にデプロイが始まってしまいます（何のための選択何でしょう）。環境変数を設定していないので当然エラーが発生します。まあそれは放置しておいて、環境変数を設定しましょう。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/86e68a5f-540c-4d7d-dba1-269c28bd9563.png">
[Add variables] を選択すると以下のような画面が表示されるので、必要となる環境変数を適宜追加して下さい。環境変数を追加するたびに自動的に再デプロイが始まります。古い環境は自動的に削除されるので、重複する心配はありません。
![10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/39198490-308e-9d93-80fa-381a02f9b771.png)
`.env` ファイル等を用意しており、1個1個登録するのが面倒くさいよという方は、一旦 [Variables] を選択し直すと（[Deployments] を選択し、[Variables] を選択する）、以下のように [RAW Editor] が選択できるようになります。こちらから、`.env` ファイルの中身をペーストして [Update Variables] を選択して下さい。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/8814b59c-0f98-efd4-5764-ea3bcebd2ed7.png)
私の環境ではデプロイは大体 20 秒くらいで終わりました。これで Discord Bot の運用ができるようになりました。
また、Railway では GitHub のリポジトリに変更があった場合、自動的に再デプロイしてくれる機能があります。無効にしたい方は [Settings] より [Disable trigger] を選択して下さい。

>Automatic Deployments
Changes made to this GitHub branch will be automatically pushed to this environment.

#### CLI からデプロイする方法
[Railway CLI](https://github.com/railwayapp/cli) には、Version 2 と Version 3 の2種類のバージョンがあります。V2 と V3 の変更がかなり大きく、個人的に V2 の方が使いやすいように思いました。V2 と V3 を使う方法について説明していきます。

##### CLI V2 を使ったデプロイ方法
自分の OS, CPU アーキテクチャにあった、CLI V2 の実行ファイルを[こちら](https://github.com/railwayapp/cli/releases/tag/v2.1.0)からダウンロードして解凍して下さい。解凍するには 7-Zip が必要になるかもしれません（私は WinRAR を使っています）。適宜、解凍ツールをインストールして下さい。解凍すると、railway（Windows の場合、railway.exe）ファイルがあると思います。これを以下のデプロイで使用していきます。
別のインストール方法もあり [railwayapp/cli](https://github.com/railwayapp/cli) の `v2.sh`を実行することでもインストールが可能です。Linux 環境だったり、Cygwin とか入れてる方はこちらでも構いません。
以降では Linux 環境でのデプロイについて説明していきます。（Windows 等でもやることは大体同じです）。まず始めに、適当なディレクトリを作成し、その中に `main.py`, `Dockerfile`, `requirements.txt`, `.env`, `railway` ファイルを突っ込みます（Linux の場合は、`railway` のパスが通っていれば入れなくても問題ないです）。`.env` ファイルには `main.py` で使う環境変数を記述して下さい。
```:.env
DISCORD_TOKEN=YourAccessToken
CHANNEL_ID=1075739
MESSAGE_ID=1075740
MEMBER_ID=10659062
```
準備ができたら以下コマンドを実行します。コマンドを実行すると URL にアクセスし認証してくれと言われるので、URL にアクセスし [Verify] を選択して下さい。認証後 `Logged in as ユーザ名（メールアドレス）`が表示されていれば問題ありません。
```console:railway login
[web@localhost work]$ railway login --browserless
Your pairing code is: sticky-whimsical-lame-produce
To authenticate with Railway, please go to 
    https://railway.app/cli-login?d=d29yZENvZGU9c3RpY2t5LXdoaW1zaWNhbC1sYW1lLXByb2R1Y2UmaG9zdG5hbWU9bG9jYWxob3N0LmxvY2FsZG9tYWlu

🎉 Logged in as Pumila (hogehoge@gmail.com)
```
ログインできたら以下コマンドを実行して下さい。`Select Starting Point:` は `Empty Project` を `Enter project name:` には適当な名前を入力して下さい。`.env` ファイルを自動的に見つけてくれるので yes を選択して下さい。その後ブラウザが開き、ダッシュボードが表示されます（ssh 経由でコンソールを立ち上げている場合は、Ctrl+C で抜けて下さい）。
```console:railway init
[web@localhost work]$ railway init 
A newer version of the Railway CLI is available, please update to: v3.0.12
✔ Starting Point: Empty Project 
✔ Enter project name: hoge█
✔ Environment: production

.env detected!
Import your variables into Railway? (y/N): y
🎉 Created project hoge
^C
```
最後に `railway up` コマンドを実行してデプロイして下さい。この時、`Failed to fetch deployments` と表示されたりしますが、デプロイできているので無視して大丈夫です。デプロイできたら、[Railway Dashboard](https://railway.app/dashboard) にアクセスし、作成したプロジェクトのサービスを選択して下さい。続いて [Variables] から [Shared Variable] を選択して下さい。すると `railway init` 時に追加した環境変数が出てくるので [Add All] を選択して下さい。追加すると自動的に再デプロイが始まります。 
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/563b43ae-f07e-d2b5-18b3-8ea155fe30c0.png)
環境変数の設定は以下のようにコマンドを実行しても追加できます。

```console:環境変数の追加
[web@localhost work]$ railway variables set hoge=hoge fuga=fuga
```

##### CLI V3 を使ったデプロイ方法
Railway CLI V3 では大きく変更が入っており、1つ目に、`railway init` 時に `.env` ファイルを読み込んでくれなくなりました。また2つ目に、環境変数の設定コマンドである `railway variables set` が実行できなくなりました。なぜこのような改変（~~改悪？~~）をしたのかは分かりませんでした（Discord のコミュニティで質問してみました。セキュリティとかの影響ですかね…）。
それでは、CLI V3 を使ったデプロイ方法の説明に入っていきます。基本的なコマンド操作は V2 と大差ありません。まずは `Railway` コマンドのインストール方法になります。
Linux, macOS, Windows: Git Bash 環境では以下のようにコマンドを実行して下さい。

```console:Shell Script
curl -fsSL cli.new | sh
```

Windows で PowerShell を使う場合は [Scoop](https://scoop.sh) を使ってインストールします。以下のようにコマンドを実行して下さい。実行ポリシーの変更について聞かれますが、Yes と答えて下さい。
```console:Scoop のインストール
PS C:\Users\hoge> Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
PS C:\Users\sato> irm get.scoop.sh | iex
```
`Scoop was installed successfully!` と表示されれば問題ありません。続いて [Railway CLI](https://github.com/railwayapp/cli) のインストールです。なお以下の操作は PowerShell（管理者）で行って下さい（普通の PowerShell だとパスが通っていないのか `scoop` コマンドが実行できないため。~~パスを通せばいいのですがめんどくさいので…~~）。
```console:railway のインストール
scoop install railway
```

`'railway' (3.0.12) was installed successfully!` と表示されれば問題ありません。
以降は Linux 環境でのデプロイについて説明していきます（Windows 等でもやることは大体同じです。PowerShell を使う場合は管理者権限で実行して下さい）。まず始めに、適当なディレクトリを作成し、`main.py`, `Dockerfile`, `requirements.txt` ファイルを突っ込んで下さい。準備ができたら以下コマンドを実行します。コマンドを実行すると URL にアクセスし認証してくれと言われるので、URL にアクセスし [Verify] を選択して下さい（PowerShell では文字化けしていてどこからどこまでが URL なのか分からなくなってます。フィーリングで URL にアクセスして下さい）。認証後 `Logged in as ユーザ名（メールアドレス）`が表示されていれば問題ありません。

```console:railway login
[web@localhost work]$ railway login --browserless
Your pairing code is: beneficial-snobbish-married-wall
To authenticate with Railway, please go to 
    https://railway.app/cli-login?d=d29yZENvZGU9YmVuZWZpY2lhbC1zbm9iYmlzaC1tYXJyaWVkLXdhbGwmaG9zdG5hbWU9bG9jYWxob3N0LmxvY2FsZG9tYWlu

🎉 Logged in as Pumila (hogehoge@gmail.com)
```

ログインできたら以下コマンドを実行して下さい。`Project Name` は入力せずに Enter を押すとランダムで生成されます（なお `Project Name` を入力する時は BackSpace キーが効きません。~~なんて不親切なUIなんだ~~）。
```console:railway init
[web@localhost work]$ railway init
> Project Name Will be randomly generated
Created project substantial-horses on Personal
https://railway.app/project/44317ae1-f06e-4bbd-a299-cd7090765f5f
```
最後に `railway up` コマンドを実行してデプロイして下さい。デプロイできたら、[Railway Dashboard](https://railway.app/dashboard) にアクセスし、作成したプロジェクトのサービスを選択して下さい。[Variables] から 環境変数を追加して下さい。`.env` ファイルを用意しているのであれば [RAW Editor] からコピペするのが楽だと思います。追加すると自動的に再デプロイが始まります。

なおコマンド実行時に以下のようなエラーが表示されることがありました。
```console
Unable to parse config file, regenerating
Error: Unauthorized. Please login with `railway login`
```
対応策として、Linux の場合 `~/.railway/config.json` を Windows の場合 `C:\Users\hoge\.railway\config.json` を削除して再ログインして下さい。

## ダウンタイムの検証
続いてダウンタイムの検証に移っていきます。検証期間は約3日間になります。以下が検証に用いたソースコードになります。

https://github.com/Pumi1a/Downtimetest

### ダウンタイムの測定方法
Discord の設定や、ソースコードの中身について軽く説明していきます（興味ない方は飛ばして下さい）。まずプライベートチャンネルを作成し、そこに Bot を追加します。これにより、ダウンタイムの検証を並行して行うことが可能になりますし、Bot の負荷軽減も見込めます（所属しているチャンネルのメッセージしか取得できないため）。また、プライベートチャンネル以外のメッセージには反応しないように設定しています。使用するファイルは、`send.py`, `main.py`, `chk.py`, `chk.sh` になります。`send.py` と `chk.py` を Raspberry Pi 上でサービスとして動かし、`main.py` を Railway 上で動かしています。これらは全て同じ Bot を利用しています。なお、この検証では Raspberry Pi 上でダウンタイムやエラーなどは発生しないことを前提にしています。`chk.sh` はダウンタイムを確認するためのファイル整形のスクリプトになります（bash で書いているので性能的に遅いかもしれません。たった12万行のファイル処理に2時間半もかかっていました。まあ放置していればいいので気が向いたら、ファイル処理の仕方を変更しようかなと思います。~~Python で書けばいいのに。~~ 執筆中 ChatGPT を用いてシェルスクリプトのコードを Python のコードに変換してもらったところ、50 秒ほどで処理が終わりました。速いですね。そちらも載せておきます）。見にくいですが、簡単なフローチャートを作ってみました（もっと綺麗に見せられる描画方法等あれば教えて下さい）。
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/286460d3-cf8a-f73b-f7af-716f6da8c27b.png)
動作は以下のようになります。
![20230406_215724.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/88ca460d-40ec-9a69-42b1-769e562f2455.gif)
`send.py` の役割は非常に簡単で、プライベートチャンネルに "test" メッセージを2秒間隔で送り続けるだけになります。
```python:send.py
#!/usr/bin/env python3
import discord
import time

# 適宜置き換えて下さい
TOKEN = 'YourAccessToken'
CHANNEL_ID = 11111111

intents = discord.Intents.none()
intents.guilds = True
client = discord.Client(intents=intents)

@client.event
async def on_ready():
    print('ログインしました')
    channel = client.get_channel(CHANNEL_ID)
    while True:
        await channel.send("test")
        time.sleep(2)
    
client.run(TOKEN)
```

続いて `main.py` の役割について説明していきます。`send.py` によって送られてきた "test" メッセージを受信すると、"test" メッセージを削除し、特定のメッセージのリアクション有無に応じて、リアクションの追加/削除を行います。特定のメッセージというのは gif 画像でいう "Main_Test" メッセージになります。"CHANNEL_ID" はプライベートチャンネルの ID を、"MESSAGE_ID" は特定のメッセージ（"Main_Test"）の ID を、"MEMBER_ID" は `main.py` を動かす Bot の ID になります。intents については負荷軽減のため最小限にしてあります。intents については記事にまとめているので気になる方は参考にしてみて下さい。

https://qiita.com/Pumila/items/8b8e14f60fd7f266872b

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

intents = discord.Intents.none()
intents.messages = True
intents.guilds = True
intents.message_content = True
client = discord.Client(intents=intents)

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

# Bot が所属しているチャンネルにメッセージがされた時に動作する処理
# プライベートチャンネルを用意しそこに Bot を追加している
@client.event
async def on_message(msg):
# メッセージのチャンネルがプライベートチャンネルに該当しない場合
# メッセージを無視する
    txt_channel = client.get_channel(CHANNEL_ID)
    if txt_channel != msg.channel:
        return
# メッセージの内容が "test" の場合削除する
    if msg.content == "test":
        await msg.delete()
# 特定のメッセージの状態取得
    message = await txt_channel.fetch_message(MESSAGE_ID)
    emoji = "👍"
# 1度のみ実行
    if get_flag():
        global member
        id = MEMBER_ID
        member = await message.guild.fetch_member(id)
# リアクションの有無に応じてリアクションの追加/削除を行う
    if not message.reactions:
        await message.add_reaction(emoji)
    else:
        await message.remove_reaction(emoji,member)

# 初回の呼び出し時のみ True を返しそれ以降は False を返す
def get_flag():
    global get_flag
    def get_flag():
        return False
    return True
    
TOKEN = os.getenv("DISCORD_TOKEN")
CHANNEL_ID = int(os.getenv("CHANNEL_ID"))
MESSAGE_ID = int(os.getenv("MESSAGE_ID"))
MEMBER_ID = int(os.getenv("MEMBER_ID"))
client.run(TOKEN)
```

次に `chk.py` の役割について説明していきます。リアクションの追加/削除された時間を取得することで、リアクションが追加された時間と削除された時間を `time.log` ファイルに出力しています。

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

# 適宜置き換えて下さい
TOKEN = 'YourAccessToken'
MESSAGE_ID = 111111

intents = discord.Intents.none()
intents.reactions = True
client = discord.Client(intents=intents)

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

# リアクションが追加された時の処理
@client.event
async def on_raw_reaction_add(payload):
    if payload.message_id != MESSAGE_ID:
        return
    global t_add
    t_add = time.time()
    diff = t_add - t_remove
    func(diff)

# リアクションが削除された時の処理
@client.event
async def on_raw_reaction_remove(payload):
    if payload.message_id != MESSAGE_ID:
        return
    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()

client.run(TOKEN)
```

最後に `chk.sh` ファイルの説明になります。リアクションが追加された時間と削除された時間の差を取り、その差が10秒以上の時、ダウンタイムと見なしています。Python のコードに書き換えたものも載せておきます。当然ですが、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]}`
  if [ $j -eq $size ]; then
    echo $logdate 1 >> $file
    break
  fi
  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
```

```python:chk2.py
#!/usr/bin/env python3
import subprocess
import datetime

file = "time.out"
open(file, 'w').close()

date = subprocess.check_output("cut -c 8- time.log | rev | cut -c 8- | rev", shell=True)
ary = date.decode().split()
size = len(ary) - 1

for i in range(0, size, 2):
    print(i, size)
    j = i + 1
    logdate = f"{ary[i]} {ary[j]}"
    if j == size:
        with open(file, 'a') as f:
            f.write(f"{logdate} 1\n")
        break
    k = j + 2
    t_old = datetime.datetime.strptime(logdate, "%Y-%m-%d %H:%M:%S").timestamp()
    t_new = datetime.datetime.strptime(f"{ary[i]} {ary[k]}", "%Y-%m-%d %H:%M:%S").timestamp()
    diff = int(t_new - t_old)

    with open(file, "a") as f:
        f.write(f"{logdate} 1\n")
        for l in range(2, diff + 1):
            t = int(t_old + l - 1)
            dt = datetime.datetime.fromtimestamp(t).strftime("%Y-%m-%d %H:%M:%S")
            if diff > 10:
                f.write(f"{dt} 0\n")
            else:
                f.write(f"{dt} 1\n")
```

### ダウンタイムの検証結果
Railway で約3日 Bot を動かしてみました。以下がその時のダウンタイムの図になります。ダウンしているのが2回で、それぞれ48秒と37秒になります（分かりにくいですが Bot を動かしてすぐのところでダウンしています）。かなり安定して稼働してくれているのではないでしょうか。さすが 24/7 で稼働できると謳っているだけありますね。
![sample-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/9276c82d-3a58-7fad-af00-9cd4447b4781.png)
ダウンタイム検証時の Discord のチャンネルの様子は以下の通りです。6回 "test" の削除が間に合っていないことが分かります。2秒に1回 "test" メッセージを送っているので、ダウンタイムの時間からして、もっと多くあってもいいと思いますが、Railway 上でラグがあって削除された、あるいは Raspberry Pi 上で "test" メッセージが送られなかったか、という推測ができます。Raspberry Pi 上で `journalctl -u サービス名` と実行しても特にエラーなどなく稼働していることを確認しましたが、Raspberry Pi 上で "test" メッセージが送られなかった可能性の方が高そうです（つまり、ダウンタイムはほとんどないのでは？）。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/ca35ec15-c14d-f099-2ba4-0abafeb58b6a.png)
また、動かしている時に以下のようなエラーが吐かれていました。API のリクエスト制限に引っかかているエラーですね。
```console
[2023-02-26 05:57:13] [WARNING ] discord.http: We are being rate limited. DELETE https://discord.com/api/v10/channels/1075739041112281120/messages/1079280900484902983 responded with 429. Retrying in 3.00 seconds.
[2023-02-26 05:57:15] [ERROR   ] discord.client: Ignoring exception in on_message
Traceback (most recent call last):
File "/opt/venv/lib/python3.11/site-packages/discord/client.py", line 409, in _run_event
await coro(*args, **kwargs)
File "/app/main.py", line 18, in on_message
await msg.delete()
File "/opt/venv/lib/python3.11/site-packages/discord/message.py", line 805, in delete
await self._state.http.delete_message(self.channel.id, self.id)
File "/opt/venv/lib/python3.11/site-packages/discord/http.py", line 759, in request
raise HTTPException(response, data)
discord.errors.HTTPException: 429 Too Many Requests (error code: 40062): Service resource is being rate limited.
```

以下のエラーは Discord サーバにアクセスできないといったエラーになります。

```console
[2023-02-26 14:26:56] [ERROR   ] discord.client: Ignoring exception in on_message
Traceback (most recent call last):
File "/opt/venv/lib/python3.11/site-packages/discord/client.py", line 409, in _run_event
await coro(*args, **kwargs)
File "/app/main.py", line 18, in on_message
await msg.delete()
File "/opt/venv/lib/python3.11/site-packages/discord/message.py", line 805, in delete
await self._state.http.delete_message(self.channel.id, self.id)
File "/opt/venv/lib/python3.11/site-packages/discord/http.py", line 624, in request
async with self.__session.request(method, url, **kwargs) as response:
File "/opt/venv/lib/python3.11/site-packages/aiohttp/client.py", line 1141, in __aenter__
self._resp = await self._coro
^^^^^^^^^^^^^^^^
File "/opt/venv/lib/python3.11/site-packages/aiohttp/client.py", line 560, in _request
await resp.start(conn)
File "/opt/venv/lib/python3.11/site-packages/aiohttp/client_reqrep.py", line 899, in start
message, payload = await protocol.read()  # type: ignore[union-attr]
^^^^^^^^^^^^^^^^^^^^^
File "/opt/venv/lib/python3.11/site-packages/aiohttp/streams.py", line 616, in read
await self._waiter
aiohttp.client_exceptions.ClientOSError: [Errno 104] Connection reset by peer
```

最後に `send.py` と `chk.py` のサービスログを載せておきます。`journalctl -u サービス名`でログの確認ができます。警告が出ていますが、特に問題もなく稼働してくれていました。

<details><summary>サービスのログ</summary><div>

```:send.py log
Feb 24 17:09:24 localhost.localdomain systemd[1]: Started test.
Feb 24 17:09:24 localhost.localdomain send.py[1188]: [2023-02-24 17:09:24] [INFO    ] discord.client: logging in using static token
Feb 24 17:09:31 localhost.localdomain send.py[1188]: [2023-02-24 17:09:31] [INFO    ] discord.gateway: Shard ID None has connected to Gateway (Session ID: 1cad9a20f6f03513e0835c4461a31348).
Feb 24 18:56:27 localhost.localdomain send.py[1188]: [2023-02-24 18:56:27] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 24 19:36:18 localhost.localdomain send.py[1188]: [2023-02-24 19:36:18] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 24 20:45:26 localhost.localdomain send.py[1188]: [2023-02-24 20:45:26] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 24 21:27:41 localhost.localdomain send.py[1188]: [2023-02-24 21:27:41] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 24 21:57:43 localhost.localdomain send.py[1188]: [2023-02-24 21:57:43] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 24 23:11:15 localhost.localdomain send.py[1188]: [2023-02-24 23:11:15] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 00:58:12 localhost.localdomain send.py[1188]: [2023-02-25 00:58:12] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 01:26:08 localhost.localdomain send.py[1188]: [2023-02-25 01:26:08] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 01:28:20 localhost.localdomain send.py[1188]: [2023-02-25 01:28:20] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 03:20:08 localhost.localdomain send.py[1188]: [2023-02-25 03:20:08] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 05:08:34 localhost.localdomain send.py[1188]: [2023-02-25 05:08:34] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 06:49:13 localhost.localdomain send.py[1188]: [2023-02-25 06:49:13] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 09:04:01 localhost.localdomain send.py[1188]: [2023-02-25 09:04:01] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 09:22:41 localhost.localdomain send.py[1188]: [2023-02-25 09:22:41] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 12:34:14 localhost.localdomain send.py[1188]: [2023-02-25 12:34:14] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 14:15:04 localhost.localdomain send.py[1188]: [2023-02-25 14:15:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 14:41:53 localhost.localdomain send.py[1188]: [2023-02-25 14:41:53] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 16:14:21 localhost.localdomain send.py[1188]: [2023-02-25 16:14:21] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 18:36:02 localhost.localdomain send.py[1188]: [2023-02-25 18:36:02] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 19:22:54 localhost.localdomain send.py[1188]: [2023-02-25 19:22:54] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 22:02:46 localhost.localdomain send.py[1188]: [2023-02-25 22:02:46] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 25 23:56:33 localhost.localdomain send.py[1188]: [2023-02-25 23:56:33] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 26 03:02:04 localhost.localdomain send.py[1188]: [2023-02-26 03:02:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 26 04:57:43 localhost.localdomain send.py[1188]: [2023-02-26 04:57:43] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 26 06:38:54 localhost.localdomain send.py[1188]: [2023-02-26 06:38:54] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 1cad9a20f6f03513e0835c4461a31348.
Feb 26 09:03:34 localhost.localdomain send.py[1188]: [2023-02-26 09:03:34] [INFO    ] discord.gateway: Shard ID None session has been invalidated.
Feb 26 09:03:46 localhost.localdomain send.py[1188]: [2023-02-26 09:03:46] [INFO    ] discord.gateway: Shard ID None has connected to Gateway (Session ID: adc5a05990c5d655620b72c2794d2895).
Feb 26 11:54:51 localhost.localdomain send.py[1188]: [2023-02-26 11:54:51] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 26 13:00:38 localhost.localdomain send.py[1188]: [2023-02-26 13:00:38] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 26 14:53:04 localhost.localdomain send.py[1188]: [2023-02-26 14:53:04] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 15:34:24 localhost.localdomain send.py[1188]: [2023-02-26 15:34:24] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 26 16:28:45 localhost.localdomain send.py[1188]: [2023-02-26 16:28:45] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 16:29:29 localhost.localdomain send.py[1188]: [2023-02-26 16:29:29] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:43:44 localhost.localdomain send.py[1188]: [2023-02-26 17:43:44] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:50:20 localhost.localdomain send.py[1188]: [2023-02-26 17:50:20] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:51:05 localhost.localdomain send.py[1188]: [2023-02-26 17:51:05] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:51:49 localhost.localdomain send.py[1188]: [2023-02-26 17:51:49] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:52:33 localhost.localdomain send.py[1188]: [2023-02-26 17:52:33] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:53:17 localhost.localdomain send.py[1188]: [2023-02-26 17:53:17] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:54:01 localhost.localdomain send.py[1188]: [2023-02-26 17:54:01] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:54:45 localhost.localdomain send.py[1188]: [2023-02-26 17:54:45] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:55:29 localhost.localdomain send.py[1188]: [2023-02-26 17:55:29] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 17:56:13 localhost.localdomain send.py[1188]: [2023-02-26 17:56:13] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 18:03:24 localhost.localdomain send.py[1188]: [2023-02-26 18:03:24] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 26 21:15:12 localhost.localdomain send.py[1188]: [2023-02-26 21:15:12] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 21:15:56 localhost.localdomain send.py[1188]: [2023-02-26 21:15:56] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 21:16:40 localhost.localdomain send.py[1188]: [2023-02-26 21:16:40] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 21:17:24 localhost.localdomain send.py[1188]: [2023-02-26 21:17:24] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 21:18:09 localhost.localdomain send.py[1188]: [2023-02-26 21:18:09] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 21:24:45 localhost.localdomain send.py[1188]: [2023-02-26 21:24:45] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 26 21:55:55 localhost.localdomain send.py[1188]: [2023-02-26 21:55:55] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 26 22:35:30 localhost.localdomain send.py[1188]: [2023-02-26 22:35:30] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 27 02:17:03 localhost.localdomain send.py[1188]: [2023-02-27 02:17:03] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 27 03:41:26 localhost.localdomain send.py[1188]: [2023-02-27 03:41:26] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session adc5a05990c5d655620b72c2794d2895.
Feb 27 05:15:51 localhost.localdomain send.py[1188]: [2023-02-27 05:15:51] [INFO    ] discord.gateway: Shard ID None session has been invalidated.
Feb 27 05:16:17 localhost.localdomain send.py[1188]: [2023-02-27 05:16:17] [INFO    ] discord.gateway: Shard ID None has connected to Gateway (Session ID: 91e5d75478fc300c3f684999054cd473).
Feb 27 05:44:50 localhost.localdomain send.py[1188]: [2023-02-27 05:44:50] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 91e5d75478fc300c3f684999054cd473.
Feb 27 08:40:14 localhost.localdomain send.py[1188]: [2023-02-27 08:40:14] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 91e5d75478fc300c3f684999054cd473.
Feb 27 09:11:57 localhost.localdomain send.py[1188]: [2023-02-27 09:11:57] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 91e5d75478fc300c3f684999054cd473.
Feb 27 09:45:12 localhost.localdomain send.py[1188]: [2023-02-27 09:45:12] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 91e5d75478fc300c3f684999054cd473.
Feb 27 13:12:28 localhost.localdomain send.py[1188]: [2023-02-27 13:12:28] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 91e5d75478fc300c3f684999054cd473.
Feb 27 14:25:21 localhost.localdomain send.py[1188]: [2023-02-27 14:25:21] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 46.1s behind.
Feb 27 15:14:11 localhost.localdomain send.py[1188]: [2023-02-27 15:14:11] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 27 15:14:57 localhost.localdomain send.py[1188]: [2023-02-27 15:14:57] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 46.1s behind.
Feb 27 15:15:41 localhost.localdomain send.py[1188]: [2023-02-27 15:15:41] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 44.1s behind.
Feb 27 15:16:27 localhost.localdomain send.py[1188]: [2023-02-27 15:16:27] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 46.1s behind.
Feb 27 16:16:35 localhost.localdomain send.py[1188]: [2023-02-27 16:16:35] [WARNING ] discord.gateway: Can't keep up, shard ID None websocket is 46.1s behind.
Feb 27 16:47:32 localhost.localdomain send.py[1188]: [2023-02-27 16:47:32] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session 91e5d75478fc300c3f684999054cd473.
```

```:chk.py log
Feb 24 17:09:23 localhost.localdomain systemd[1]: Started test2.
Feb 24 17:09:24 localhost.localdomain chk.py[1182]: [2023-02-24 17:09:24] [INFO    ] discord.client: logging in using static token
Feb 24 17:09:25 localhost.localdomain chk.py[1182]: [2023-02-24 17:09:25] [INFO    ] discord.gateway: Shard ID None has connected to Gateway (Session ID: c422723bdf1d66ffbbfe84418c38a5d0).
Feb 24 17:09:33 localhost.localdomain chk.py[1182]: [2023-02-24 17:09:33] [ERROR   ] discord.client: Ignoring exception in on_raw_reaction_add
Feb 24 17:09:33 localhost.localdomain chk.py[1182]: Traceback (most recent call last):
Feb 24 17:09:33 localhost.localdomain chk.py[1182]:   File "/usr/local/lib/python3.11/site-packages/discord/client.py", line 409, in _run_event
Feb 24 17:09:33 localhost.localdomain chk.py[1182]:     await coro(*args, **kwargs)
Feb 24 17:09:33 localhost.localdomain chk.py[1182]:   File "/home/web/discord/downtimetest/railway/chk.py", line 22, in on_raw_reaction_add
Feb 24 17:09:33 localhost.localdomain chk.py[1182]:     diff = t_add - t_remove
Feb 24 17:09:33 localhost.localdomain chk.py[1182]:                    ^^^^^^^^
Feb 24 17:09:33 localhost.localdomain chk.py[1182]: NameError: name 't_remove' is not defined
Feb 24 21:05:04 localhost.localdomain chk.py[1182]: [2023-02-24 21:05:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 24 22:21:36 localhost.localdomain chk.py[1182]: [2023-02-24 22:21:36] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 01:42:46 localhost.localdomain chk.py[1182]: [2023-02-25 01:42:46] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 05:19:51 localhost.localdomain chk.py[1182]: [2023-02-25 05:19:51] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 06:19:04 localhost.localdomain chk.py[1182]: [2023-02-25 06:19:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 08:29:03 localhost.localdomain chk.py[1182]: [2023-02-25 08:29:03] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 12:28:49 localhost.localdomain chk.py[1182]: [2023-02-25 12:28:49] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 16:14:43 localhost.localdomain chk.py[1182]: [2023-02-25 16:14:43] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 19:29:03 localhost.localdomain chk.py[1182]: [2023-02-25 19:29:03] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 20:14:56 localhost.localdomain chk.py[1182]: [2023-02-25 20:14:56] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 23:06:45 localhost.localdomain chk.py[1182]: [2023-02-25 23:06:45] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 25 23:51:35 localhost.localdomain chk.py[1182]: [2023-02-25 23:51:35] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 03:06:28 localhost.localdomain chk.py[1182]: [2023-02-26 03:06:28] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 04:16:59 localhost.localdomain chk.py[1182]: [2023-02-26 04:16:59] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 06:23:23 localhost.localdomain chk.py[1182]: [2023-02-26 06:23:23] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 09:52:53 localhost.localdomain chk.py[1182]: [2023-02-26 09:52:53] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 11:11:11 localhost.localdomain chk.py[1182]: [2023-02-26 11:11:11] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 13:44:11 localhost.localdomain chk.py[1182]: [2023-02-26 13:44:11] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 15:00:55 localhost.localdomain chk.py[1182]: [2023-02-26 15:00:55] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 18:26:15 localhost.localdomain chk.py[1182]: [2023-02-26 18:26:15] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 26 22:02:40 localhost.localdomain chk.py[1182]: [2023-02-26 22:02:40] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 01:36:20 localhost.localdomain chk.py[1182]: [2023-02-27 01:36:20] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 03:08:43 localhost.localdomain chk.py[1182]: [2023-02-27 03:08:43] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 03:37:04 localhost.localdomain chk.py[1182]: [2023-02-27 03:37:04] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 05:59:27 localhost.localdomain chk.py[1182]: [2023-02-27 05:59:27] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 07:06:08 localhost.localdomain chk.py[1182]: [2023-02-27 07:06:08] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 09:39:44 localhost.localdomain chk.py[1182]: [2023-02-27 09:39:44] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 12:40:56 localhost.localdomain chk.py[1182]: [2023-02-27 12:40:56] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 16:16:21 localhost.localdomain chk.py[1182]: [2023-02-27 16:16:21] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 18:54:17 localhost.localdomain chk.py[1182]: [2023-02-27 18:54:17] [INFO    ] discord.gateway: Shard ID None has successfully RESUMED session c422723bdf1d66ffbbfe84418c38a5d0.
Feb 27 20:30:34 localhost.localdomain systemd[1]: Stopping test2...
Feb 27 20:30:34 localhost.localdomain systemd[1]: test2.service: Succeeded.
Feb 27 20:30:34 localhost.localdomain systemd[1]: Stopped test2.
```
</div></details>

## 最後に
使ってみた感想として、UI がシンプルなのですごく使いやすかったです。ダウンタイムもほぼありませんし、無料で Discord Bot を利用するなら第1候補として挙がってくるのではないでしょうか。性能もいいですしね。また、デプロイしたアプリの実行ログが全て残っていて、少し感動しました（Heroku だと残らないので）。さらに Discord コミュニティがかなり活発的であり、質問したらすぐ回答が返ってきました。分からないことがあったら（自分で調べてみるのは前提ですが）、コミュニティに聞いてみるのがいいかもしれません。マニュアルはそこまで充実しておらず、少し分かりにくいなと感じましたが、使う上では問題ないでしょう。

## 参考
[Railway Documents](https://docs.railway.app)