5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NotionとLINE Notifyでカレンダーの予定を通知する

Last updated at Posted at 2022-12-23

0. 目次

  1. はじめに
  2. 必要なもの
  3. 使用技術
  4. 解説
    1. SDKを用意
    2. 使用するライブラリのインストール
    3. 環境変数を読み込ませる
    4. Notionから予定を取得する
    5. LINEメッセージの生成
    6. LINEに送る
  5. まとめ

はじめに

MYJLab Advent Calendar 2022 23日目の記事を担当するのぶです。
昨日の@Kuroi_ccさんの記事は、「nginxでhttpsサイトを公開しよう!」でした。

MYJLab Advent Calendar 2022の23日目はNotionカレンダーに登録されている予定をLINEに通知してみようと思います。
私は普段Notionカレンダーを使って予定を管理しています。
NotionカレンダーはPC版だと見やすくて使い勝手がいいのですが、スマホ版だと一目で予定が確認しづらいです。
そこで今回はNotionカレンダーに登録されている当日の予定をラインで確認できるようにすることでこの課題を解決したいと思います!
全体のソースコード

# こんな感じでラインに送られてくる
[Today-My-Schedule] 
 今日の予定です
 ---------------------------
 10:00 今日の予定1 (url)
,10:00~20:00 今日の予定2 (url)
,2022-12-23~2022-12-24 今日の予定3 (url)
,終日 今日の予定4 (url)


必要なもの

これらを事前に取得しておく必要があります!
やり方はリンク先の資料を読むとわかりやすいです。

使用技術

名前 概要
TypeScript JavaScriptを拡張して作られたプログラミング言語
@notionhq/client Notion API用のライブラリ
axios HTTPクライアント
dotenv Node.jsで環境変数を扱えるようにするライブラリ
lodash 強力な関数型プログラミングの関数を提供してくれるライブラリ
luxon JavaScriptで日時操作を楽にしてくれるライブラリ
qs クエリ操作を楽にするライブラリ
winston 強力なロギングライブラリ

解説

全体の処理の流れ

処理は大きく分けて三つです!

公式のTypeScript用SDKを手元にダウンロードする

git clone https://github.com/makenotion/notion-sdk-typescript-starter

順番にライブラリのインストール

npm install
npm install axios qs winston
npm install --save dotenv lodash luxon

環境変数を読み込む

事前に用意したトークンやIDを.envに書きます。

.env
NOTION_TOKEN=
LINE_TOKEN=
DATABASE_ID=

変数に代入するのと一緒に環境変数があるかどうかもチェックします、なかったらエラーを出して処理を終了させます。

main.ts
// 存在もチェック
  const [notionToken, databaseId, lineToken] = isEnv([
    process.env.NOTION_TOKEN,
    process.env.DATABASE_ID,
    process.env.LINE_TOKEN,
  ]);

function isEnv(ENV: (string | undefined)[]): string[] {
  const env = ENV.map((v) => {
    if (!v) {
      throw new Error("環境変数が設定されていません");
    }
    return v;
  });
  return env;
}

Notionから予定を取得する

luxonライブラリはデフォルトだとUTCになっているので、JTCにします。
Notionカレンダー(データベース)からは今日の予定の時間、タイトル、リンクを取得して、それらを組みにした配列にします。

main.ts
  const notion = new Client({
    auth: notionToken,
  });

  const today = DateTime.now().setZone("Asia/Tokyo").toFormat("yyyy-LL-dd");

//クエリを生成
  const notionDatabaseQuery: QueryDatabaseParameters = {
    database_id: databaseId,
    filter: {
      and: [
        {
          property: "日付",
          date: {
            equals: today,
          },
        },
      ],
    },
  };




const getMyScheduleTitle = (
  todayMySchedule: QueryDatabaseResponse
): string[] => {
  const myScheduleTitle: (string | undefined)[] = todayMySchedule.results.map(
    (result) => {
      if (!("properties" in result)) return;
      if (!("title" in result.properties.Name)) return;

      return result.properties.Name.title[0].plain_text;
    }
  );

  return myScheduleTitle.filter((title): title is string => !!title);
};

const getMyScheduleTime = (
  todayMySchedule: QueryDatabaseResponse
): string[] => {
  const myScheduleDate: (string | undefined)[] = todayMySchedule.results.map(
    (result) => {
      if (!("properties" in result)) return;
      if (!("date" in result.properties["日付"])) return;
      return ReturnScheduleTime(result.properties["日付"].date);
    }
  );

  return myScheduleDate.filter((date): date is string => !!date);
};


