SlackのRSS機能をGASでもう少しだけ便利にしてみた

# はじめに

SlackはRSSリーダーとしても使うことができ、公式アプリもあります。

https://slack.com/intl/ja-jp/help/articles/218688467-Slack-%E3%81%AB-RSS-%E3%83%95%E3%82%A3%E3%83%BC%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B

ただいくつか使いづらい点があったので、[こちら](https://www.ulvaniac.co.jp/2023/09/20/gas%e3%82%92%e5%88%a9%e7%94%a8%e3%81%97%e3%80%81rss-feed%e3%82%92google-chat%e3%81%ab%e9%80%9a%e7%9f%a5%e3%81%99%e3%82%8b%e5%ae%9f%e8%a3%85%e4%be%8b/#8)を参考にGemini協力のもとRSSフィードの管理・Slackの特定チャンネルへの投稿をできるようにしました。

# Slack RSSアプリの課題

**①まとめてRSSフィードを追加・削除・変更できない**

RSSフィードは、気にいったサイトがあったらその都度追加していくものなので、一括追加をするタイミングはあまりないのですが、削除や通知先の変更については整理のときにまとめてやることがあります。

ただ、デフォのRSSアプリでは一つずつする必要があり、たくさん登録している場合はとても大変です。

**②登録中のRSSフィードの詳細情報を追記できない**

RSSフィードとして登録しているサイトは、登録当初は何のためのサイトなのか覚えてるのですが、日にちが経ったり登録者が退職すると、なぜ登録されているのかがわからなくなります。

しかし、デフォのRSSアプリではメモ欄はないため、コメントを残せません。

**③投稿時の細かい書式調整ができない**

本文は3行目までメッセージに記載、タイトルは太文字・斜字の表記で…など、みやすい書体は人によってそれぞれです。こうしたカスタマイズもデフォのRSSアプリではできません。

# Google Sheetに記載されたRSS URLを参照しSlackに投稿する

上記課題を解決するために、

- RSS URLはGoogle Sheetで管理
- Google Sheetに記載されているURLを参照してSlackに投稿する

という仕組みにしました。

## 使い方
**①Google Sheetにて以下の通りで設定**
・シート名は「list」
・画像の通りで各カラムを設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687068/a0bfce8f-3386-4228-8465-9603eaced948.png)


**②GASを作成**
　2-1. 以下コードをGoogle SheetのApps Scriptにコピペ
<details><summary>サンプルコード</summary>

