Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
17
Help us understand the problem. What is going on with this article?
@sakas1231

google calendar上の予定をslack通知させて予定の守れるエンジニアになりたい!

背景

スクリーンショット 2021-05-04 21.32.48.png

これは、チーム内のKPTでProblemの洗い出しをしているときに「自分最近作業に集中しちゃってmtg遅刻しちゃってるな」と感じたので、戒め & 自分の意識付けのために書いたチケットです。(チームのProblemじゃないじゃん、というところは目をつぶっていただいて:pray:)
ただその意識も永久的に続くわけではないだろうなと思ったのでどうせならシステムで解決したい!

ということで、 Google Calendarに上がっているmtg予定の1分前になったらslackにメンションを飛ばしてくれる仕組みを作りました。

実は... [5/5 14:20 追記]

知ってる方もいるかも知れませんが、slackにはgoogle calendarのインテグレーションが備わっており、
image.png
こちらを導入するだけで簡単に 1分前にslack通知してくれる 機能は実現できるのですが、
スクリーンショット 2021-05-05 14.05.47.png
今回自作するに至った理由は、このスクショのように

  • メンションの設定ができない
  • フォーマットのカスタマイズができない
    • このタイトルについてるリンクをカスタマイズできない(google calendarの予定に飛んでしまう)
    • mtgの参加者やアジェンダなどの表示ができない

といったことができず、mtgのリマインダとして利用するにはちょっと情報量不足・要件が満たせないと感じたためです。
もしこの情報量で十分だという方は黙ってこれを導入しちゃうのが一番だと思います。

special thx)
https://twitter.com/sakichi01_/status/1389734107848921088?s=20

とりあえず成果物

ということで成果物はこちら。
インテグレーションで実現できなかったメンションやアジェンダ、参加者情報の記載が出来るようになりました。
スクリーンショット 2021-05-04 22.16.59.png

以下のソースコードをgasに入れて、トリガーを1分毎に回しています。

const calendarId = 'calendarId@group.calendar.google.com'; 
const channel = '#channel_name';
const mention = '@sakas1231';
const username = '通知くん';
const icon_emoji = ':ghost:';
const incomingHookUrl = 'https://hooks.slack.com/services/webhook_url';
const tagName = 'notifired';

// slackリマインドさせる時刻
// e.g.) 1なら1分前に通知
const PRE_NOTI_MINUTES = 1;

const isNotifiableEvent = event => !event.isAllDayEvent && 
    !event.isNotifired;

const buildEventObject = event => {
  // よしなにeventをparseしてプロパティを埋める
  const url = 'xxx';
  const members = 'xxx';
  return {
    isAllDayEvent: event.isAllDayEvent(),
    startTime: moment(event.getStartTime().getTime()),
    endTime: moment(event.getEndTime().getTime()),
    title: event.getTitle().replace('\n', ''),
    description: event.getDescription(),
    url,
    members,
    isNotifired: !!event.getTag(tagName),
  }
};

const buildSlackBlock = eventObj => {
  const restTotalSeconds = eventObj.startTime.unix() - moment().unix();
  const rest = {
    seconds: restTotalSeconds % 60,
    minutes: parseInt(restTotalSeconds / 60, 10),
  }
  return {
    channel,
    mention,
    icon_emoji,
    username,
    link_names: 1,
      blocks: [
          {
            type: 'section',
            text: {
                type: 'mrkdwn',
                text: `${mention} あと *${rest.minutes}${rest.seconds}秒* でmtgだよ`
            }
        },
      {
            type: 'section',
            text: {
                  type: 'mrkdwn',
                text: `*<${eventObj.url}|${eventObj.title}>*`
            }
        },
      {
        type: 'section',
        text: {
              type: 'mrkdwn',
                  text: `*説明:*\n${eventObj.description}`
              },
      },
        {
            type: 'section',
              fields: [
                  {
                    type: 'mrkdwn',
                    text: `*参加者:*\n${eventObj.members}`
                },
                {
                      type: "mrkdwn",
                    text: `*時間:*\n${eventObj.startTime.format("HH:mm")} - ${eventObj.endTime.format("HH:mm")}`
                },
            ]
        }
    ]
  }
};

const notification = event => {
  const jsonData = buildSlackBlock(event);
  const payload = JSON.stringify(jsonData); 
  const options = {
    method : 'post',
    contentType : 'application/json',
    payload,
    muteHttpExceptions: true
  };  
  const res = UrlFetchApp.fetch(incomingHookUrl, options);
  return res.getResponseCode();
};