function ReturnScheduleTime(date: any): string {
  if (check_date(date.start)) {
    if (date.end == null) return "終日"; // YYYY-MM-DD
    return `${date.start}~${date.end}`; //  YYYY-MM-DD ~ YYYY-MM-DD
  }

  if (date.end === null) {
    const start = date.start.split("T")[1].substr(0, 5);
    return `${start}`;
  } //YYYY-MM-DDT
  const start = date.start.split("T")[1].substr(0, 5);
  const end = date.end.split("T")[1].substr(0, 5);

  return `${start}~${end}`; //YYYY-MM-DDT ~ YYYY-MM-DDT
}

const check_date = (s: string) => {
  if (typeof s == "string") {
    const a = s.match(/^(\d+)\-(\d+)\-(\d+)$/);
    if (a) {
      const y = parseInt(a[1]);
      const m = parseInt(a[2]) - 1;
      const d = parseInt(a[3]);
      const x = new Date(y, m, d);
      return y == x.getFullYear() && m == x.getMonth() && d == x.getDate();
    }
  }
  return false;
};

const getMyScheduleLink = (
  todayMySchedule: QueryDatabaseResponse
): string[] => {
  const myScheduleLink: (string | undefined)[] = todayMySchedule.results.map(
    (result) => {
      if (!("url" in result)) return;
      return result.url;
    }
  );

  return myScheduleLink.filter((link): link is string => !!link);
};

  type ScheduleTime = string;
  type ScheduleTitle = string;
  type ScheduleLink = string;

//時間とタイトルとリンクを組みにする
  const schedule = (await notion.databases
    .query(notionDatabaseQuery)
    .then((s) => {
      return zip<string>(
        getMyScheduleTime(s),
        getMyScheduleTitle(s),
        getMyScheduleLink(s)
      );
    })) as [ScheduleTime, ScheduleTitle, ScheduleLink][];

  schedule.sort((a, b) => {
    if (a[0] > b[0]) {
      return 1;
    } else {
      return -1;
    }
  });

LINEメッセージの生成

今日の予定の時間とタイトルとリンクを組みにした配列(タプル)をmapで回してLINEに送る用のメッセージを作ります。

  let message: string;
  if (schedule.length === 0) {
    message = `
    今日の予定です
    ---------------------------
    なし
  `;
  } else {
    message = `
    今日の予定です
    ---------------------------
    ${schedule.map((s): string => {
      return `${s[0]} ${s[1]} (${s[2]})\n`;
    })}
  `;
  }

LINEに送る

あとはLINE APIに乗っけて送るだけです!

main.ts
const linePost = async (message: string, lineToken: string) => {
  try {
    const res = await axios({
      method: "post",
      url: "https://notify-api.line.me/api/notify",
      headers: {
        Authorization: `Bearer ${lineToken}`,
        "Content-Type": "application/x-www-form-urlencoded",
      },
      data: qs.stringify({
        message: message,
      }),
    });
  } catch (e: any) {
    const { status, statusText } = e.response;

    logger.error(`Error! HTTP Status: ${status} ${statusText}`);
  }
};

  await linePost(message, lineToken);

全体のソースコード

全部組み合わせるとこんな感じになります!

import dotenv from "dotenv";
import {
  getAllUsers,
  getMyScheduleLink,
  getMyScheduleTime,
  getMyScheduleTitle,
  getMyUserId,
} from "./notion";
import { QueryDatabaseParameters } from "@notionhq/client/build/src/api-endpoints";
import { Client } from "@notionhq/client";
import { DateTime } from "luxon";
import { linePost } from "./line";
import { zip } from "lodash";
import logger from "./logger";

dotenv.config();

const isEnv = (ENV: (string | undefined)[]): string[] => {
  const env = ENV.map((v) => {
    if (!v) {
      throw new Error("環境変数が設定されていません");
    }

    return v;
  });

  return env;
}

const getAllUsers = async (notionToken: string) => {
  try {
    const res = await axios({
      method: "get",
      url: "https://api.notion.com/v1/users",
      headers: {
        Authorization: `Bearer ${notionToken}`,
        "Notion-Version": "2022-06-28",
      },
    });

    return res.data;
  } catch (e: any) {
    const { status, statusText } = e.response;
    logger.error(`Error! HTTP Status: ${status} ${statusText}`);
  }
};

const getMyUserId = (myName: string, users: any): string => {
  return users.results.filter((user: any) => user.name === myName)[0].id;
};

