2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

42-review-notifierを作ったお

Posted at

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の取得

  1. Discordで通知したいチャンネルを開く
  2. ⚙️ チャンネルの編集 → 連携サービスウェブフック
  3. 新しいウェブフック → 任意の名前を設定
  4. ウェブフックURLをコピー

2. ユーザーIDの取得

  1. Discord設定 → 詳細設定 → 開発者モード ON
  2. 自分のアイコンを右クリック → IDをコピー

3. Google Apps Scriptプロジェクト作成

  1. https://script.google.com にアクセス(42のメールを受信するGoogleアカウント)
  2. 新しいプロジェクト をクリック
  3. プロジェクト名を「42 Review Notifier」などに設定

4. コードの配置

config.jsを作成

GASエディタで「ファイル」→「新規」→「スクリプト」を選択し、configという名前で以下を作成:

config.js
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. テスト実行

  1. 関数選択で testNotification を選択
  2. 実行 をクリック
  3. 初回は 権限の確認 が必要
    • 「詳細」→ 「(プロジェクト名)に移動」→ 「許可」
  4. Discordに通知が届けばOK!

実装のポイント

1. メール本文からの日時抽出

42のメールは以下のフォーマットです:

You will review somebody's code from December 07, 2025 11:45 for 30 minutes.

正規表現で日時を抽出:

main.js
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 を使ってスケジュールを保存:

main.js
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 関数で、時刻が来たリマインダーを通知:

main.js
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形式で見やすく:

main.js
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),
  });
}

通知例

予約確定時(即座)

image

@あなた 🔔 42 Evaluation 予約確定
📅 予定時刻: 2025年12月23日 14:30
⏱️ 所要時間: 30分
⏰ 10分前にメンション通知します

10分前

@あなた ⏰ 予定の10分前です!
予定時刻: 2025年12月23日 14:30

工夫したポイント

1. 処理済みメールの管理

同じメールを何度も処理しないよう、メールIDを保存:

main.js
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. エラーハンドリング

main.js
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を作りたい方の参考になれば幸いです!

リポジトリ

参考

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?