```jsx
 // Slack Webhook URL をここに定義します
const SLACK_WEBHOOK_URL = "Webhook URL"; // 実際の Webhook URL に置き換えてください

/**
 * Ver2.0のRSSからfromTime以降のpubDateのitemを取得する。
 * @param {string} rssUrl - 取得するRSSのURL
 * @param {Date} fromTime - fromTime以降のpubDateのitemを取得
 * @returns {Object} Jsonデータのオブジェクト
 */
function getRssV2(rssUrl, fromTime) {
  let options = {
    "method": "get",
    "contentType": 'application/xml; charset="UTF-8"',
  };
  const requestDate = new Date();
  let response = UrlFetchApp.fetch(rssUrl, options);

  let lastDate = new Date(response.getHeaders()["Date"]);

  // 万が一、レスポンスヘッダにDateフィールドが含まれない場合に、リクエスト時の時間を入れる
  if (Number.isNaN(lastDate.getTime())) {
    console.info(`レスポンスヘッダにDateフィールドを含みません。[url: ${rssUrl}]`)
    lastDate = requestDate;
  }
  let xmlText = response.getContentText();

  // XMLを解析
  let document = XmlService.parse(xmlText);
  let root = document.getRootElement();
  let rssVersion = root.getAttribute("version").getValue();

  if (rssVersion != "2.0") {
    throw new Error(`対応していない形式です。[rss version: ${rssVersion}]`);
  }

  // RSSフィード内の記事を取得
  let items = root.getChildren("channel")[0]
    .getChildren("item");

  let filteredItems = items.filter((item) => {
    let pubDate = new Date(item.getChild("pubDate").getText());
    return pubDate.getTime() >= fromTime.getTime();
  })
  filteredItems.sort((item1, item2) => {
    let item1PubDate = new Date(item1.getChild("pubDate").getText());
    let item2PubDate = new Date(item2.getChild("pubDate").getText());
    return item1PubDate.getTime() - item2PubDate.getTime();
  })

  let messages = filteredItems.map((item) => {
    let pubDate = new Date(item.getChild("pubDate").getText());
    let title = item.getChild("title").getText();
    let link = item.getChild("link").getText();
    let description = item.getChild("description").getText(); // 本文を取得
    return {
      "pubDate": pubDate,
      "title": title,
      "link": link,
      "description": description  // 本文をメッセージに含める
    };
  });

  /**
   * @type {Object}
   * @property {Date} lastDate - 取得時の時間。通常、RSS Feedに対するレスポンスヘッダのDateフィールドの時間。Dateフィールドがない場合、リクエスト時の時間。
   * @property {string[]} messages - RSS Feedのitem要素の一部をテキストにしたものの配列。pubDateで昇順ソートしている。
   */
  return { "lastDate": lastDate, "messages": messages };
}

function main() {
  // 列名に対するSpreadsheetの列番号を表す
  const columnRowNum = 1;
  const columnRssUrl = 3;
  const columnLastDate = 4;

  // RSSを取得したときに、1回でもエラーが発生したら、true
  let didErrorHappen = false;

  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("list");

  for (let i = 2; ; ++i) { // 無限ループとし、break で抜ける
    let rssUrl = sheet.getRange(i, columnRssUrl).getValue();
    if (rssUrl === "") { // RSS URL が空ならループを終了
      break;
    }
    let rowNum = sheet.getRange(i, columnRowNum).getValue();

    let lastDateRange = sheet.getRange(i, columnLastDate);
    let lastDateStr = lastDateRange.getValue();
    let lastDate = new Date(lastDateStr);

    // 最後に更新した日付(lastDate)が存在しない場合に、lastDateを現在時刻から15分前にする
    if (Number.isNaN(lastDate.getTime())) {
      lastDate = new Date();
      lastDate.setMinutes(lastDate.getMinutes() - 15);
      console.log(`lastDateが存在しません。${lastDate}以降のRSS Feedを調べます。`);
    }

    let rssInfo;

    try {
      rssInfo = getRssV2(rssUrl, lastDate);
    } catch (e) {
      console.error(e);
      didErrorHappen = true;
      continue;
    }

    rssInfo["messages"].forEach((message) => postSlack(message, SLACK_WEBHOOK_URL));
    lastDateStr = rssInfo["lastDate"].toString();
    console.info(`[#${rowNum}] rssUrl: ${rssUrl}, lastDate: ${lastDateStr}`)
    lastDateRange.setValue(lastDateStr);
  }

  if (didErrorHappen) {
    throw Error("Error happens. Please see logs.");
  }
}

/**
 * Web Hookを利用して、Slackにmessageを送信する。
 * @param {Object} message - Slackに投稿するmessageオブジェクト (title, link, description)
 * @param {string} webHookUrl - Slackに送信するためのWeb HookのURL
 */
function postSlack(message, webHookUrl) {
  let payload = {
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": `*${message.title}*\n${message.link}`
        }
      }
    ]
  };
  let options = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify(payload)
  };
  let response = UrlFetchApp.fetch(webHookUrl, options);
  if (response.getResponseCode() != 200) {
    throw Error(`failed to send message to slack.[response code: ${response.getResponseCode()}][message: ${message}][webHookUrl: ${webHookUrl}]`);
  }
}
```

</details>
　2-2.RSSを投稿したいチャンネルのWebhook URLを取得<br>
　　　詳しい手順は省略しますが、https://api.slack.com/appsから取得してください。<br>
　　　bot名もご自由に。<br>
　2-3.取得したWebhook URLをGAS2行目の"Webhook URL”欄に追記<br>
　　　※ダブルコーテーションは残したままWebhook URLの部分のみを書き換える。
　　　　”Webhook URL” →　”https://hooks.slack.com/services/hogehoge” 

**③関数「main」に対して時間主導型のトリガーを設定**
　サイトの更新も多くて1日数回程度かと思いますので、半日に1回の実行などお好みで設定してください。ただし、毎分実行などあまり実行間隔が短すぎるとエラーが起きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687068/5a411df9-d11e-46d8-86f9-bac7c5ecdf87.png)

    
## 実際に使った様子
**Google Sheet**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687068/fb5af22a-6de9-4b22-9c60-d0f10e527e47.png)

**投稿されたメッセージ**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687068/d55e96a9-537d-468b-99d3-d5ccbdd1912e.png)
    

# 設定の調整方法

**投稿時の書式の変更**
私自身もGASに詳しくないので、Geminiにやってもらいましょう！笑

**投稿先チャンネルの変更**
取得したWebhookURLの投稿先チャンネルを変更することで変更可能です。



# おわりに
普段自分が見ているRSSを他人に共有する機会があり、今回のGASを作成することになりました。
簡単なGASの作り替えはGeminiにお任せすればなんとかなりますね！便利な時代だぁ