😺
Next.js cron-job.orgで毎朝カレンダー通知するslack bot作成してみた
記事の内容
Next.js Vercel cron-job.orgを使用して毎朝カレンダー通知するslack bot作成する方法を紹介します!完成イメージはこんな感じ本当はメールアドレスとか詳細も表示されるように実装しているが個人情報を隠すため情報量が少ない画像を載せています!!!
対象読者
Next.jsを触ったことがあり何かしら作ってみたい方
slackのbot機能開発に興味のある方
実装方法
プロジェクトを立ち上げる
# npx
npx create-next-app@latest
# yarn
yarn create next-app
ディレクトリ構成
my-next-app/
├── lib/
│ ├── googleCalendar.js
│ └── slack.js
├── pages/
│ ├── api/
│ │ └── cron.js
│ └── index.js
├── .env
├── package.json
├── next.config.js
└── README.md
Slack API Keyの取得
この方の記事を参考にslackのapi key取得及び設定を行う
実装コード
cron.js
import { authorize, getEvents } from '../../lib/googleCalendar';
import { sendReminder } from '../../lib/slack';
import moment from 'moment-timezone';
import 'moment/locale/ja';
moment.locale('ja');
export default async function handler(req, res) {
res.setHeader('Cache-Control', 'no-store, max-age=0');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.setHeader('Surrogate-Control', 'no-store');
try {
if (req.method === 'GET') {
const auth = authorize();
const events = await getEvents(auth);
const getTokyoDate = (offsetDays = 0) => {
const tokyoDate = moment.tz('Asia/Tokyo').add(offsetDays, 'days');
return tokyoDate.format('YYYY-MM-DD');
};
const today = getTokyoDate();
const tomorrow = getTokyoDate(1);
const nextWeek = getTokyoDate(7);
const todayEvents = events.filter(event => event.start.dateTime && event.start.dateTime.startsWith(today));
const tomorrowEvents = events.filter(event => event.start.dateTime && event.start.dateTime.startsWith(tomorrow));
const nextWeekEvents = events.filter(event => event.start.dateTime && event.start.dateTime.startsWith(nextWeek));
const formatDate = (dateString) => {
return moment.tz(dateString, 'Asia/Tokyo').format('YYYY年M月D日(dddd)');
};
const formatTime = (dateTime) => {
return moment.tz(dateTime, 'Asia/Tokyo').format('HH:mm');
};
const formatAttendees = (attendees) => {
return attendees.map(a => a.displayName || a.email).join(', ');
};
const formatEvent = (event) => {
let eventDetails = `- ${event.summary} at ${formatTime(event.start.dateTime)}`;
if (event.location) {
eventDetails += `\n 場所: ${event.location}`;
}
if (event.description) {
eventDetails += `\n 説明: ${event.description}`;
}
if (event.attendees && event.attendees.length > 0) {
eventDetails += `\n 参加者: ${formatAttendees(event.attendees)}`;
}
if (event.creator) {
eventDetails += `\n 作成者: ${event.creator.displayName || event.creator.email}`;
}
if (event.organizer) {
eventDetails += `\n 主催者: ${event.organizer.displayName || event.organizer.email}`;
}
if (event.hangoutLink) {
eventDetails += `\n Google Meetリンク: ${event.hangoutLink}`;
}
if (event.htmlLink) {
eventDetails += `\n 詳細リンク: ${event.htmlLink}`;
}
if (event.description && event.description.includes('@channel')) {
eventDetails += `\n <!channel> @channel`;
}
return eventDetails;
};
const formatEventSection = (title, events) => {
let section = `*${title}*\n`;
if (events.length === 0) {
section += 'なし\n';
} else {
events.forEach(event => {
section += `${formatEvent(event)}\n\n`;
});
}
return section;
};
const currentTokyoTime = moment.tz('Asia/Tokyo').format('YYYY年M月D日(dddd) HH:mm');
let message = `:spiral_calendar_pad: *現在のTokyo時間* (${currentTokyoTime}) :spiral_calendar_pad:\n\n`;
message += `:spiral_calendar_pad: *今日の予定* (${formatDate(today)}) :spiral_calendar_pad:\n`;
message += '```\n';
message += formatEventSection('今日の予定', todayEvents);
message += '```\n\n';
message += `:spiral_calendar_pad: *明日の予定* (${formatDate(tomorrow)}) :spiral_calendar_pad:\n`;
message += '```\n';
message += formatEventSection('明日の予定', tomorrowEvents);
message += '```\n\n';
message += `:spiral_calendar_pad: *来週の予定* (${formatDate(nextWeek)}) :spiral_calendar_pad:\n`;
message += '```\n';
message += formatEventSection('来週の予定', nextWeekEvents);
message += '```\n';
await sendReminder(message);
res.status(200).json({ message: 'Reminders sent successfully!' });
} else {
res.status(405).json({ message: 'Method not allowed' });
}
} catch (error) {
console.error('Error in handler:', error);
res.status(500).json({ error: error.message });
}
}
slack.js
import { WebClient } from '@slack/web-api';
const slackClient = new WebClient(process.env.SLACK_BOT_TOKEN);
export async function sendReminder(message) {
await slackClient.chat.postMessage({
channel: process.env.SLACK_CHANNEL_ID,
text: message,
icon_emoji: ':bot:',
mrkdwn: true
});
}
googleCalendar.js
import { google } from 'googleapis';
import moment from 'moment-timezone';
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
const calendar = google.calendar('v3');
export async function getEvents(auth) {
const calendarId = 'primary';
const tokyoTimeMin = moment().tz('Asia/Tokyo').toISOString(true);
const res = await calendar.events.list({
calendarId: calendarId,
timeMin: tokyoTimeMin, // 東京タイムゾーンを考慮したtimeMinを設定
maxResults: 10,
singleEvents: true,
orderBy: 'startTime',
auth: auth // 認証情報を追加
});
// ここでレスポンスヘッダーの日付をJSTに変換する
const serverDate = moment(res.headers.date).tz('Asia/Tokyo').format('YYYY-MM-DDTHH:mm:ssZ');
const expiresDate = moment(res.headers.expires).tz('Asia/Tokyo').format('YYYY-MM-DDTHH:mm:ssZ');
return res.data.items;
}
export function authorize() {
const { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN } = process.env;
const oAuth2Client = new google.auth.OAuth2(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET
);
oAuth2Client.setCredentials({ refresh_token: GOOGLE_REFRESH_TOKEN });
return oAuth2Client;
}
.env
取得した自分のものを設定してください
SLACK_BOT_TOKEN=
SLACK_CHANNEL_ID=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REFRESH_TOKEN=
Discussion