Slack APIでメッセージのURLをもとにそのメッセージに返信する

# やりたいこと

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1022607/fb0184a0-fea7-93ec-623a-529d76dd0afe.png)

Slackメッセージの三点メニューから取得できるリンクのみを使って、任意のタイミングでSlack APIを使ってそのメッセージに返信したい。

> [How to retrieve Slack messages via API identified by permalink? \- Stack Overflow](https://stackoverflow.com/questions/61190746/how-to-retrieve-slack-messages-via-api-identified-by-permalink)

この記事を参考に、今回の記事を作成しました。
一部JavaScriptを使っていますが、メッセージリンク（URL）の加工をしているだけなので、ほかの言語でも同じロジックを組めば再現できると思います。

# 使用するメッセージリンクのサンプル

Slackのリンクは下記の構成であることを想定しています。もし、違ったパラメータ等を含んでいるときはうまく調整しながら参考にしてください。

> 👇親スレッドのリンクをコピーしたとき
> `https://<サブドメイン>.slack.com/archives/<Conversation ID>/p<ドットを除いたメッセージの作成日時のタイムスタンプ>`
> 
> 👇親スレッドに返信されたメッセージのリンクをコピーしたとき
> `https://<サブドメイン>.slack.com/archives/<Conversation ID>/p<ドットを除いたメッセージの作成日時のタイムスタンプ>?thread_ts=<スレッドのメッセージの作成日時のタイムスタンプ ドット付き>&cid=<Conversation ID>`

上記どちらのリンクを使った場合でも、返信は親スレッドに対して行う想定でロジックを組み立てていきます。

| 名称 | 説明 |
|:----------:|:-----------|
| サブドメイン | slackワークスペース名        |
| Conversation ID／チャンネルID     | メッセージが存在するチャンネルのID。チャンネルIDと呼ぶほうが多い？      |
| ドットを除いたメッセージの作成日時のタイムスタンプ       | メッセージが作成されたときの小数点以下第6位まであるUNIX時刻からドットを除いた値<br>例：1701232196111111の場合、2023年11月29日13時29分56.111111秒<br>Slack APIドキュメントではドットが付いているUNIX時刻の値を `thread_ts` と呼んでおり、メッセージのIDのようなもののようです。        |

# 事前準備（Slackアプリ作成）

> [Slackでアプリケーションを作成し、OAuth Tokenを発行するまで \#Slack \- Qiita](https://qiita.com/kobayashi_ryo/items/a194e620b49edad27364)

こちらの記事を参考にSlackアプリケーションを作成します。
Slackアプリケーションにアプリ作成者の権限を委任しておくことで、あとの作業で行うメッセージへの返信ができるようになると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1022607/d65961e1-ba3b-2034-7d9d-f0ce42c41dcc.png)

写真ではほかにも権限つけていますが、今回の用途では `chat:write` を「User Token Scopes」に指定します。（ほかの4つは不要です）

`Install to Workspace` ボタンを最後に押して、許可しておきます。

最後に「OAuth Tokens for Your Workspace」から「User OAuth Token」の値をコピーしておきます。

# メッセージに返信する（JavaScript）

下記 `extractChannelIDThreadTS` 関数を参考に、リンクからチャンネルIDとメッセージの作成日時（UNIX時刻）を取得します。
この2つの値があればメッセージに返信することができます。

```javascript
/**
 * extractChannelIDThreadTS関数
 * SlackメッセージのURL（パーマリンク）から、チャンネルIDとメッセージのts（タイムスタンプ）を取得します。
 * 
 * @param {string} permaLink slackメッセージのURL
 * @returns {channelId, threadTS}
 */
function extractChannelIDThreadTS(permaLink) {

  // trueの場合、permaLinkがスレッドの親のURLではなく、スレッドに返信したメッセージのURLだと判断する
  const isIncludesthread_ts = permaLink.includes('thread_ts')

  // URLをスラッシュで分解
  const splitPermaLink = permaLink.substring(8).split('/')

  // チャンネルID
  const channelId = splitPermaLink[2]

  // 返信するスレッドの親メッセージのts_thread
  let threadTS = null

  if (isIncludesthread_ts) {
      const indexOfthread_ts = permaLink.indexOf('thread_ts=') + 10
      const thread_ts_length = 17
      threadTS = permaLink.substring(indexOfthread_ts, indexOfthread_ts + thread_ts_length)
  } else {
      // 先頭'p'の1文字を切り抜き、UNIX時刻になるようドット'.'を入れる
      threadTS = splitPermaLink[3].substring(1).substring(0, 10) + '.' + splitPermaLink[3].substring(1).substring(10)
  }
  
  return {channelId, threadTS}
}

// 以下例
const slackPermaLink = `https://<サブドメイン>.slack.com/archives/<Conversation ID>/p<ドットを除いたメッセージの作成日時のタイムスタンプ>`
const {channelId, threadTS} = extractChannelIDThreadTS(slackPermaLink)
console.log(channelId, threadTS)
// C05PB1R852L 1701336881.449019
```

次に `replySlackMessage` 関数を使ってSlack APIでメッセージに返信します。
`headers` の `Authorizatin` に直接トークンを書いていますが、ここは環境変数で置き換えてください。 `xoxb` から始まるトークンはボットトークンです。

```javascript
/**
 * replySlackMessage関数
 * Slackメッセージに返信します。
 * 
 * @param {string} channel 返信するメッセージがある場所のチャンネルID
 * @param {string} thread_ts メッセージのts（UNIXタイムスタンプ）。小数点以下第6位まで必要。
 * @param {string} text 返信するテキスト
 */
async function replySlackMessage(channel, thread_ts, text) {

  const slackReqUrl = `https://slack.com/api/chat.postMessage`
  const resSlack = await fetch(slackReqUrl, {
    method: 'POST',
    body: JSON.stringify({channel, thread_ts, text}),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer xoxb-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    }
  })

  if (!resSlack.ok) {
    alert('Slackスレッドの返信に失敗しました。')
  }
}

