0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Chatworkシリーズ #20】17記事書いて見えた、Chatwork APIエコシステムに足りないもの

0
Last updated at Posted at 2026-03-24

17本目の記事を公開した夜、ふと手が止まった。

Chatwork APIで未読管理をやった。FAQ抽出もやった。人間関係マップも、Webhookも、ファイルAPIも、タスクAPIも。MCP接続、n8n巡回、果てはClaude Code Channelsまで。「Chatwork APIでできること」は、たぶん相当やり尽くした方だと思う。

だからこそ見えてきた。「ここが足りない」が。

これは批判じゃない。17本書くほど使い倒しているユーザーからの、率直な「こうなったら最高なのに」だ。

足りないもの①: スレッド/リプライのAPI

Chatworkにはリプライ機能がある。

[rp aid=12345 to=67890-1234567890]
そのやり方でOKです

GUIからは普通に使える。問題はAPIだ。

GET /rooms/{room_id}/messages でメッセージは取れる。だが「このメッセージへのリプライ一覧」を取得する手段がない。

# こういうエンドポイントが存在しない
curl -X GET "https://api.chatwork.com/v2/rooms/{room_id}/messages/{message_id}/replies" \
  -H "X-ChatworkToken: $TOKEN"

#9でFAQ抽出パイプラインを作ったとき、これが地味に痛かった。質問メッセージとその回答リプライを紐付けたかったのに、APIからはリプライチェーンを辿れない。結局、メッセージ本文に含まれる [rp aid=xxx to=xxx] タグを正規表現でパースして、自前で紐付けた。

import re

def extract_reply_target(body: str) -> dict | None:
    """メッセージ本文からリプライ先を抽出する"""
    match = re.search(r'\[rp aid=(\d+) to=(\d+)-(\d+)\]', body)
    if match:
        return {
            "account_id": int(match.group(1)),
            "room_id": int(match.group(2)),
            "message_id": match.group(3)
        }
    return None

動く。動くが、公式サポートされた方法じゃない。Chatworkが [rp] タグの仕様を変えたら壊れる。

Slackには conversations.replies がある。thread_tsを指定すれば、そのスレッドの全リプライが返ってくる。これだけで会話の文脈を機械的に追える。

# Slackならこれでスレッドが取れる
curl -X POST "https://slack.com/api/conversations.replies" \
  -H "Authorization: Bearer $SLACK_TOKEN" \
  -d "channel=C1234567890" \
  -d "ts=1234567890.123456"

Chatworkにも、これに相当するものが欲しい。リプライの構造化はチャットツールのAPIとして基本機能だと思っている。

足りないもの②: リアクション(絵文字リアクション)

2023年ごろ、Chatworkにリアクション機能が追加された。メッセージに👍や❤️を付けられるようになった。

GUIでは普通に使えるのだが、APIにリアクション関連のエンドポイントが一切ない。

# こういうのが欲しい
# リアクション取得
GET /rooms/{room_id}/messages/{message_id}/reactions

# リアクション追加
POST /rooms/{room_id}/messages/{message_id}/reactions
{"emoji": "thumbsup"}

リアクションは「読んだ」のシグナルだ。既読(read)よりも軽い「了解」「OK」を表現できる。メールの開封通知に近い、だが押しつけがましくない。

自動化の観点で言うと、リアクション集計ができたらかなり面白い。

  • 重要な告知メッセージに、何人がリアクションしたか
  • チーム内の「暗黙の合意」をリアクション数で可視化する
  • リアクションゼロのメッセージ=誰も見ていない可能性がある

「見た」をAPIで取れないのは、地味にもったいない。

足りないもの③: メッセージ検索API

Chatworkの画面には検索窓がある。キーワードを入れれば過去メッセージを探せる。

だが、APIに検索エンドポイントがない。

# こういうのが存在しない
GET /rooms/{room_id}/messages/search?q=keyword&from=2026-01-01

#11で詳しく書いたが、GET /rooms/{room_id}/messages は最新100件しか返さない。過去メッセージを全件取得するには force=1 パラメータを付けて何度もポーリングする必要がある。

