🎧

近くのライブ情報お知らせLINE botを作った話

に公開

概要

下記についてまとめています ✍🏻

  • 近くのライブ情報のお知らせ bot を GAS と LINE Messaging API で作った ✌🏻
  • なにを使ってどう作ったかの詳細
  • Antigravity を使った感想

作った理由

自分は音楽が好きでよくライブに行きます。
好きなアーティストの情報を追ってチケットを取る以外にも、

  • あんまり詳しくないけど聴いてて気になるバンド
  • 近くに来るなら行ってみたいアーティスト
  • 好きなライブハウス

を調べてチケットを取ってライブに行くこともよくあります。
ですが、数あるライブハウス/アーティストの公式サイトや SNS を巡回するのは手間なので、

  • 自分が好きなジャンルの音楽
  • 近隣のライブハウス等音楽イベント開催情報

を自動で自分がよく見る媒体に通知してくれたら便利だし、ライブ開催後に「近くに来てたなら行きたかった(;^ω^)」と残念な気持ちになることも減らせるんでは?思ったので、LINE でお知らせする仕組みを作りました!

使用ツール

  • GAS
  • Google AI Studio
  • LINE Messaging API
  • Antigravity

最近話題の Antigravity (Google 製 AI エージェント搭載の IDE) を使ってなにかを作るのが初めてだったのですが、めちゃくちゃ便利すぎてびっくりしてます。有能すぎる。
※Gemini API と LINE Messaging API は無料で使用できるので(個人利用の範囲)、コストかからず運用できます

通知内容

  • 日時
  • アーティスト名
  • 場所
  • チケット情報

作成した LINE bot を友だち登録することで、上記の情報を通知するように設定しました!
毎週月曜の午前 10~11 時に通知するようにしていますが、「今すぐ検索」 とチャットすると随時検索して結果を返すようにもしています。

検索条件について

「設定確認」 とメッセージを送ると、現在の検索条件を確認できます。
エリア、ジャンル、期間をそれぞれ変更できるようにしています。

ざっくり構成と設定手順

[LINEアプリ]
   (メッセージ送受信)
[GAS (Webアプリ)]
   (検索指示)
[Gemini API (Grounding with Google Search)]
   (検索結果)
[GAS]
   (整形して通知)
[LINEアプリ]

大きく分けると GAS と LINE Messaging API の 2 つの部分で構成されています。

GAS の設定手順

① Google AI Studio で Gemini API キーを作成

  • Google AI Studio にログイン
  • サイドメニュー下部の Get API Key を押下
  • 画面右上の「API キーを作成」を押下し、適当なキー名を入力して作成(プロジェクト未作成であれば一緒に作成してください)
  • 生成された API キーをコピー

② GAS を作成しコードを追加する

  • Google Apps Script で「スクリプトを開始」→「新しいプロジェクト」を押下して作成する
  • 下記のコードを追加する
コード.gs
// ==========================================
// 設定エリア
// ※セキュリティのため、APIキーは直接書かず
// GASの「プロジェクトの設定 > スクリプトプロパティ」に保存して読み込みます
// ==========================================
const scriptProperties = PropertiesService.getScriptProperties();
const GEMINI_API_KEY = scriptProperties.getProperty("GEMINI_API_KEY");
const LINE_ACCESS_TOKEN = scriptProperties.getProperty("LINE_ACCESS_TOKEN");

// デフォルト設定
const DEFAULT_SETTINGS = {
  area: "◯◯市",
  genres: ["ロック", "パンク", "Jpop", "ロックバンド"],
  period: "1 ヶ月以内",
};

// ==========================================
// LINE からのメッセージ受信 (Webhook)
// ==========================================
function doPost(e) {
  const json = JSON.parse(e.postData.contents);
  const events = json.events;

  events.forEach((event) => {
    if (event.type === "message" && event.message.type === "text") {
      handleUserMessage(event);
    }
  });

  return ContentService.createTextOutput(
    JSON.stringify({ content: "post ok" })
  ).setMimeType(ContentService.MimeType.JSON);
}

