【Notionの活用例】Google Apps Script でメルマガを Notion に送る(スクリプト例)

feature-image

メルマガを全て Notion に送りつけて快適にメルマガ購読環境を作った例を書きました。

【Notion活用例】Google apps script  でメルマガ集約を自動化して快適閲覧
【Notion活用例】Google apps script でメルマガ集約を自動化して快適閲覧
https://fand.jp/read-the-mail-magazine-with-notion/
今までの生活で不便だったところを Notion 中心のライフスタイルに変更していきます。この記事では「メルマガはメールで読まずに Notion で読めるようにする」にトライします。具体的には「Google Apps Script で Notion へ情報(テキストデータ)を送るに …

この記事では、Google Apps Script のコード例を記載します。あくまでコード作りの考え方、あるいはコードの雛形のようなものを共有する目的です。

具体的な書き方は、購読しているメルマガの文書構造によって差があるところですので、好みに応じてカスタマイズをしてください。

メインのスクリプト

最初にコードを記載し、その後にポイントとしていくつかメモを記載します。

const DATABASE_ID =
  PropertiesService.getScriptProperties().getProperty("DATABASE_ID");

function myFunction() {
  // 処理の対象にしたいメルマガに設定している Gmail のラベル
  const query = "label:magazine -label:GAS処理済み";

  // スレッドに処理済みラベルを付ける
  const label = GmailApp.getUserLabelByName("GAS処理済み");

  // 1回あたりの実行で処理する最大のメール数を設定
  const getLimit = 10;

  // Gmail を検索
  const threads = GmailApp.search(query, 0, getLimit);

  threads.forEach(function (thread) {
    thread.getMessages().forEach(function (message) {
      const subject = message.getSubject();
      Logger.log("processing: " + subject);

      // メッセージの日付。あまり重要ではないので、あってもなくてもどちらでも
      const date = message.getDate();
      // メッセージの本文を取得するメソッド
      const plainBody = message.getPlainBody();
      // Notion API の仕様にあわせて送信するオブジェクト構造を作る関数
      const notionPayload = getNotionRequestPayload(
        DATABASE_ID,
        subject,
        plainBody,
        date
      );

      const response = PostNotion.post(notionPayload);
      const responseCode = response.getResponseCode();
      const responseBody = response.getContentText();

      // レスポンスコードを見て正常終了していれば完了を示すラベルを設定
      if (responseCode === 200) {
        Logger.log("Seuccessfully to post notion");
        thread.addLabel(label);
        // あわせて Slack へも通知(成功の通知)
        PostSlack.sendMessage(
          ":wave: Succeeded to get magazine: subject: " + subject
        );
      } else {
        Logger.log(
          Utilities.formatString(
            "Request failed. Expected 200, got %d: %s",
            responseCode,
            responseBody
          )
        );
        // あわせて Slack へも通知(失敗の通知)
        PostSlack.sendMessage(":cry: Failed to get magazine");
      }

      Utilities.sleep(3000);
    });
  });
}

function getNotionRequestPayload(databaseId, subject, plainBody, date) {
  // parseMagazineBodyStructure が肝。メールマガジンによって特徴があると思うのであわせる
  const mainContent = parseMagazineBodyStructure(plainBody);
  const magNumber = subject.match(/(\d+)号/)[1];

  // Notion API の仕様にあわせて本文を作る。「block」の理解が重要
  const children = [
    {
      object: "block",
      type: "heading_2",
      heading_2: {
        text: [{ type: "text", text: { content: "○○マガジンの見出し(任意のテキスト)" } }],
      },
    },
    mainContent,
  ];

  // 上で作った本文と、メタ的な上位情報(プロパティなど)を合体させて完成
  // 「号数」「日付」という列を対象のデータベースに作っておいてく
  return {
    parent: {
      database_id: databaseId,
    },
    children: children,
    properties: {
      Subject: {
        title: [
          {
            text: {
              content: subject,
            },
          },
        ],
      },
      号数: {
        number: Number(magNumber),
      },
      日付: {
        rich_text: [
          {
            text: {
              content: date,
            },
          },
        ],
      },
    },
  };
}

const parseMagazineBodyStructure = (plainBody) => {
  // メール本文のテキストが渡ってくるので正規表現を使ってほしい情報を抽出する
  // 面倒だったら正規表現を使わずにメッセージ全文をそのまま格納することも可
  const section1 = plainBody.match(/^(1.今週のニュース)/m)[1];
  const section2 = plainBody.match(/^(2.読者からのお便り)/m)[1];
  return convertBodyObjectForNotionRequest([section1, section2]);
};


// @param string[] 適度に分割された本文のテキストの配列
const convertBodyObjectForNotionRequest = (strings) => {
  // わたってきた配列から各要素の文字列に対して、 Notion API 解釈できるテキスト形式に変換する
  const blocks = strings.map((string) => {
    return {
      object: "block",
      type: "paragraph",
      paragraph: {
        text: [
          {
            type: "text",
            text: {
              content: string,
            },
          },
        ],
      },
    };
  });

  return blocks;
};