# 100件ずつ遡る苦行
curl -s "https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1" \
  -H "X-ChatworkToken: $TOKEN"
# → 最大100件。もっと古いのが欲しければ、最古のmessage_idを控えて再リクエスト
# → これを繰り返す。数千件あると数十回のAPIコール

FAQ抽出(#9)では、8ルーム・数千件のメッセージをこの方法で全件取得した。動くには動く。だがレート制限(300回/5分)との戦いになる。

Slackの search.messages はこう書ける。

curl -X GET "https://slack.com/api/search.messages?query=keyword&sort=timestamp" \
  -H "Authorization: Bearer $SLACK_TOKEN"

一発で目的のメッセージに辿り着ける。全件取得してからgrepするのと、検索APIを叩くのでは、コスト感覚がまるで違う。

正直、これが一番「実装されたら世界が変わる」機能だと思っている。

足りないもの④: Webhookの種類

#10でWebhook署名検証の記事を書いた。Chatwork Webhookは動く。動くのだが、種類が足りない。

現状のWebhookイベントはこのあたり。

  • メッセージ作成
  • メンバー変更(追加・削除)

足りないのはこの3つ。

タスク期限到来Webhook。 タスクの期限が来たことをWebhookで通知してほしい。現状は自前でポーリングしてタスク一覧を取得し、期限を比較するしかない。

# 現状: 自前でタスク期限チェック
import datetime

tasks = get_room_tasks(room_id)
now = datetime.datetime.now()

for task in tasks:
    if task["limit_time"] and task["limit_time"] < now.timestamp():
        if task["status"] == "open":
            notify(f"期限切れタスク: {task['body']}")

ファイルアップロードWebhook。 ファイルが上がったら自動でバックアップする、という処理を組みたい。現状はメッセージ作成Webhookを受け取って、メッセージ本文に [dw] タグ(ダウンロードリンク)が含まれているかで判定するしかない。

メンション通知Webhook。 「自分宛のメンションだけを拾いたい」ニーズは大きいはずだ。全メッセージを受け取ってから [To:自分のaccount_id] をフィルタリングする手間が消える。

// 現状: 全メッセージを受けてからフィルタ
app.post('/webhook', (req, res) => {
  const body = req.body.webhook_event.body;
  const myAccountId = 'XXXXXXX';

  if (body.includes(`[To:${myAccountId}]`)) {
    // 自分宛の処理
    handleMention(req.body);
  }
  // それ以外は捨てる ← この「捨てる」が無駄
  res.sendStatus(200);
});

Webhook1種類につきAPIコール1回分のフィルタリングコストが消える。チリも積もれば山になる話だ。

足りないもの⑤: WebSocket/リアルタイム接続

現状、Chatwork APIにリアルタイム接続の手段がない。

#17でClaude Code Channelsを実装したとき、メッセージ検知は10秒間隔のポーリングだった。

// 10秒ごとにメッセージを取りに行く
setInterval(async () => {
  const messages = await fetchNewMessages(roomId);
  if (messages.length > 0) {
    await processMessages(messages);
  }
}, 10000);

10秒。人間の会話ならギリギリ許容範囲だ。だが「リアルタイム」とは呼べない。

ポーリング間隔を短くすると、レート制限に引っかかる。300回/5分=1回/秒が上限。複数ルームを監視すると、すぐに枠を食い尽くす。間隔を長くすると、反応が遅くなる。

このジレンマをWebSocketは根本解決する。

// WebSocketがあったらこうなる(妄想)
const ws = new WebSocket('wss://api.chatwork.com/v2/stream');
ws.on('open', () => {
  ws.send(JSON.stringify({
    type: 'subscribe',
    rooms: [roomId],
    events: ['message_created', 'task_updated']
  }));
});

ws.on('message', (data) => {
  const event = JSON.parse(data);
  // 即座にイベントが届く。ポーリング不要
  handleEvent(event);
});

Slackの socket_mode がまさにこれだ。接続を張りっぱなしにして、イベントが来たら即座に処理する。サーバーの負荷もクライアントの負荷も、ポーリングより遥かに小さい。

Claude Code Channelsを「リアルタイムチャット」と呼ぶには、正直10秒のラグが引っかかっていた。WebSocketがあれば、このラグが消える。

足りないもの⑥: リッチテキスト/マークダウン対応

Chatworkのメッセージフォーマットは独自記法だ。

[info][title]お知らせ[/title]
本日のミーティングは15時からです。
[hr]
詳細は[b]添付ファイル[/b]を参照してください。
[code]
const greeting = "hello";
[/code]
[/info]

マークダウンじゃない。**太字** は効かない。[b]太字[/b] と書く必要がある。

APIからメッセージを投稿するとき、毎回Chatwork記法に変換するラッパーを書くことになる。

def markdown_to_chatwork(md: str) -> str:
    """MarkdownをChatwork記法に変換する"""
    text = md
    # 太字
    text = re.sub(r'\*\*(.*?)\*\*', r'[b]\1[/b]', text)
    # コードブロック
    text = re.sub(r'```(\w*)\n(.*?)```', r'[code]\2[/code]', text, flags=re.DOTALL)
    # 水平線
    text = text.replace('---', '[hr]')
    # インフォボックス(Chatwork独自)
    # → マークダウンには対応するものがないので、独自ルールで変換
    return text

MCP経由でClaude Codeからメッセージを送るとき、Claude Codeはマークダウンで考える。それをChatwork記法に変換してから投稿する。この変換レイヤーが、地味に面倒だ。

マークダウンをネイティブサポートしてくれたら、この変換が丸ごと消える。もしくは、APIリクエストに format: "markdown" を指定したら、サーバー側で変換してくれるだけでもいい。

足りているもの(フェアに書く)

ここまで「足りない」を並べた。フェアじゃないので、足りているものも書く。

基本のCRUDは揃っている。 メッセージ・ルーム・メンバー・タスク・ファイル。必要な操作は一通りできる。17記事書けたのは、この基盤があったからだ。

認証がシンプル。 APIトークンをヘッダに載せるだけ。

curl -H "X-ChatworkToken: $TOKEN" \
  "https://api.chatwork.com/v2/me"

OAuth2の認証フローを組む必要がない。個人利用・チーム利用なら、これで十分だ。セットアップに5分かからない。

レート制限が明確。 300回/5分。レスポンスヘッダに残り回数が入っている。

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1711234567

暗黙のレート制限で急に429が返ってくるAPIとは違う。設計しやすい。

JSONが素直。 ネストが浅く、キー名が直感的。パースで悩むことがほぼない。

{
  "message_id": "12345",
  "account": {
    "account_id": 67890,
    "name": "田中太郎"
  },
  "body": "メッセージ本文",
  "send_time": 1711234567,
  "update_time": 0
}

send_time がUnixタイムスタンプなのは好みが分かれるが、一貫していて扱いやすい。

ドキュメントが日本語で充実。 これは日本企業の強みだ。英語のドキュメントを読み解く必要がない。APIリファレンスに日本語のサンプルコードが付いている。

「足りない」から自分で作った17本

振り返ると、シリーズの半分以上は「足りないものを自分で作った」記事だ。

#17のClaude Code Channels。WebSocketもリアルタイム接続もなかったから、ポーリングで自作した。

#9のFAQ抽出パイプライン。検索APIがなかったから、全件取得してNLPで分類した。

#14のn8n全ルーム巡回。全ルーム横断の未読管理がなかったから、n8nで組んだ。

#12の既読制御。既読管理が思い通りにいかなかったから、readunread を組み合わせて自前で制御した。

APIの「足りない」は「自分の出番」だ。足りていたら記事は17本も書けなかった。

足りないから調べる。調べるから深く理解する。理解するから記事が書ける。記事を書くからまた新しい「足りない」が見つかる。

この循環が回っている限り、シリーズは続く。

最後に

17本書いて、一番感じたのは「Chatwork APIは過不足ない基本機能がある」ということだ。過不足ないからこそ、その上に自分で積める。

足りないものを数えるのは簡単だ。でも、足りないところを自分で埋めてきた17記事分の経験の方が、よほど価値がある。

ただ——スレッドAPIだけは本当に欲しい。

Chatworkさん、お願いします。

Chatworkシリーズ


Chatworkシリーズ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?