42 TokyoのEvaluation予約をDiscordで通知するBotを作りました
はじめに
42 Tokyoでは、ピアラーニングの一環として他の学生のコードをレビューする「Evaluation」というシステムがあります。予約が入るとGmailに通知が来るのですが、メールを見逃して遅刻しそうになることがありました。
そこで、Gmailの予約メールを自動検知してDiscordにメンション付きで通知するBotをGoogle Apps Scriptで作成しました。
課題
- 📧 42からのEvaluation予約メールを見逃す
- ⏰ 予約時刻の直前まで気づかない
- 💻 常時起動するサーバーを用意したくない(コスト)
解決方法
Google Apps Script(GAS)+ Discord Webhook で実装
特徴
- ✅ 完全無料: サーバー不要、GASの無料枠で動作
- ✅ 2回通知: 予約確定時&10分前にメンション
- ✅ 自動日時抽出: メール本文から予定時刻を自動パース
- ✅ セットアップ簡単: 10分程度で完了
技術スタック
| 要素 | 技術 |
|---|---|
| 言語 | JavaScript (Google Apps Script) |
| メール取得 | Gmail API(GAS組み込み) |
| 通知 | Discord Webhook |
| スケジューリング | GASトリガー(時間駆動) |
| ストレージ | PropertiesService(GAS組み込み) |
アーキテクチャ
┌─────────────┐
│ 42 Intra │ メール送信
└──────┬──────┘
│
↓
┌─────────────┐
│ Gmail │ 受信
└──────┬──────┘
│
↓ 5分おきにチェック
┌─────────────┐
│ GAS Bot │ 日時抽出 → スケジュール保存
└──────┬──────┘
│
↓ Webhook
┌─────────────┐
│ Discord │ メンション通知(2回)
└─────────────┘
セットアップ
1. Discord Webhookの取得
- Discordで通知したいチャンネルを開く
- ⚙️ チャンネルの編集 → 連携サービス → ウェブフック
- 新しいウェブフック → 任意の名前を設定
- ウェブフックURLをコピー
2. ユーザーIDの取得
- Discord設定 → 詳細設定 → 開発者モード ON
- 自分のアイコンを右クリック → IDをコピー
3. Google Apps Scriptプロジェクト作成
- https://script.google.com にアクセス(42のメールを受信するGoogleアカウント)
- 新しいプロジェクト をクリック
- プロジェクト名を「42 Review Notifier」などに設定
4. コードの配置
config.jsを作成
GASエディタで「ファイル」→「新規」→「スクリプト」を選択し、configという名前で以下を作成:
const CONFIG = {
DISCORD_WEBHOOK_URL: 'https://discord.com/api/webhooks/xxxxx/xxxxx',
DISCORD_USER_ID: '123456789012345678',
EMAIL_SUBJECT_FILTER: 'You have a new booking',
REMINDER_MINUTES_BEFORE: 10,
SEARCH_HOURS: 24,
};
main.jsを追加
デフォルトの コード.gs に以下のコードを貼り付け:
5. トリガーの設定
GASエディタで ⏰ トリガー → トリガーを追加
メールチェック用
- 実行する関数:
checkEmails - イベントのソース: 時間主導型
- 時間ベースのトリガータイプ: 分ベースのタイマー
- 間隔: 5分おき
リマインダー送信用
- 実行する関数:
sendScheduledReminder - イベントのソース: 時間主導型
- 時間ベースのトリガータイプ: 分ベースのタイマー
- 間隔: 1分おき
6. テスト実行
- 関数選択で
testNotificationを選択 - 実行 をクリック
- 初回は 権限の確認 が必要
- 「詳細」→ 「(プロジェクト名)に移動」→ 「許可」
- Discordに通知が届けばOK!
実装のポイント
1. メール本文からの日時抽出
42のメールは以下のフォーマットです:
You will review somebody's code from December 07, 2025 11:45 for 30 minutes.
正規表現で日時を抽出:
function extractDateTime(body) {
const monthNames = {
'january': 0, 'february': 1, 'march': 2, // ...
};
const datePattern = /from\s+([a-z]+)\s+(\d{1,2}),\s+(\d{4})\s+(\d{1,2}):(\d{2})/i;
const match = body.match(datePattern);
if (match) {
const month = monthNames[match[1].toLowerCase()];
const day = parseInt(match[2]);
const year = parseInt(match[3]);
const hour = parseInt(match[4]);
const minute = parseInt(match[5]);
return new Date(year, month, day, hour, minute);
}
return null;
}
2. リマインダーのスケジュール管理
GASの PropertiesService を使ってスケジュールを保存:
function scheduleReminder(messageId, subject, eventTime) {
const props = PropertiesService.getScriptProperties();
const reminders = JSON.parse(props.getProperty('reminders') || '[]');
const reminderTime = new Date(
eventTime.getTime() - CONFIG.REMINDER_MINUTES_BEFORE * 60 * 1000
);
if (reminderTime <= new Date()) return; // 過去はスキップ
reminders.push({
messageId: messageId,
subject: subject,
eventTime: eventTime.toISOString(),
reminderTime: reminderTime.toISOString(),
});
props.setProperty('reminders', JSON.stringify(reminders));
}
1分おきに実行される sendScheduledReminder 関数で、時刻が来たリマインダーを通知:
function sendScheduledReminder() {
const props = PropertiesService.getScriptProperties();
const reminders = JSON.parse(props.getProperty('reminders') || '[]');
const now = new Date();
const updatedReminders = [];
for (const reminder of reminders) {
const reminderTime = new Date(reminder.reminderTime);
if (now >= reminderTime) {
sendReminderNotification(reminder.subject, new Date(reminder.eventTime));
} else {
updatedReminders.push(reminder);
}
}
props.setProperty('reminders', JSON.stringify(updatedReminders));
}
3. Discord Webhookでの通知
Embed形式で見やすく:
function sendNewEmailNotification(subject, sender, body, extractedDateTime) {
const mention = `<@${CONFIG.DISCORD_USER_ID}>`;
const durationMatch = body.match(/for\s+(\d+)\s+minutes/i);
const duration = durationMatch ? durationMatch[1] + '分' : null;
const embed = {
title: '🔔 42 Evaluation 予約確定',
color: 0x00babc, // 42カラー
fields: [
{
name: '📅 予定時刻',
value: Utilities.formatDate(extractedDateTime, 'Asia/Tokyo', 'yyyy年MM月dd日 HH:mm'),
inline: false
},
{ name: '⏱️ 所要時間', value: duration, inline: true },
{
name: '⏰ リマインダー',
value: `${CONFIG.REMINDER_MINUTES_BEFORE}分前にメンション通知します`,
inline: false
},
],
timestamp: new Date().toISOString(),
};
const payload = {
content: mention,
embeds: [embed],
};
UrlFetchApp.fetch(CONFIG.DISCORD_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
});
}
通知例
予約確定時(即座)
@あなた 🔔 42 Evaluation 予約確定
📅 予定時刻: 2025年12月23日 14:30
⏱️ 所要時間: 30分
⏰ 10分前にメンション通知します
10分前
@あなた ⏰ 予定の10分前です!
予定時刻: 2025年12月23日 14:30
工夫したポイント
1. 処理済みメールの管理
同じメールを何度も処理しないよう、メールIDを保存:
function checkEmails() {
const processedIds = getProcessedEmailIds();
const emails = searchEmails();
for (const email of emails) {
const messageId = email.getId();
if (processedIds.includes(messageId)) continue; // スキップ
// 処理...
markAsProcessed(messageId);
}
}
function getProcessedEmailIds() {
const props = PropertiesService.getScriptProperties();
return JSON.parse(props.getProperty('processedEmails') || '[]');
}
function markAsProcessed(messageId) {
const ids = getProcessedEmailIds();
ids.push(messageId);
// 100件まで保持(古いものは削除)
while (ids.length > 100) ids.shift();
PropertiesService.getScriptProperties()
.setProperty('processedEmails', JSON.stringify(ids));
}
2. 設定の分離
機密情報をGitHubにコミットしないよう、設定ファイルを分離:
.
├── main.js # メインロジック
├── config.js # 個人設定(.gitignoreで除外)
├── config.example.js # 設定テンプレート
└── .gitignore
3. エラーハンドリング
function sendDiscordMessage(payload) {
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true,
};
try {
const response = UrlFetchApp.fetch(CONFIG.DISCORD_WEBHOOK_URL, options);
console.log('Discord送信成功:', response.getResponseCode());
} catch (e) {
console.error('Discord送信エラー:', e);
}
}
ハマったポイント
GASの時刻の扱い
GASは Utilities.formatDate() でタイムゾーンを明示的に指定する必要があります:
// ❌ これだとUTCになる
const dateStr = new Date().toLocaleString();
// ✅ 正しい
const dateStr = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy年MM月dd日 HH:mm');
トリガーの実行間隔
- メールチェックは5分おきで十分
- リマインダー送信は1分おきじゃないと遅延する可能性
今後の改善案
- 複数の件名フィルターに対応(配列化)
- Evaluation終了後の自動既読化
- Slack対応
- カレンダー連携(Google Calendar API)
- GASのClasp対応(ローカル開発環境)
まとめ
Google Apps Scriptを使うことで、完全無料かつサーバー不要でGmail→Discord通知Botを実装できました。
42 Tokyoに通う方や、同様のメール通知Botを作りたい方の参考になれば幸いです!
リポジトリ
