bookersの投稿API（python）を使ってみた

# bookersについて
[有料予想・有料記事を登録できるサイト](https://bookers.tech/)
記事投稿できるAPIが提供されている為、競馬予想から競馬予想投稿までを自動化してみました。

# 投稿API用シークレットキー
bookersにログイン後、アカウント設定の最下部の**投稿API用シークレットキー**を取得する。
![投稿API用シークレットキー](https://storage.googleapis.com/zenn-user-upload/8edc193121b0-20230204.png)

# 記事一覧取得[GET]
エンドポイント
/api/postcreate/

pythonサンプルコード
```python
import requests
# Tokenに投稿API用シークレットキーを指定する
headers = {
    'Authorization': 'Token *****************',
}
res = requests.get('https://bookers.tech/api/postcreate/', headers=headers)
print(res.json())
```
最新20件の記事詳細と次の20件取得用のURLが返却される。

レスポンス
- 成功:200
- 失敗:403

JSON構造体
```json
{
    "count": 2,
    "next": None,
    "previous": None,
    "results": [
        {
            "uuid": "99999999-9999-9999-9999-999999999999",
            "title": "2023/2/6（月）厳選5鞍",
            "category": 2,
            "description": "有料記事の無料部分",
            "text": "有料記事の有料部分",
            "price": 100,
            "limited_number": 0,
            "is_shared": false,
            "signature": "99999999-9999-9999-9999-999999999999"
        },
        {
            "uuid": "99999999-9999-9999-9999-999999999999",
            "title": "テスト2",
            "category": 2,
            "description": "有料記事の無料部分",
            "text": "有料記事の有料部分",
            "price": 100,
            "limited_number": 0,
            "is_shared": false,
            "signature": "99999999-9999-9999-9999-999999999999"
        }
    ]
}
```
リソースで使用されているプロパティ
| 項目名 | 型 | 説明 |
| ---- | ---- | ---- |
| uuid | str | uuid |
| title | str(UTF-8) | タイトル（255文字まで） |
| category | int | カテゴリ<br>（1:その他,2:競馬,3:競艇,4:競輪,5:テクノロジー,6:ビジネス,7:漫画,8:創作,9:音楽,10:ライフスタイル,11:コラム） |
| description | str(UTF-8) | 有料記事の無料部分。**markdown記法対応** |
| text | str(UTF-8) | 有料記事の有料部分。**markdown記法対応** |
| price | int<0～50000> | 価格<br>2023.02.05現在は、10000まで設定可能 |
| limited_number | int<1～> | 有料販売記事数 |
| is_shared| bool | 無料プレゼントURLの有効化フラグ |
| signature| str | 署名 |

# 記事詳細取得[GET]
uuidを指定することで、単一記事の詳細情報を取得できる。

エンドポイント
/api/postupdate/

pythonサンプルコード
```python
import requests
# Tokenに投稿API用シークレットキーを指定する
headers = {
    'Authorization': 'Token *****************',
}
# uuid 99999999-9999-9999-9999-999999999999 とした場合
requests.get('https://bookers.tech/api/postupdate/99999999-9999-9999-9999-999999999999/',
    headers=headers)
```
レスポンス
- 成功:200
- 失敗:403

# 記事投稿[POST]
記事投稿に成功した場合は、採番されたuuidが返却される。

エンドポイント
/api/postcreate/

pythonサンプルコード
```python
import requests
import json
# Tokenに投稿API用シークレットキーを指定する
headers = {
    'Authorization': 'Token ******************',
    'Content-Type': 'application/json',
}
data = {
    "title": "テスト2",
    "category": 2,
    "description": "有料記事の無料部分",
    "text": "有料記事の有料部分",
    "price": 100,
    "is_shared": False,
}
res = requests.post('https://bookers.tech/api/postcreate/',
    headers=headers,
    data=json.dumps(data))
```
レスポンス
- 成功:201
- 失敗:400

# 記事更新[PATCH]
uuidを指定し、記事の詳細情報を更新できる。

エンドポイント
/api/postupdate/

pythonサンプルコード（※タイトルを更新するサンプル）
```python
import requests
import json
# Tokenに投稿API用シークレットキーを指定する
headers = {
    'Authorization': 'Token ******************',
    'Content-Type': 'application/json',
}
data = {
    'title' :'元小池の競馬AI予想！2023/2/5（日）厳選5鞍（Twitterのいいねで無料）',
}
# uuid 99999999-9999-9999-9999-999999999999 とした場合
res = requests.patch('https://bookers.tech/api/postupdate/99999999-9999-9999-9999-999999999999/',
    headers=headers,
    data=json.dumps(data))
```
レスポンス
- 成功:200
- 失敗:403

# 自作メソッド
記事一覧取得では、最大20件取得できる仕様のため、全件表示できる自作メソッドと記事投稿のメソッドを作成。
```python
import json
import requests
import time

def get_article_list() -> None:
    """
    記事一覧を表示する。
    """
    print('get article list start')

    # Tokenに投稿API用シークレットキーを指定する
    headers = {
        'Authorization': 'Token *****************',
    }
    # APIリクエストURL
    url = 'https://bookers.tech/api/postcreate/'

    # カウント
    count = 1

    while True:
        # 1秒スリープ
        time.sleep(1)
        # APIリクエスト（最新20件の記事詳細と次の20件取得用のURLが返却される）
        res = requests.get(url, headers=headers)

        # 無事レスポンスが返ってきたら中身を出力
        if res.status_code == 200:
            # JSONデータに変換
            json_data = res.json()

            # キーを表示
            print(json_data.keys())

            print(json_data['count'])
            print(json_data['next'])
            print(json_data['previous'])

            # リクエストURLを更新
            url = json_data['next']

            # JSONデータを表示
            for json_obj  in json_data['results']:
                print(str(count) + '件目')
                # JSONの表示
                __display_json(json_obj)
                count += 1

            # リクエストURLがNoneの場合は、ループ終了
            if url == None:
                break
        else:
            print('取得失敗' + res.text)
            break

    print('get article list end')

def __display_json(json_obj: dict) -> None:
    """
    JSONを表示する

    Parameters
    ----------
    json_obj : dict
        JSONオブジェクト
    """
    # 各項目を表示
    print('=================================================================')
    print('uuid:' + json_obj['uuid'])
    print('タイトル（title）:' + json_obj['title'])
    print('カテゴリ（category）:' + str(json_obj['category']))
    print('無料記事部分（description）:' + json_obj['description'])
    print('有料記事部分（text）:' + json_obj['text'])
    print('価格（price）:' + str(json_obj['price']))
    print('有料販売記事数（limited_number）:' + str(json_obj['limited_number']))
    print('無料プレゼントURLの有効化フラグ（is_shared）:' + str(json_obj['is_shared']))
    print('著名（signature）:' + json_obj['signature'])
    print('=================================================================')

def create_article(title: str,
        free_text: str,
        paid_text: str,
        category=2,
        price=100,
        is_shared=False) -> None:
    """
    記事を投稿する。

    Parameters
    ----------
    title : str
        記事のタイトル
    free_text : str
        有料記事の無料記事部分の内容
    paid_text : str
        有料記事の有料記事部分の内容
    category : int
        記事のカテゴリ（デフォルトは2）
        1:その他,2:競馬,3:競艇,4:競輪,5:テクノロジー,
        6:ビジネス,7:漫画,8:創作,9:音楽,10:ライフスタイル,
        11:コラム
    price : int
        記事の価格（デフォルトは100円）
    is_shared : bool
        無料プレゼントURLの有効化フラグ。（デフォルトはFalse）
    """
    print('create article start')

    # Tokenに投稿API用シークレットキーを指定する
    headers = {
        'Authorization': 'Token *****************',
        'Content-Type': 'application/json',
    }
    data = {
        'title': title,
        'category': category,
        'description': free_text,
        'text': paid_text,
        'price': price,
        'is_shared': is_shared,
    }
    res = requests.post('https://bookers.tech/api/postcreate/',
        headers=headers,
        data=json.dumps(data))

    # 無事レスポンスが返ってきたら中身を出力
    if res.status_code == 201:
        print('記事作成成功')
        # JSONデータに変換
        json_obj = res.json()
        # キーを表示
        print(json_obj.keys())
        # JSONの表示
        __display_json(json_obj)
    else:
        print('記事作成失敗' + res.text)

    print('create article end')
```

# エンドポイントのまとめ
| エンドポイント | 説明 |
| ---- | ---- |
| /api/postcreate/ | 記事一覧取得[GET]、記事作成[POST] |
| /api/postupdate/ | 記事詳細取得[GET]、記事更新[PATCH] |
| /api/postdlcupdate/ | DLC（ダウンロードコンテンツ）追加[PATCH] |
| /api/magazinepostcreate/ | マガジン/記事の紐づけ[POST] |
| /api/magazinepostdelete/ | マガジン/記事の紐づけ解除[POST]|
| /api/postcampaignupdate/ | Twitter拡散キャンペーン追加[PATCH] |
| /api/postdraftcreate/ | 下書き保存[POST] <br>挙動は、既存の記事作成[POST]APIと同様<br>また、下書き記事を記事更新APIで更新すると記事が公開される仕様

今回は、`/api/postcreate/`と`/api/postupdate/`について利用してみたが、他にも機能についても今後、検証したいと思います。

# APIを使用してみての感想
~~1. 非公開（下書き）の状態で、POSTする機能が欲しい~~
2. 投稿予約機能が欲しい（画面UIにも存在しない機能である）
~~3. 記事一覧取得[GET]にて画面UIにて削除した記事が返却される（仕様通りではないと思われる）~~
~~4. プロパティの`is_shared`について、動作確認を行ったが、どこの要素に効くパラメタなのか分からなかった~~

1については、2023/02/06 API提供済み。
2については、1が機能追加されることで、API利用側の制御にて解決できる。
3については、2023/02/06 修正済み。
4については、無料プレゼントURLの有効化フラグであることが分かった。
# 参考

https://bookers.tech/post/8ba5c8bb-f3a1-4e99-be1d-03ba0f72b324/

https://bookers.tech/post/14d26216-5844-4fd9-8031-5f9806db446d/

https://bookers.tech/post/9ac5fd7b-33f0-4775-a5f8-7ad8edd21d08/

https://bookers.tech/post/edf1cc28-82fd-4457-a139-a61e54165fe8/

https://bookers.tech/post/256507d0-c71b-4cf2-8dd0-47f439dec4d6/

https://bookers.tech/post/af13a3f0-8aae-45bb-9587-945b9b9d7908/