// ユーザーメッセージの処理
function handleUserMessage(event) {
  const userId = event.source.userId;
  const text = event.message.text.trim();
  const replyToken = event.replyToken;

  // 設定変更コマンドの判定
  if (text.startsWith("エリア設定:")) {
    const newArea = text.replace("エリア設定:", "").trim();
    saveUserSetting(userId, "area", newArea);
    replyLineMessage(replyToken, `エリアを「${newArea}」に変更しました!`);
  } else if (text.startsWith("ジャンル設定:")) {
    const newGenres = text
      .replace("ジャンル設定:", "")
      .trim()
      .split(/[,、\s]+/)
      .filter(Boolean); // カンマやスペースで分割
    saveUserSetting(userId, "genres", newGenres);
    replyLineMessage(
      replyToken,
      `ジャンルを「${newGenres.join("、")}」に変更しました!`
    );
  } else if (text.startsWith("期間設定:")) {
    const newPeriod = text.replace("期間設定:", "").trim();
    saveUserSetting(userId, "period", newPeriod);
    replyLineMessage(replyToken, `検索期間を「${newPeriod}」に変更しました!`);
  } else if (text === "設定確認") {
    const settings = getUserSettings(userId);
    const msg = `現在の設定:\n\n📍 エリア: ${
      settings.area
    }\n🎸 ジャンル: ${settings.genres.join("、")}\n📅 期間: ${
      settings.period
    }\n\n変更したい場合は\n「エリア設定:東京」\n「ジャンル設定:ジャズ、R&B」\n「期間設定:2週間以内」\nのように送ってください。`;
    replyLineMessage(replyToken, msg);
  } else if (text === "今すぐ検索") {
    replyLineMessage(replyToken, "検索を開始します...少々お待ちください。");
    // 検索実行(非同期的に実行できないため、ここでは簡易的に検索関数を呼び出すが、LINE の応答時間制限(30 秒)に注意)
    // 本格的には CacheService 等でトリガー起動させるのがベターだが、今回は直接呼ぶ
    const settings = getUserSettings(userId);
    const result = searchLiveInfo(settings);
    if (result) {
      pushLineMessage(userId, result);
    } else {
      pushLineMessage(userId, "条件に合うライブ情報は見つかりませんでした。");
    }
  } else {
    replyLineMessage(
      replyToken,
      "コマンドが分かりません。\n\n「設定確認」\n「今すぐ検索」\n\n または設定変更コマンドを送ってください。"
    );
  }
}

// ==========================================
// 設定の保存・取得 (PropertiesService)
// ==========================================
function saveUserSetting(userId, key, value) {
  const scriptProperties = PropertiesService.getScriptProperties();
  // ユーザーごとの設定として保存するため、キーに userId を含める
  // 値は文字列である必要があるため JSON.stringify
  scriptProperties.setProperty(`${userId}_${key}`, JSON.stringify(value));
}

function getUserSettings(userId) {
  const scriptProperties = PropertiesService.getScriptProperties();
  const area =
    JSON.parse(scriptProperties.getProperty(`${userId}_area`)) ||
    DEFAULT_SETTINGS.area;
  const genres =
    JSON.parse(scriptProperties.getProperty(`${userId}_genres`)) ||
    DEFAULT_SETTINGS.genres;
  const period =
    JSON.parse(scriptProperties.getProperty(`${userId}_period`)) ||
    DEFAULT_SETTINGS.period;

  return { area, genres, period };
}

// ==========================================
// メイン処理 (定期実行用)
// ==========================================
function main() {
  // ここでは簡易的に「最後にアクセスしたユーザー」または「特定のユーザー ID」に対して実行する想定
  // 本来は全ユーザー ID を保存してループさせる必要があるが、今回は自分専用 Bot として実装

  // ★ ここに自分の LINE User ID を直接書くか、一度 Bot に話しかけてログから特定する
  // 今回は「最後に設定変更したユーザー」の設定を使って実行する簡易実装とします
  // ※ 厳密にやるなら、doPost で userId をリストに保存する処理が必要

  // 暫定対応: プロパティに保存されているキーからユーザー ID を推測(1 人利用想定)
  const props = PropertiesService.getScriptProperties().getProperties();
  let targetUserId = null;
  for (let key in props) {
    if (key.includes("_area")) {
      targetUserId = key.split("_")[0];
      break;
    }
  }

  if (!targetUserId) {
    console.log(
      "ユーザー ID が見つかりません。一度 LINE でメッセージを送ってください。"
    );
    return;
  }

  const settings = getUserSettings(targetUserId);
  const liveInfoText = searchLiveInfo(settings);

  if (liveInfoText) {
    pushLineMessage(targetUserId, liveInfoText);
  }
}