function main(){
  // カレンダーから予定を取得する
  const cal = CalendarApp.getCalendarById(calendarId);
  const events = cal.getEventsForDay(new Date());
  const now = moment().unix();

  events.forEach(event => {
    const eventData = buildEventObject(event);
    // 通知する必要ないイベントの場合はcontinue
    if (!isNotifiableEvent(eventData)) {
      return;
    }
    // 通知時刻
    const eventNotiTime = eventData.startTime.clone().add(-1 * PRE_NOTI_MINUTES, 'm').unix();
    if (eventNotiTime > now) {
      return;
    }
    // ステータスコードが400未満でsetTag
    if (notification(eventData) < 400) {
      event.setTag(tagName, 'true');
    }
  });
}

image.png

説明

  const cal = CalendarApp.getCalendarById(calendarId);
  const events = cal.getEventsForDay(new Date());

Google App ScriptではGoogle CalendarのAPIを利用できるクラスが用意されており、それを利用しています。

  events.forEach(event => {
    const eventData = buildEventObject(event);
    // 通知する必要ないイベントの場合はcontinue
    if (!isNotifiableEvent(eventData)) {
      return;
    }
    // 通知時刻
    const eventNotiTime = eventData.startTime.clone().add(-1 * PRE_NOTI_MINUTES, 'm').unix();
    if (eventNotiTime > now) {
      return;
    }
    // ステータスコードが400未満でsetTag
    if (notification(eventData) < 400) {
      event.setTag(tagName, 'true');
    }
  });

それぞれの予定毎に通知対象となる(開始1分前)ような予定をピックアップしてslack通知させています。
通知が完了した予定には event.setTag(key, value) を用いてmetadataを付与することで

  • 通知が完了したイベントの再送をさせない
  • 通知ができなかったイベントは次のトリガーで通知を飛ばす

ようにしています。
もっと正確にやるのであれば、 event.setTag(key, value) の部分でリトライ処理などを入れても良いかもしれません。

const notification = event => {
  const jsonData = buildSlackBlock(event);
  const payload = JSON.stringify(jsonData); 
  const options = {
    method : 'post',
    contentType : 'application/json',
    payload,
    muteHttpExceptions: true
  };  
  const res = UrlFetchApp.fetch(incomingHookUrl, options);
  return res.getResponseCode();
};

slackのincoming webhookURLを用いてslack通知をさせています。
中身の構造作成は別関数でやっています。
UrlFetchApp.fetch の optionに muteHttpExceptions: true をつけるとエラー系のステータスコードも取得できるので有効にしています。

const buildSlackBlock = eventObj => {
  const restTotalSeconds = eventObj.startTime.unix() - moment().unix();
  const rest = {
    seconds: restTotalSeconds % 60,
    minutes: parseInt(restTotalSeconds / 60, 10),
  }
  return {
    channel,
    mention,
    icon_emoji,
    username,
    link_names: 1,
      blocks: [
          {
            type: 'section',
            text: {
                type: 'mrkdwn',
                text: `${mention} あと *${rest.minutes}${rest.seconds}秒* でmtgだよ`
            }
        },
      {
            type: 'section',
            text: {
                  type: 'mrkdwn',
                text: `*<${eventObj.url}|${eventObj.title}>*`
            }
        },
      {
        type: 'section',
        text: {
              type: 'mrkdwn',
                  text: `*説明:*\n${eventObj.description}`
              },
      },
        {
            type: 'section',
              fields: [
                  {
                    type: 'mrkdwn',
                    text: `*参加者:*\n${eventObj.members}`
                },
                {
                      type: "mrkdwn",
                    text: `*時間:*\n${eventObj.startTime.format("HH:mm")} - ${eventObj.endTime.format("HH:mm")}`
                },
            ]
        }
    ]
  }
};

slack通知の構造を作成しています。
Block Kit Builderを用いると簡単に雛形の作成ができるのでおすすめです。

ちなみにしれっと今回は諸事情でmoment.jsを使用しましたが、moment.jsはレガシープロジェクトとして使用が非推奨となっていますのでその他のライブラリを使用するほうが適切です。

その後

まだ実運用で試してませんが、これでおそらく自分のmtg遅刻は解消されるはずです。
もし、mtg遅刻してしまうなんていう問題に同じく悩まされているのであればぜひお試しください!

17
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
sakas1231
どすこい
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
17
Help us understand the problem. What is going on with this article?