const getMyScheduleTitle = (
  todayMySchedule: QueryDatabaseResponse
): string[] => {
  const myScheduleTitle: (string | undefined)[] = todayMySchedule.results.map(
    (result) => {
      if (!("properties" in result)) return;
      if (!("title" in result.properties.Name)) return;

      console.log(result.properties.Name.title[0].plain_text);

      return result.properties.Name.title[0].plain_text;
    }
  );

  return myScheduleTitle.filter((title): title is string => !!title);
};

const getMyScheduleTime = (
  todayMySchedule: QueryDatabaseResponse
): string[] => {
  const myScheduleDate: (string | undefined)[] = todayMySchedule.results.map(
    (result) => {
      if (!("properties" in result)) return;
      if (!("date" in result.properties["日付"])) return;
      return ReturnScheduleTime(result.properties["日付"].date);
    }
  );

  return myScheduleDate.filter((date): date is string => !!date);
};


function ReturnScheduleTime(date: any): string {
  if (check_date(date.start)) {
    if (date.end == null) return "終日"; // YYYY-MM-DD
    return `${date.start}~${date.end}`; //  YYYY-MM-DD ~ YYYY-MM-DD
  }

  if (date.end === null) {
    const start = date.start.split("T")[1].substr(0, 5);
    return `${start}`;
  } //YYYY-MM-DDT
  const start = date.start.split("T")[1].substr(0, 5);
  const end = date.end.split("T")[1].substr(0, 5);

  return `${start}~${end}`; //YYYY-MM-DDT ~ YYYY-MM-DDT
}

const check_date = (s: string) => {
  if (typeof s == "string") {
    const a = s.match(/^(\d+)\-(\d+)\-(\d+)$/);
    if (a) {
      const y = parseInt(a[1]);
      const m = parseInt(a[2]) - 1;
      const d = parseInt(a[3]);
      const x = new Date(y, m, d);
      return y == x.getFullYear() && m == x.getMonth() && d == x.getDate();
    }
  }
  return false;
};

const getMyScheduleLink = (
  todayMySchedule: QueryDatabaseResponse
): string[] => {
  const myScheduleLink: (string | undefined)[] = todayMySchedule.results.map(
    (result) => {
      if (!("url" in result)) return;
      return result.url;
    }
  );

  return myScheduleLink.filter((link): link is string => !!link);
};

const linePost = async (message: string, lineToken: string) => {
  try {
    const res = await axios({
      method: "post",
      url: "https://notify-api.line.me/api/notify",
      headers: {
        Authorization: `Bearer ${lineToken}`,
        "Content-Type": "application/x-www-form-urlencoded",
      },
      data: qs.stringify({
        message: message,
      }),
    });
  } catch (e: any) {
    const { status, statusText } = e.response;

    logger.error(`Error! HTTP Status: ${status} ${statusText}`);
  }
};



const main = async () => {
  const [notionToken, databaseId, myName, lineToken] = isEnv([
    process.env.NOTION_TOKEN,
    process.env.DATABASE_ID,
    process.env.NOTION_NAME,
    process.env.LINE_TOKEN,
  ]);

  const notion = new Client({
    auth: notionToken,
  });

  const today = DateTime.now().setZone("Asia/Tokyo").toFormat("yyyy-LL-dd");


  const notionDatabaseQuery: QueryDatabaseParameters = {
    database_id: databaseId,
    filter: {
      and: [
        {
          property: "日付",
          date: {
            equals: today,
          },
        },
      ],
    },
  };

  type ScheduleTime = string;
  type ScheduleTitle = string;
  type ScheduleLink = string;

  const schedule = (await notion.databases
    .query(notionDatabaseQuery)
    .then((s) => {
      return zip<string>(
        getMyScheduleTime(s),
        getMyScheduleTitle(s),
        getMyScheduleLink(s)
      );
    })) as [ScheduleTime, ScheduleTitle, ScheduleLink][];

  schedule.sort((a, b) => {
    if (a[0] > b[0]) {
      return 1;
    } else {
      return -1;
    }
  });

  let message: string;
  if (schedule.length === 0) {
    message = `
    今日の予定です
    ---------------------------
    なし
  `;
  } else {
    message = `
    今日の予定です
    ---------------------------
    ${schedule.map((s): string => {
      return `${s[0]} ${s[1]} (${s[2]})\n`;
    })}
  `;
  }

  //line
  await linePost(message, lineToken);
}

main()
  .then(() => {
    logger.info("success");
  })
  .catch((err) => {
    logger.error(err);
  });


まとめ

Notion APIは公式ドキュメントのサンプルが充実しているので、もっといろんなことができそうです。
今回は自分の生活に使えそうなものができたので満足です。
読んでいただきありがとうございましたm(_ _)m

参考記事

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?