// ==========================================
// Gemini 検索機能
// ==========================================
function searchLiveInfo(settings) {
  const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`;

  const promptText = `
あなたは優秀なイベントリサーチャーです。以下の条件でライブ情報を検索し、教えてください。

# 検索条件

- **対象エリア**: ${settings.area}
- **対象施設**: ライブハウス、コンサートホール、アリーナ、市民会館等、音楽イベントを開催する建物全般
- **対象期間**: 今日から${settings.period}
- **抽出条件**: 出演者のジャンルが「${settings.genres.join(
    "、"
  )}」に関連するもの、またはジャンル不問で有名なアーティスト

# 出力フォーマット

見つかったイベントを以下の形式で出力してください。
Markdown 記法(\*\*や-など)は使わず、絵文字と改行で見やすくしてください。

📅 [日付] [イベント名 / 出演者]
📍 [会場名]
🎫 [チケット価格] / [販売状況]
🔗 [販売サイトの URL] (もしあれば)

---

※ 情報が見つからない場合は「条件に合うライブ情報は見つかりませんでした。」と答えてください。
`;

  const payload = {
    contents: [{ parts: [{ text: promptText }] }],
    tools: [{ google_search: {} }],
  };

  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };

  try {
    const response = UrlFetchApp.fetch(apiUrl, options);
    const data = JSON.parse(response.getContentText());

    if (data.candidates && data.candidates[0].content) {
      return data.candidates[0].content.parts[0].text;
    } else {
      return null;
    }
  } catch (e) {
    console.error("エラー:", e);
    return null;
  }
}

// ==========================================
// LINE 送信機能 (分割送信対応版)
// ==========================================
function replyLineMessage(replyToken, text) {
  const url = "https://api.line.me/v2/bot/message/reply";
  const messages = createMessages(text);

  const payload = {
    replyToken: replyToken,
    messages: messages,
  };
  sendToLine(url, payload);
}

function pushLineMessage(userId, text) {
  const url = "https://api.line.me/v2/bot/message/push";
  const messages = createMessages(text);

  const payload = {
    to: userId,
    messages: messages,
  };
  sendToLine(url, payload);
}

// テキストを 2000 文字ごとに分割して複数のメッセージオブジェクトを作成する
function createMessages(text) {
  const maxLength = 2000; // 安全マージンをとって 2000 文字
  const messages = [];

  for (let i = 0; i < text.length; i += maxLength) {
    messages.push({
      type: "text",
      text: text.substring(i, i + maxLength),
    });
    // LINE は一度に送れるのは 5 件まで
    if (messages.length >= 5) break;
  }
  return messages;
}

function sendToLine(url, payload) {
  const options = {
    method: "post",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + LINE_ACCESS_TOKEN,
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true, // エラー詳細を見るために true 推奨
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const code = response.getResponseCode();
    if (code !== 200) {
      console.error("LINE 送信エラー:", response.getContentText());
    }
  } catch (e) {
    console.error("LINE 送信例外:", e);
  }
}

⚠️ 重要: API キーの設定 コード内のセキュリティを守るため、API キーはコードに直接書かず、GAS の機能で保存します。

  • 画面左側の歯車アイコン(プロジェクトの設定)を押下
  • 下部の「スクリプトプロパティ」で「スクリプトプロパティを追加」を押下
  • プロパティに GEMINI_API_KEY 、値に取得した Gemini のキーを入力
  • 同様に LINE_ACCESS_TOKEN 、値に LINE messaging API のトークンを入力して「スクリプトプロパティを保存」

③ デプロイする

  • 画面右上の「デプロイ」ボタンを押す
  • 歯車アイコンを押して「ウェブアプリ」を選択する
  • 適当な説明文、次のユーザーとして実行には「自分(メアド)」、アクセスできるユーザーは「全員」を選択する


アクセスユーザーを「全員」に設定することで、誰でもアクセスできるようにする。ただし、誰でも実行するわけではないので、実行するユーザーは「自分(メアド)」に設定する

④ 定期実行のトリガー設定

毎週自動でお知らせを送るために、GAS のトリガーを設定します。

  • 画面左側の時計アイコン(トリガー)をクリック
  • 右下の「トリガーを追加」ボタンをクリックし、下記のように設定して保存する
    • 実行する関数: main
    • イベントのソース: 時間主導型
    • トリガーのタイプ: 週ベースのタイマー
    • 曜日と時刻: 月曜日 午前 10 時〜11 時

ここまでで GAS 側の準備は完了です!

LINE Messaging API の設定手順

① プロバイダ作成 ~ チャネルの作成

  • LINE Developers にログイン(個人 LINE アカウントでのログインですぐ作成できました)
  • コンソール(ホーム)画面で「新規プロバイダー作成」を押下し、適当なプロバイダー名を入力して作成する


コンソール画面


新規プロバイダー作成

  • 作成したプロバイダーに移動し、「新規チャネル作成」をクリックして「Messaging API」 を選択する


新規チャネル作成

  • LINE アカウントを作成するよう促されるので、そのまま進める


公式アカウントとはありますが、認証済みアカウントになったり勝手に料金発生・公開されることはありません

  • アカウント名(Bot の名前)など必要な項目を入力して「作成」

  • LINE 公式アカウントを作成した後、LINE 公式アカウント設定画面の「Messaging API」メニューから「LINE Developers で設定する」ボタンを押し、そこで作成したプロバイダーを選択して紐付ける


途中プライバシーポリシーの設定画面になりますが、ここは空で OK です

② アクセストークンの取得

  • LINE Developers 画面に戻り、作成したチャネルの「Messaging API 設定」タブを開く

  • 一番下の「チャネルアクセストークン(長期)」を発行し、コピーする(→ GAS のコードに貼り付ける)

③ Webhook URL の設定

  • GAS を「ウェブアプリ」としてデプロイして発行された URL をコピー
  • LINE Developers の Webhook 設定の「Webhook URL」欄に貼り付けて「更新」を押下する
  • 「Webhook の利用」 スイッチを ON にする

設定中、WebhookURL の検証を押下するとエラー表示が出ましたが、GAS 側でテスト実行すると問題なく LINE に通知が飛んだので、同じくエラーになる方は実際に動くかを確認するといいかもしれません。

④ 自動応答の無効化

  • 「LINE 公式アカウント機能」のリンクから応答設定へ移動
  • 「応答メッセージ」をオフ にする(Bot が勝手に定型文を返さないようにするため)

⑤ 友だち追加

  • QR コードを読み込んで、自分のスマホで Bot を友だち追加する
    • LINE bot 作成直後に追加して通知を確認しながら進めるとやりやすかったです

以上で LINE 側の設定完了です。
実際に GAS で実行したり、LINE から「今すぐ検索」などメッセージを送り動くかを確認して、問題なければ完成です 🤘🏻

Antigravity を使った感想

実装の計画書を渡すと、今回の軸である場所検索とライブ情報収集の検証、判定、実装まで一連をちゃちゃっとやってくれました。
LINE の Developer ツールは初めて使うため不明点や、画面のどこから設定したらよいかなど聞くと、実際にブラウザを起動して誘導して教えてくれたり(Chrome の Antigravity 拡張機能を使用)とものすごいわかりやすかったです。

さいごに

自分がほしい情報を通知してくれるものを作れたので大満足です。
ただ、実装テスト中に気になるライブを見つけたのでチケットサイトを確認したところ、ちょうど 2 次受付終了してるのを発見してしまいました(;^ω^)< 人生うまくいかないな!

マーベリックスのテックブログ

Discussion