社内メンバーのアドベントカレンダー投稿をSlackに通知するBotをGASで作ってみた

本記事は[株式会社Works Human Intelligence](https://www.works-hi.co.jp/)のアドベントカレンダー、[「Develop fun!」を体現する Works Human Intelligence #2](https://qiita.com/advent-calendar/2020/whi-2)の2日目になります。
弊社ではアドベントカレンダーを2枚実施しておりますので、[1枚目](https://qiita.com/advent-calendar/2020/whi)もぜひご覧ください！

@takeC74 氏の[昨日の記事](https://qiita.com/takeC74/items/87378f5f41cbc18fd99e)から引き続きSlackネタでお送りいたします！

また、1枚目の2日目には、@yociya-nakada 氏の手になる[ツール開発のすゝめ](https://qiita.com/yociya-nakada/items/a9cdb4a56117240c20ef) が上がっています。
本記事で紹介するような、社内でのちょっとした「困った」を解決したり、業務をより楽しくするためのツール作りの魅力を綴った記事になりますので、こちらもぜひご賞味ください！
## はじめに
弊社では今年の4月からQiita Organizationを運営しております。投稿記事のSlack通知のため、一番最初はSlackのRSS Appを利用していましたが、以下のような理由
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375883/0390affc-f42c-1c5f-c7d5-cac5d550f3df.png)
から、RSS Appの利用をやめ、GASを利用してRSSを定期的に走査しSlackに投げるBotを運用していました。
(Botの作者は弊社でSREを務める @kabik 氏です。彼のアドベントカレンダーは1枚目の12/16に投稿されますので、ぜひ楽しみにしていてください！ )

しかし、現状のBotの仕組みだと、アドベントカレンダーとして投稿した記事ではうまくSlackに連携されないことが発覚したため、今回筆者が改修を行いました。
本記事では、元々のBotの仕組み、なぜその仕組みではアドベントカレンダーがうまく連携されないか、どのように改修したか、を紹介していきます。

## 元々のBotの仕組み
さて、元々のBotの仕組みは以下のようなものです。
![firstArch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375883/d2e8364e-f48d-147d-9c7b-1390973ac69d.png)

- [GASの時間主導型トリガー](https://note.com/tamo_3/n/n6a5453454259)でWHIのQiita OrganizationのRSS を1時間ごとにチェック
- チェックした時刻がX時Y分だとすると、X-1時台に投稿されたものがSlackの特定のチャンネルにポストされる
  - 例1: 13時51分にチェック→12時台に投稿されたものが対象
  - 例2: 0時15分にチェック→前日の23時台に投稿されたものが対象
- Slackにポストする時は、投稿した人へのメンションと投稿した記事をポストする
  - Qiita IDとSlack IDの紐づけはGSS

しかしアドベントカレンダー始動直前である11月27日、社内からの指摘で、この仕組みでは「一度限定共有で投稿後、公開設定に変更した記事」はSlackに連携されないことが発覚しました。

## なぜ連携されないか
それはRSS内のpublished(投稿日時)が、「限定共有から公開に切り替えたタイミング」ではなく、「限定共有/公開問わず、最初に記事を投稿したタイミング」であるためです。
[OrganizationのRSSのXML](https://qiita.com/organizations/works-hi/activities.atom)は以下のような構造です。


```xml
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="ja-JP" xmlns="http://www.w3.org/2005/Atom">
  <id>tag:qiita.com,2005:/organizations/works-hi/activities</id>
  <link rel="alternate" type="text/html" href="https://qiita.com"/>
  <link rel="self" type="application/atom+xml" href="https://qiita.com/organizations/works-hi/activities.atom"/>
  <title>株式会社Works Human Intelligence の記事</title>
  <description>Qiita で 株式会社Works Human Intelligence に所属するユーザの最近の記事</description>
  <updated>2020-11-26T17:05:00+09:00</updated>
  <link>https://qiita.com/organizations/works-hi</link>
  <entry>
    <id>tag:qiita.com,2005:PublicArticle/1343957</id>
    <published>2020-11-26T17:05:00+09:00</published>
    <updated>2020-11-26T17:05:00+09:00</updated>
    <link rel="alternate" type="text/html" href="https://qiita.com/e99h2121/items/873281d73cc504e5a64d"/>
    <url>https://qiita.com/e99h2121/items/873281d73cc504e5a64d</url>
    <title>「失敗を許容する」なんて言われても失敗したくないです</title>
    <content type="html">
&lt;h2&gt;
&lt;span id="この記事は" class="fragment"&gt;&lt;/span&gt;&lt;a href="#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AF"&gt;&lt;i class="fa fa-link"&gt;&lt;/i&gt;&lt;/a&gt;この記事は&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://qiita.com/advent-calendar/2020/whi"&gt;Develop fun!を体現する Works Human Intelligence Advent Calendar 2020&lt;/a&gt;&lt;br&gt;
&lt;a href="https://qiita.com/advent-calendar/2020/whi-2"&gt;Develop fun!を体現する Works Human Intelligence #2 Advent Calendar 2020&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Works Human Intelligence アドベントカレンダーの投稿練習第二弾です。タイトルとは裏腹に、失敗を許容すると言われてもなお失敗したくない、それって「怒らないから正直に言いなさい」っていう先生じゃんと思ってしまう人（私）向けに書きました。&lt;/p&gt;
中略
</content>
    <published>2020-11-26T17:05:00Z</published>
    <updated>2020-11-26T17:05:00Z</updated>
    <author>
      <name>e99h2121</name>
    </author>
  </entry>
  <entry>
    <id>tag:qiita.com,2005:PublicArticle/1343307</id>
    <published>2020-11-25T16:25:11+09:00</published>
    <updated>2020-11-25T16:36:19+09:00</updated>
    <link rel="alternate" type="text/html" href="https://qiita.com/magnoliavine/items/4bd29e2384d45cf73ab4"/>
    <url>https://qiita.com/magnoliavine/items/4bd29e2384d45cf73ab4</url>
    <title>Electronの`shell.openExternal`が上手く動かない際の注意すべき点</title>
    <content type="html">&lt;p&gt;先日Electronを使用したWindowsアプリケーションを作成している際、Electronの&lt;a href="https://www.electronjs.org/docs/api/shell#shellopenexternalurl-options" rel="nofollow noopener" target="_blank"&gt;&lt;code&gt;shell.openExternal&lt;/code&gt;&lt;/a&gt;が上手く動かないトラブルにハマりました。&lt;br&gt;
原因としては非常に初歩的なうっかりミスだったのですが、誰か同じ点で躓いている方のお役に立てばと思い記事にさせていただきました。&lt;/p&gt;

&lt;h2&gt;
&lt;span id="やりたかったこと" class="fragment"&gt;&lt;/span&gt;&lt;a href="#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8"&gt;&lt;i class="fa fa-link"&gt;&lt;/i&gt;&lt;/a&gt;やりたかったこと&lt;/h2&gt;

&lt;p&gt;「外部ブラウザで何か画面を開き、自分のウィンドウは閉じる」みたいな処理をしたい&lt;/p&gt;
```

GASの1時間ごとのCronでは、`entry`の子要素の`published`によって連携対象かどうかを判断しているわけですが、この日時が「最初に記事を投稿したタイミング」である、ということです。
そのためこの仕組みでは、限定共有から公開に切り替えた記事の場合、「Cron => 限定共有 => 公開 => Cron」というケース以外は取りこぼしてしまうことになります。

アドベントカレンダー参加者の皆さんはご存じの方も多いと思いますが、Qiitaのアドベントカレンダーでは、[限定共有記事を登録しておくと当日午前7時に自動で公開設定に変更される](https://help.qiita.com/ja/articles/qiita-adcal-join)機能があります。
弊社のアドベントカレンダーでもこの機能を活用することになっているので、このままだとほとんどの記事がBotで通知されないぞ！これはよろしくない！となったわけであります。


## ではどうするか
### publishedではなくupdatedで走査する？
上記XMLから分かる通り、`published`と同階層に`updated`があります。
これを利用すれば、公開に設定変更したタイミングが取得できそうです。
しかし、公開後にさらに編集を加えた場合にもBotの通知対象になってしまいます。

### 通知した記事を保持しておいて差分を取得する！
そもそも記事の投稿日時を利用してやっていることは、前回までにSlackで通知したものより新しい記事のみを抽出する、ということです。
日時を利用した仕組みでは難しいのであれば、シンプルに通知した記事の一覧自体を保存しておいて、RSSの一覧と比較すればよいだけです。

記事を示す一意の要素といえばURLです。GASを使っているわけですし、先述の仕組みの通り、SlackIDとQiitaIDの紐づけにGSSのシート上のデータを引っ張ってきているので、通知済URLに関してもGSSに保持することにします。

---

しかし、ここでふと思いついてしまいました。 __普通の記事とアドベントカレンダーの記事の通知、混ざらないほうが良くね、と。__
せっかくのアドベントカレンダー記事の投稿通知なんだし、通知メッセージも今までのメッセージとは少し変えたい。そもそも宛先のチャンネルだって専用のものを作ったほうがよさそうだ。
ということで、アドベントカレンダー用のBotの要件は以下のようにします。

- 今までのQiita記事の連携先とは異なるチャンネルに通知する
- 連携する記事は以下の条件を満たす
  1. 過去連携されたことがない記事である
  1. アドベントカレンダーの記事である
  1. 弊社の記事である

## 改善後の仕組み
### タグで絞り込みたい！
上記、連携する記事の条件のうち、1.は先述のURLをGSSに持っておいて比較する方法で対応できます。
しかし、OragnizationのRSSを見る限り、アドベントカレンダーの記事であることを示す要素はなさそうです。
[アドベントカレンダーのRSS](https://qiita.com/advent-calendar/2019/smarthr/feed.atom)もありますが、XMLの構造がOrganizationのものとだいぶ異なり、対応にはGASの改修に少し手間がかかりそうです。

であれば自分でアドベントカレンダーであることを示す要素を入れてしまおう。
Qiita記事におけるカテゴライズといえばタグです。`advent`みたいなタグで絞りこんであげれば……  あれ？OrganizationのRSSの中にはタグも、ない？
……うーむ、困った。どうしよう。
### アドカレ用のタグを作って、タグのRSSを使おう！
……ひらめきました。OrganizaionのRSSがだめであれば、弊社のアドカレでのみ使うタグを作って、そのタグのRSSを読みに行けばいいじゃないか、と。
後はQiitaにタグのRSSがあるかどうかですが、是非は[ググればすぐ判明します。](https://qiita.com/s_tatsuki/items/0e8358c78c7961d0afee)
XMLの構造もOrganizationとほぼ同じなので、既存のGASのコードをある程度流用することができます。

この方法の欠点は、

1. タグは誰でも付けられるため、完全にはOrganization外の記事を弾くことができないこと
1. 5個までのタグの枠を1つ使ってしまうこと

です。
しかし、以下のような方法も考えましたが、タイトルに制約が出るほうが弊害が大きそうだ、ということや、GASの実装が複雑になる(1.のリスクを回避するためにそこまでするか？)ということから、今回はシンプルにタグのRSSだけを見る方式にします。

- 記事タイトルに「アドカレ」などの特定の文字列を入れてもらう
- タグのRSSとOrganizationのRSSの両方を見て、かぶってる記事だけを通知する

## SlackのIncoming Webhook のメッセージにおける小技あれこれ
### メッセージ中にメンションやリンクを入れる
先述の通り、このBotで通知するメッセージには、投稿者へのメンションと、投稿された記事へのハイパーリンクが含まれております。
メンションに関しては、普通に文字列で`@SlackID`と書いてもメンションに変換されることはありません。
またリンクに関しては、普通のURLであればURLの文字列を送ればSlack側でリンクとして表示されますが、ハイパーリンクに関してはそうはいきません。
双方とも工夫が必要です。ググりましょう。ありました。
[SlackのIncoming Webhooksでメンションを飛ばす方法](https://qiita.com/ryo-yamaoka/items/7677ee4486cf395ce9bc)
[Slack の 🍣 にリンクを貼る](https://qiita.com/tanjo/items/ea467c7de3fbd849aa4b)
流石Qiitaですね！

### 通知メッセージに絵文字を入れたい

アドベントカレンダー専用の通知メッセージにする以上、クリスマスっぽい雰囲気を出したいわけで、すなわち絵文字は必須です。
ただご存じの通り、絵文字はいわゆる[サロゲートペア](https://qiita.com/YusukeHirao/items/2f0fb8d5bbb981101be0)であり、扱いがとても面倒です。
まぁでも面倒なだけでできないわけじゃないし、fromCodePointとかを使ってやるしかないかなぁ、と考えていたところ、もしかして[Slackの絵文字の記法](https://slack.com/intl/ja-jp/help/articles/202931348-%E7%B5%B5%E6%96%87%E5%AD%97%E3%81%A8%E3%82%A8%E3%83%A2%E3%83%86%E3%82%A3%E3%82%B3%E3%83%B3%E3%81%AE%E4%BD%BF%E7%94%A8)が使えるんじゃないか、と思いつきました。
少しググってみると…[ありました](https://tonari-it.com/gas-slack-incomming-webhook-weather-message/#toc6)。思った通り、Slackの絵文字記法を含んだメッセージを送れば、Incoming Webhook越しでも絵文字に変換されるようです。


実際に試してみると…

```javascript
function generateMessage(slackId, articleUrl, articleTitle) {
  articleTitle = sanitizeSlackMessage(articleTitle);
  return "<@" + slackId + "> さんのアドカレ記事が投稿されました！:santa: \n<" + articleUrl + "|" + articleTitle + ">";
}
```
![messageSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375883/b2709c85-a718-7a3a-7701-1b16efdbdfb1.png)
ちゃんとメンション、ハイパーリンク、絵文字になっています！

## 最終的にこうなった
![lastArch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375883/80aa1719-fce4-a45d-b92a-1dd5511e970e.png)

- [GASの時間主導型トリガー](https://note.com/tamo_3/n/n6a5453454259)で、毎日午前8時~9時に以下のロジックを実行
  1. タグ `whi-advent` のRSSに存在する全記事のURL、タイトル、投稿者を取得
  1. GSSの「連携済み」シートから、Slack通知済みURLの一覧を取得
  1. 1.のリストをループで回す。URLが2.のリストに含まれていない場合は以下を実行(含まれていた場合は何もしない)
      - GSSの「連携済み」シートにURLを追加
      - Slackに、投稿した人へのメンションと投稿した記事をポスト(Qiita IDとSlack IDの紐づけはGSSの「メンバー管理」シート)

ちなみにGAS実行を午前7時~8時ではなく、午前8時~9時にしているのは、Qiitaの限定共有から公開への変更が厳密に7時きっかりに行われるのかが不明だったためです。
例えば記事の公開への変更が午前7時2分に行われた場合、GAS実行が7時1分に行われると取りこぼしてしまいます。それを嫌い安全側に倒したというわけです。 ~~後7～8時とかまだ寝てる人も多いし、メンション付けるのはよくない~~

---
ソースは以下で公開しています！
https://github.com/cold-wisteria/SlackBot_QiitaRSS

## 最後に
GASは非常に手軽かつ強力なツールです。

実現したいfunctionalityは自由度の高いスクリプトで実装しつつ、定期実行やパラメータストアといった共通要素はGUIで設定できる組み込み設定として準備しています。
スクリプトでは、Google Workspaceのアプリケーション群の操作はもちろんのこと、通信やparseのための豊富な関数を用意し、一方で自身もWebhook URLを公開してWebAPIとしてふるまえる。
Qiitaをはじめネット上に情報はゴロゴロ転がっている。

まさしくAPI連携によるSaaSツール同士のコラボレーションによりワークフローを実現する現代の業務にビタッとハマるツールと言えるでしょう。
Google Workspaceを導入している会社であれば活用しない手はありません。

GASをはじめとした連携の仕組みにより業務をSlackにインテグレーションし、Slackを起点に業務を行う、というのが現段階においては手軽さと業務効率を両立する一つの解ではないでしょうか。