Red言語でChatWorkのデータをローカルに保存する

# 弊社、ChatWorkやめるってよ

会社でChatWorkを使っていたのですが、Microsoft Teamsに移行するとお達しがありました。過去のデータが見れなくなっちゃうといつか困るかもしれないので、WEB APIを使ってバックアップを取ろうと考えました。残念ながらChatWork側のWEB API仕様としてメッセージは直近100件分しか取得できない[^1]のですが、まあないよりはマシということで・・・

[^1]: [ChatWork APIドキュメント](https://developer.chatwork.com/ja/endpoint_rooms.html#GET-rooms-room_id-messages)

せっかくなので推しの[Red言語](https://github.com/red/red)でやるぞ！

## ソースコード

ひとまずの出来上がり品がこちら

[ChatWorkKeeper](https://github.com/koba-yu/ChatWorkKeeper)

GitHubの[リリースページ](https://github.com/koba-yu/ChatWorkKeeper/releases)でバイナリとしても配布しています。色々と機能追加・リファクタリングの余地はありますが、残念ながらすでに会社がChatWork解約済みのためリポジトリの更新は停止する予定です。

## RedでWEB APIが関わるツールを実装する魅力

Redでアプリを作るのはちょいちょいやっていましたが、社内の他の人にも使ってもらったのは今回初めてでした。問題なく他の人の環境でも動いてくれて満足しています。

RedはまだVer1.0になっておらず、互換性のない変更も予想されるため、プロダクションで使用するのはリスクがある状況です。一方でちょっとしたツールを作成するには十分な能力がすでにあります。Redは特にCSV、XML、JSONなどのデータを処理するときや、今回のようにWEB APIが絡むツールをカジュアルに開発するにはとても便利&快適に開発できる言語です。

**でもデータを処理するって普通なんでもそうじゃね？つまり最高だってことですわ。**

以下に、なぜRedが便利&快適なのかについて、まとめてみました。

[1. 標準REPL & 公式VS Code拡張機能で十分開発できる](#1標準repl--公式vs-code拡張機能で十分開発できる)
[2.ノートPCで十分開発できる](#2ノートpcで十分開発できる)
[3.記号が少ない。Shiftキーを使う頻度が少ない](#3記号が少ないshiftキーを使う頻度が少ない)
[4.他の人の環境で簡単に動く](#4他の人の環境で簡単に動く)
[5.WEB APIの呼び出し＆ハンドリングが楽](#5web-apiの呼び出し--ハンドリングが楽)

### 1.標準REPL & 公式VS Code拡張機能で十分開発できる

Redは標準でREPLが付いています。また、公式の[VS Code拡張機能](https://github.com/red/VScode-extension)があるのでVS Code派の人はこれで十分でしょう。他のエディタ派の方でもRebolという言語のシンタックスハイライトにエディタが対応していれば概ね代替可能だと思います。

### 2.ノートPCで十分開発できる

上記と関連しますが、ごついでかいIDEでなくても開発できるので、それほど高スペックではないノートPCでも十分開発できます。自分はあまりプライベートであまりPCにお金をかけたくはないタイプで、安価なノートPCで開発していますが、Redでちょっとしたツールを作るのはノートPCでも十分可能です。

### 3.記号が少ない。Shiftキーを使う頻度が少ない

これが快適すぎてRedから離れられないのです・・・
ソースを見てもらうとお分かりいただけると思いますが、他の言語でよく使う記号、`,` `.` `:` `;` `{}` `()` `_`がとても少ないです。主に使う記号は`:` と `[]` ですが、この2つは日本語キーボードで打ちやすいですし、種類も少ないのでだいぶ快適です。Shiftキーを押す頻度が少ないのがとても楽。Shiftキー、部屋が寒いときとかミスタイプ増えませんか？笑

### 4.他の人の環境で簡単に動く

Redは単一バイナリにコンパイルでき、実行にランタイムのインストールは不要です。そのため、完成したツールを他の人に渡して使ってもらうのが非常に気楽で助かりました。

ちなみにファイルサイズですが、コンパイル直後は1.14MBありましたが、機械語のバイナリなので[UPX](https://upx.github.io)で圧縮可能です。実際に圧縮した結果は369KBでした。GitHubのリリースページは圧縮済みのものを付けています。1ファイルなので大きくても配布上そこまで問題ではないのですが、やはり軽いほうが気楽ではあります。

### 5.WEB APIの呼び出し & ハンドリングが楽

#### 呼び出しが楽

以下は[ChatWorkのチャット一覧を取得するAPI](https://developer.chatwork.com/ja/endpoint_rooms.html#GET-rooms)を呼び出しする簡単なサンプルです。他の言語で言うところのimportのようなものは不要で、REPLにすぐに打ち込めます。

```red
write https://api.chatwork.com/v2/rooms [
	get [X-ChatWorkToken: "APIキーの値"]
]
```

`get`の部分はHTTPメソッドの種類を書きます。その後ろでHTTPリクエストヘッダを`key: value`形式で指定します。実際のソースコードではもう少し複雑な書き方をしていますが、APIを試しに呼んでみて結果を見てみたりするのはとても楽です。

#### 呼び出し結果を保存しておける

Redは値もコードもいつでも簡単にファイルに保存できます。ここでは（ChatWorkのデータは載せられないので）サンプルとして[京都市オープンデータサイト](https://data.city.kyoto.lg.jp/riyou/api)のAPIを使ったときのレスポンスデータで説明します。以下に実行するコードの例と保存されるファイルの例を記載します。

```red
; リクエストヘッダがないAPIはreadでより簡単に呼べる
url: https://data.city.kyoto.lg.jp/API/action/datastore/search.json?resource_id=f14b57c2-48dd-4aa7-b754-a4f4ac340f2d&limit=3

; レスポンスボディのみ保存
save %response.red read url
```

[結果ファイル](https://gist.github.com/koba-yu/a510bca7d561124cbe79104a335405fc)

```red
; レスポンスステータスコードやヘッダも保存
save %response-info.red read/info url
```

[結果ファイル](https://gist.github.com/koba-yu/2d37eb38bfd838ae31cc586eb4e63df3)

```red
; レスポンスボディをRedのデータフォーマットに変換して保存
save %response-json.red load/as read url 'json
```

[結果ファイル](https://gist.github.com/koba-yu/85036349265a41983e9d8287fb912381)

RedのデータフォーマットはJSONよりも見やすく、そのままRedの関数で絞り込みなどもできるため、よく知らないAPIの中身を見ながら開発する際には便利です[^2]。

[^2]: Redはプログラミング言語であると同時にデータフォーマットでもあるので、こういうことができます。詳細は[こちらの記事](https://qiita.com/koba-a-koba/items/e4804b847e64c3434a77)もご参照ください。

また、上記のように結果をファイルとして保存しておくことで、返ってくる結果に対してどのように実装するのかを保存したファイルを見ながら検討できます。分岐に対応するようなレスポンスは残しておくと、後で「この実装ってなんでこうしてたんだっけ・・・」とならずに済み、開発時のイライラの低減に繋がります。

#### リトライ&待機処理が簡単に書ける

ChatWorkのAPIは5分間で300回までしか呼び出しできない制限があります。制限に達してしまうとステータスコードが429で返るようになり、レスポンスヘッダに入っている`X-RateLimit-Reset`の時間まで待機した後に再実行が必要になります。下記はこの内容を簡易的に実装したコードです。

```red
; APIを呼ぶコードをブロックとして保持
api-call: [write/info https://api.chatwork.com/v2/rooms [
		get [X-ChatWorkToken: "APIキーの値"]
	]
]

; 1回目のAPI呼び出し
response: do api-call

; ステータスが429だったら、
if response/1 = 429 [

	; X-RateLimit-Resetの時間（UNIX時間） + 1秒後の時間
	reset-time: (to-date to-integer response/2/X-RateLimit-Reset) + 0:0:1

	; 現在時刻との差だけ待つ（つまり時間になるまで待つ）
	wait difference reset-time now/utc

	; APIをもう一度実行
	response: do api-call
]
```

とても簡潔に書けますね。実際にはAPI呼び出し時にタイムアウトなどでエラーが発生することがあるので、そのハンドリングが必要です。これは`do`を`try`関数に変えて、返ってきた結果が`error!`型かをチェックすればOKです。

```red
; APIを呼ぶコードをブロックとして保持
api-call: [write/info https://api.chatwork.com/v2/rooms [
		get [X-ChatWorkToken: "APIキーの値"]
	]
]

; エラー処理用のコードブロック
handling: [if error? response [ "エラー処理をここに書く（ログを書いて終了など）" ]]

; 1回目のAPI呼び出し
response: try api-call

; エラー処理
do handling

; ステータスが429だったら、
if response/1 = 429 [

	; X-RateLimit-Resetの時間（UNIX時間） + 1秒後の時間
	reset-time: (to-date to-integer response/2/X-RateLimit-Reset) + 0:0:1

	; 現在時刻との差だけ待つ（つまり時間になるまで待つ）
	wait difference reset-time now/utc

	; APIをもう一度実行
	response: do api-call

	; エラー処理
	do handling
]
```

一般的な言語では`try`は構文ですが、Redでは関数なのでブロックを渡せるというのが非常に柔軟です。

## まとめ

いろいろ書きましたがRed楽しいのでみなさんも機会があればぜひ！