Web予約のページがUIが悪かったので、スクリプトを使って予約した

（予約したかった）

#### どんな予約ページだったか
予約をする際、カレンダーに空き情報を表示せず、何月何日何時まで入力して初めて空き情報が確認できるサイトだった。
（しかも人気店で二ヶ月くらい埋まっている。途方もない）
15分単位で予約できて、営業時間は8時間なので、 1h(15*4) * 8h * 60day の 1920 通りから予約できる日を見つけなければならない（嫌です）
(URLを貼りたいが、問題があるかもしれないので一旦保留）

#### 作ったもの
timestampをインクリメントしながら、その値でURLを作成しサーバーにリクエストを送ることで予約の可否を確認するスクリプト。

### 解決のステップ

#### 1 ループさせたいpath の特定 
まず予約コースが複数あるので、単純に予約する際に用いているget を送ると大きなjson が返ってくる。どうにかして、目当てのコースだけが表示できる検索方法を探し、request の内容を保存する
(最悪、絞れなくても 大きなjson からお目当てのコースのid を特定した後に grep　等を組み合わせることでも解決できる。自分はid が特定できなかった。）

#### 2 見つけたパスに対してget を送ってみる
人数を一人以上に設定してください。とresponse が返ってきた。人数を設定する方法がわからない。

#### 3 クエリとクッキーヘッダをコピーしてcurl を送ってみる。
クエリをよく見ると、reservation%5Bnum_people_adult%5D=1
のような内容があった。これを1 から2 にすると上記のresponse は返ってこなくなった。時間もreservation%5Bstart_at_epoch%5D=1694857500
という項目があった。
また、私はこの時点でちゃっかり解決したのだが、query に authenticity_token という変数が含まれていて、これをコピーしたのもリクエストを成功させる要因になった。

authenticity_tokenについては以下のサイトを参考にして意味を理解した。

https://zenn.dev/hogeisfuga/articles/e6abb1723c8271

一瞬、curl からのリクエストを弾くためのものなのかもしれないと尻込みしてしまったが、問題なかった。CSRF対策のうち、今回のサイトのようにurl に token をset させるのは一番平易な対策らしいので、他の方法だと拒まれるのかもしれない。

ヘッダのコピーはresponse の内容に影響を与えなかった。でも情報を保存していそうなところをコピーした狙いは良かったと思う

#### 4 timestamp 周りの情報を収集
時間指定するためのクエリは特定できたので、そのtimestamp がどういったルールで管理されているかを特定する。今回は、フォームに日付を入力するたびに、ページがその日の時間表をget していたので利用した。また、予約時間を15分ずつ変えてみたり、1日またいだりしてクエリの値を確認した。

#### 5 実際にスクリプトを書いてみる
```check_reservable_.sh
#!/bin/bash
# 定数を定義
BASIC_URL="https://example.com/available?authenticity_token=aaa&reservation%5Bstart_at_epoch%5D="
LAST="&reservation%5Bnum_people_adult%5D=1"

# TIMESTAMP の初期値と最終値を定義
start_timestamp=1697182200
end_timestamp=1700033400

# TIMESTAMP の値が最終値を超えるまでループを実行
while [ "$start_timestamp" -le "$end_timestamp" ]; do
  # curl コマンドを実行
  curl "${BASIC_URL}${start_timestamp}${LAST}"

  echo ": ${start_timestamp}"

  # TIMESTAMP の値をインクリメント
  start_timestamp=$((start_timestamp + 900))

done
```

二ヶ月先までしか予約は出来ないので、二ヶ月後のタイムスタンプを終了条件にcurl を実行した。

#### 6 微調整
リクエストを高速に行うと、サーバーが一定時間空のレスポンスを返してくるようになった。
--> 空のresponse が送られてきたらsleep 1 をして熱りが覚めるのを待つ

それでも空のレスポンスを送られる時間が長くて効率が落ちた
--> 毎リクエストで sleep 0.1 した

インクリメントする中で、コースの予約対象時間外のリクエストを送ってしまっていた。
--> python で、timestamp を引数に予約時間かどうかを判定するスクリプトを書いた。このスクリプトの返り値がエラーだった場合、curl は送らずインクリメントする。これで、サーバーに無駄なリクエストを送らずに済む



最終的なスクリプト

```check_reservable_.sh
#!/bin/bash

# 引数が指定されているか確認
if [ "$#" -ne 1 ]; then
    echo "Usage: $0 <auth_token>"
    exit 1
fi

auth_token="$1"

# 定数を定義
BASIC_URL="https://example.com/available?authenticity_token=${auth_token}&reservation%5Bstart_at_epoch%5D="
LAST="&reservation%5Bnum_people_adult%5D=2"

# TIMESTAMP の初期値と最終値を定義
start_timestamp=1697182200
end_timestamp=1700033400

while [ "$start_timestamp" -le "$end_timestamp" ]; do

  # Pythonスクリプトを実行して終了ステータスを確認
  python3 checkOpen.py $start_timestamp

  # $? は直前のコマンドの終了ステータスを取得する特殊変数\
  if [ $? -eq 0 ]; then  # 営業時間内の場合
      # curl コマンドを実行して結果を変数に保存
      response=$(curl -s "${BASIC_URL}${start_timestamp}${LAST}")
      # その他の処理...
    # responseが空かどうかを確認
    if [ -z "$response" ]; then
      echo "Empty response received. Retrying after 1 second..."
      sleep 1
      continue
    else
      echo "$start_timestamp : $response"
    fi
  fi

  # TIMESTAMP の値をインクリメント
  start_timestamp=$((start_timestamp + 900))

done

```

```checkOpen.py
import sys
from datetime import datetime, timedelta

# 基準のタイムスタンプと日時
BASE_TIMESTAMP = 1699947000
BASE_DATE = datetime(2023, 11, 14, 16, 30)

def convert_timestamp_to_date(timestamp):
    # 差分の秒数を計算
    delta_seconds = timestamp - BASE_TIMESTAMP

    # 基準日時に差分を加算して新しい日時を取得
    new_date = BASE_DATE + timedelta(seconds=delta_seconds)

    # yy/mm/dd HH:MM 形式で日時を返す
    return new_date.strftime('%y/%m/%d %H:%M')

# テスト

import sys

if len(sys.argv) < 2:
    print("Usage: script_name.py [timestamp]")
    sys.exit(1)

# 引数からタイムスタンプを取得
try:
    timestamp = int(sys.argv[1])
except ValueError:
    print("Invalid timestamp provided.")
    sys.exit(1)

formatted_date = convert_timestamp_to_date(timestamp)
# print(formatted_date)  # 23/11/15 16:30

# 時間と分を取得
hour_minute = datetime.fromtimestamp(timestamp).time()

# 16:30 ~ 21:00 の間のチェック
if datetime.strptime("16:30", "%H:%M").time() <= hour_minute <= datetime.strptime("21:00", "%H:%M").time():
    sys.exit(0)
else:
    sys.exit(1)

```

#### 感想
python のスクリプトでテストしている中で サーバから429 response (送りすぎ拒否) を受け取ってしまったため、実はまだ予約できていない。2時間くらい待ってもアクセスできない。悲しい。
予約のUI悪いページって、なんとなく意図的な雰囲気を感じるんだけどどうなんだろう。予約取りづらい（物理）ページに愛着わいてしまう自分がいる。
なんにせよスクリプトを使えば予約上手になれそうです。めでたしmdtc