考え方や注意点

  • メール本文の取得先は Gmail を前提としています。Google Apps Script を使うわけですので、多くの方がその前提であろうと思います。
  • Gmail のラベル付与状態により、処理済み・未処理を判別しています。「既読」「未読」で区別する手もありますが、つい手動で開いてしまった場合は意図せず既読になってしまうからです。この例では、”GAS処理済み” というラベルを作っておきます。
  • Database ID は環境変数で設定していますが、自分専用であれば直接コード内に書いてもOKです。
  • この例では Notion に対して「プレーンテキスト」で送っています。URLを送っても自動でリンクされたりはしません(将来、Notion API 側で自動リンクしてくれるといいな・・・)。リンクさせたい場合は bookmark というブロックを使う必要があります。
  • 現時点、単一ブロックに遅れるテキストの文字長は 2,000 文字が最大です。これを超えると Notion API に拒否されますので、2,000 文字を超える場合は事前に分割が必要です(面倒くさい)
  • 加えて、ブロック数そのものも 1,000 個が最大です。ただ、個人用途で結構な長文のマガジンを購読していますが 300 ブロック以下で十分運用できているので当面は問題にならない印象です。

詳しいリミットについては公式のAPIドキュメントにもまとまっています。まだベータ版ですので、今後緩和されることも期待してます。

https://developers.notion.com/reference/request-limits
https://developers.notion.com/reference/request-limits
https://developers.notion.com/reference/request-limits
Notice: You are in development mode or could not get the remote server

Notion API にテキストを送る時には Block という構造の理解が欠かせません。参考記事もご覧ください。

Notion API からページの本文を取得するためにBlockを理解する
Notion API からページの本文を取得するためにBlockを理解する
https://fand.jp/notion/describe-notion-api-block/
Notion のページ本文をAPIで取得するために重要な Block について説明します。

共通ライブラリ(Notion への送信)

Notion への送信(POST)は他のスクリプトでも使い回せた方が都合がいいので、ライブラリとして登録します。こうすることで更に都合がよいのが、 Notion API を叩くためのトークンの設定も1箇所で済むことになります。

複数のプロジェクトに重要なトークンが散らばっているのは事故の可能性もありますし、何より面倒ですよね。プロジェクトが2つ以上になった時点で共通化した方がいいと思います。

この画像のように Notion へ送信する部分を PostNotion というライブラリで登録しています。

PostNotion そのものは別のプロジェクトで、以下のコードです。こうすることで、メインのプログラムから PostNotion.post(xxx) として Notion へのデータ送信が可能になります。

function post(object) {
  const NOTION_TOKEN =
    PropertiesService.getScriptProperties().getProperty("NOTION_TOKEN");

  const notion_url = "https://api.notion.com/v1/pages";

  const options = {
    method: "post",
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
      Authorization: "Bearer " + NOTION_TOKEN,
      "Notion-Version": "2021-05-13",
    },
    payload: JSON.stringify(object),
    muteHttpExceptions: true,
  };

  return UrlFetchApp.fetch(notion_url, options);
}

バージョンに注意しよう

Notion API では Notion-Version のヘッダにAPIバージョンを設定します。上記では 2021-05-13 としていますが、2022/01/31現在の Notion API 仕様では 2021-08-16 が最新です。互換性は今後も続くと思いますが、現在はベータ提供ですので今後どのようになるかは確証がありません。

詳しくは次の公式ドキュメントを参照してください。

Notion API Versioning —https://developers.notion.com/reference/versioning

Google Apps Script 用の CLIツール - clasp

最初のお試しにあたってはブラウザ上のエディタでも十分ですが、コード量が増えてきたり、ソースコードのバージョン管理をしようとするとローカルのエディタで開発したくなってきます。そんな場合、Google が公式で提供している CLI ツール clasp を使いましょう。

google/clasp: 🔗 Command Line Apps Script Projects — https://github.com/google/clasp

GASでモジュール分割したい場合

コード量が増えてくると import xxx form "xxxx" (あるいは require )によりファイル分離したくなってきますが、Apps Script 上ではモジュールを読み込んでくれませんでした。他のやり方があるかもしれませんが、十分な調査はできていません。

解決策のひとつとして、ローカルのエディタでお好みの環境で開発し、最後に Webpacker でバンドルして配置してしまえばいいようです。この場合、Typescript を始め開発の自由度が増しますね。

まとめ

Notion でメルマガを快適にする事例、およびコードの例を紹介しました。

現代においてもメルマガは有益な情報コンテンツですが、購読するツールはさほど進化していない印象です。日頃使い慣れたツールに集約しておくと、なかなか便利ですよ。