【GAS】Twitterへの投稿を「自分として」Slackに転送する

# 動機

私は自分のTwitterと業務受託先のSlackを連携しており、あらゆるつぶやきがtimesに飛んでいくようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/6724c84f-baba-d7ff-946f-49c9f37b2bd3.png)
しかしながら時には飛んでいってほしくない呟きもあるわけです。人間なので。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/e5701b91-1eb9-b94c-5409-402c3083dc97.png)
まあ最悪後から消せたらいいんですが、他人が投稿したメッセージの削除というのはそこそこ強めの権限なので通常メンバーには付与されていないことが多いです。そこで今回はTwitterへの投稿をアプリではなく**自分の投稿として**Slackに転送するbotを自作していくことにします。

# 用意するもの
- SlackAPIを叩くためのトークン
- TwitterAPIを叩くためのトークン
- Googleのアカウント

# SlackAPIを叩くためのトークン
今回叩きたいAPIは[**これ**](https://api.slack.com/methods/chat.postMessage)なので、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/789c7af5-e88b-1ed8-ac4a-0e0945876b03.png)
`chat:write`権限のついたトークンを発行します。

https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21

スコープについてはApp Manifestに
```yaml
oauth_config:
  scopes:
    user:
      - chat:write
```
を追加すればOKです。

# TwitterAPIを叩くためのトークン
TwitterAPIはいつぞやから面倒な申請がなくなり(Essentialに限る)、登録すればすぐにAPIを使えるように[なりました](https://blog.twitter.com/developer/ja_jp/topics/community/2021/build-whats-next-with-the-new-twitter-developer-platform)。

https://di-acc2.com/system/rpa/9688/

今回はOAuth2.0だけを利用するのでUser Authentication Settingsは編集しなくて大丈夫です。

(↓使用するAPIのリファレンス)

https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets

# コーディング
:::note info
2022/8/19 update
- slackへ投稿していないツイートの判定方法を変更し投稿漏れをなくしました
- ツイートにURLが含まれる場合にそのURLのみを再度投稿するようになりました
    - これによりslack上で該当URLのプレビューが表示されます
:::

こんな感じのブックを作ったらいよいよGASを書いていきます。
![5b0da3e4-077c-a9f1-0277-bce8d0cae29f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/cc6617c5-9815-d286-fd7f-116d38c7501d.png)


```javascript
const BOOK = ブックID
const SHEET = シート名

const main = () => {
  const sht = SpreadsheetApp.openById(BOOK).getSheetByName(SHEET)
  const log = sht.getDataRange().getValues().flat()
  const tweets = getTweets().data
  const unsents = until(tweets, tweet => !log.includes(tweet.id))
  if (!unsents.length) return
  unsents.reverse().forEach(tweet => tweetOnSlack(tweet))
  sht.getRange(1, 1, tweets.length).setValues(tweets.map(tweet => [tweet.id]))
}

const queryString = (params) => {
  return "?" + Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&")
}

const get = (url, bearerToken, options) => {
  return UrlFetchApp.fetch(url + queryString(options), {
    method : "get",
    headers: {
      Authorization: `Bearer ${bearerToken}`
    },
  })
}

const post = (url, bearerToken, options) => {
  return UrlFetchApp.fetch(url, {
    method : "post",
    headers: {
      Authorization: `Bearer ${bearerToken}`
    },
    payload : options,
  })
}

const until = (array, cb) => {
  const index = array.findIndex(elm => !cb(elm))
  return index < 0 ? array : array.slice(0, index)
}

const TWITTER_API = "https://api.twitter.com"
const TWITTER_TOKEN = 取得したトークン
const USER_NAME = Twitterのユーザー名(APIで取得してもよい)
const USER_ID = TwitterのユーザーID

const getTweets = () => {
  const endpoint = `${TWITTER_API}/2/users/${USER_ID}/tweets`
  const response = get(endpoint, TWITTER_TOKEN, {
    "tweet.fields": "entities",
  })
  return JSON.parse(response.getContentText())
}

const isReply = (tweet) => {
  return tweet.startsWith("@") ? !tweet.startsWith(`@${USER_NAME}`) : false
}

const SLACK_API = "https://slack.com/api"
const SLACK_TOKEN = 取得したトークン
const CHANNEL = チャンネルのID

const postMessage = (text) => {
  const endpoint = `${SLACK_API}/chat.postMessage`
  const response = post(endpoint, SLACK_TOKEN, {
    channel: CHANNEL,
    text: text,
  })
  return JSON.parse(response.getContentText())
}

const tweetOnSlack = (tweet) => {
  if (isReply(tweet.text)) return
  const urls = tweet.entities?.urls?.map(url => url.unwound_url || url.expanded_url) || []
  const slackMessages = [
    `https://twitter.com/${USER_NAME}/status/${tweet.id}`,
    ...urls.filter(url => !url.match("https://twitter.com")),
  ]
  slackMessages.forEach(msg => postMessage(msg))
}
```

- ブックのIDはURLの `http//docs.google.com/spreadsheets/d/*ここ*/edit#gid=0`
- TwitterのユーザーIDは[**idtwi**](https://idtwi.com/)を使って調べるのが早い
- チャンネルのIDは目的のチャンネルに投稿されたメッセージのリンクから確認する
    - `https://hoge.slack.com/archives/*ここ*/タイムスタンプ`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/a9947e9a-4b92-4c6a-0146-401ff83f668c.png)

- セルA1に記入する日付は[**ISO8601**](https://ja.wikipedia.org/wiki/ISO_8601)形式である必要があります

# 定期実行の設定
エディタの左側のメニューにエディタという項目があるので進みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/341bed44-ad03-a2a1-e11e-f72fdef9d993.png)

Googleに深く頭を下げつつ1分おきのトリガーをセットします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/61aa44b3-a217-22bf-9dd9-2eff3c382755.png)

# 結果

https://twitter.com/nicco_mirai/status/1531093777523576833

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/86dc455c-d440-1e19-7a47-423a9b29ce31.png)
自分の投稿としてつぶやきが飛ぶようになりました！

# おわりに
- 各APIを叩く関数
- 関数を定期実行する機構
- 最終実行時刻を保存するためのDB

これらを一括で用意できるGoogle Sheetはやはり天才だと思いました。

ちなみに遅延をもっと減らしたいのであれば
AWS Lambda + Amazon CloudWatch + Amazon DynamoDB
で同様の仕組みが作れますので気になる方はぜひ。