// 以下例
const replyMessage = '<@usercode>\n今日のTODO\n<https://www.google.co.jp/|Google トップ>'
replySlackMessage(channelId, threadTS, replyMessage)
```

上記のマークダウンに近いかたちで `replyMessage` を使うと
下記の見た目で返信されます。

--------------------------

@usercode
今日のTODO
[Google トップ](https://www.google.co.jp/)

--------------------------

以上で終わりです。
別言語でも同じロジックを組みさえすれば、同じことができると思います。

# 追記（フロントエンドからの送信時のCORSエラー）

フロントエンドから、Slack APIをたたくとCORSエラーになってしまうため、リクエストの `Content-Type` を `application/x-www-form-urlencoded; charset=utf-8` にするとうまくいきました。
なので `replySlackMessage` 関数は下記に変更しました。

```javascript
/**
 * replySlackMessage関数
 * Slackメッセージに返信します。
 * 
 * @param {string} channel 返信するメッセージがある場所のチャンネルID
 * @param {string} thread_ts メッセージのts（UNIXタイムスタンプ）。小数点以下第6位まで必要。
 * @param {string} text 返信するテキスト
 */
async function replySlackMessage(channel, thread_ts, text) {

  const slackReqUrl = `https://slack.com/api/chat.postMessage`
  const body = `token=${トークンの値そのまま}&channel=${channel}&thread_ts=${thread_ts}&text=${text}`
  const resSlack = await fetch(slackReqUrl, {
    method: 'POST',
    body,
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
    }
  })

  if (!resSlack.ok) {
    alert('Slackプロジェクトスレッドへの返信に失敗しました。')
  }
}
```

こちらの記事を参考にさせていただきました。

> [SlackWebAPIでJSONが利用できなかった話 \#JavaScript \- Qiita](https://qiita.com/masamin/items/59fabdc6fc28ae51cbbb)

# 参考

* [chat\.postMessage method \| Slack](https://api.slack.com/methods/chat.postMessage)
    * Slack返信に使っているSlack APIドキュメント
* [How to retrieve Slack messages via API identified by permalink? \- Stack Overflow](https://stackoverflow.com/questions/61190746/how-to-retrieve-slack-messages-via-api-identified-by-permalink)
* [Slackでアプリケーションを作成し、OAuth Tokenを発行するまで \#Slack \- Qiita](https://qiita.com/kobayashi_ryo/items/a194e620b49edad27364)
* [SlackWebAPIでJSONが利用できなかった話 \#JavaScript \- Qiita](https://qiita.com/masamin/items/59fabdc6fc28ae51cbbb)