coiai Logo
ホーム記事コラムPodcast会社情報
お問い合わせ
Featured

Line GAS の連携ができなくて、Cloudflare Workers を使って自動通知を送る

これは何❓

弊社では毎日朝にミーティングを行なっているのですが、LineにミーティングのURLを自動で送付してほしいなと思い。
LineBotを使って自動化した話です。

⚙️機能

毎日11:00(JST)に Google Meet のURLをLINEグループへ自動投稿
• LINEグループで setup とメッセージする → 送信先グループIDを取得して保存
• LINEグループで ping → pong(できてるかの確認)
• Cloudflare Cron(Workflow)で毎日実行 → LINEにMeet URLをpush

Google App Script と Line で試してみた

Line と Google App Script を使うと302エラーが出て動作が安定しなかったので他の方法を試すことにしました。

事前準備(Cloudflare / LINE)

1) LINE Developers(Messaging API)

LINEのグループIDを取得する方法

こちらの記事を参考にさせていただきました!
• Webhook URL:https://<あなたのworker>.workers.dev/
• Use webhook:ON
• 応答メッセージ:OFF(推奨)
• BotをLINEグループに招待

2) Cloudflare Workersで設定したこと

Cloudflare Workersとは、ざっくり言うと 「超軽量なサーバーレス関数(JavaScript/TypeScript)」です。

サーバーを用意せずに、HTTPリクエストを受けて処理して返すことができます!

Secretsの設定

• LINE_ACCESS_TOKEN:長期チャネルアクセストークン(Messaging API)
• MEET_URL:毎日投下したいGoogle MeetのURL

アクセストークンは Line Developers からコピーしておいて

Workers & Pages の Variables and Secrets に Add してあげればOKです。

KVの設定

• namespaceを作成(例:LINE_KV)

• Binding

Cron Trigger

• 毎日11:00 JST → UTC 02:00 なので
• Cron:0 2 * * *

実装(そのまま動く src/index.js)

※トークンはコードに直書きせず、Secretに設定しました!

// src/index.js

// src/index.js
export default {
  // LINE Webhook 受信
  async fetch(request, env, ctx) {
    const reqId = crypto.randomUUID();
    const url = new URL(request.url);

    console.log(`[${reqId}] ===== fetch start =====`);
    console.log(`[${reqId}] method=${request.method} path=${url.pathname}`);

    // GETなどは疎通確認用に200で返す
    if (request.method !== "POST") {
      console.log(`[${reqId}] non-POST -> 200 OK`);
      return new Response("OK", { status: 200 });
    }

    // Bodyを一旦テキストで受けてログ(※個人情報が入る可能性があるので運用で注意)
    let raw = "";
    try {
      raw = await request.text();
      console.log(`[${reqId}] raw body=${raw}`);
    } catch (e) {
      console.log(`[${reqId}] body read error`, e);
      return new Response("OK", { status: 200 });
    }

    // JSON parse
    let body;
    try {
      body = JSON.parse(raw || "{}");
    } catch (e) {
      console.log(`[${reqId}] JSON parse error`, e);
      return new Response("OK", { status: 200 });
    }

    // Verify は events=[] のことがある → 200返して終わり
    const event = body?.events?.[0];
    if (!event) {
      console.log(`[${reqId}] no events (verify?) -> 200 OK`);
      return new Response("OK", { status: 200 });
    }

    console.log(`[${reqId}] event.type=${event.type}`);
    console.log(`[${reqId}] source=${JSON.stringify(event.source || {})}`);

    // replyToken がないイベントもある
    const replyToken = event.replyToken;
    console.log(`[${reqId}] hasReplyToken=${!!replyToken}`);

    // メッセージイベントだけ処理(必要なら拡張)
    if (event.type === "message" && event.message?.type === "text") {
      const text = (event.message.text || "").trim();
      const lower = text.toLowerCase();
      console.log(`[${reqId}] text="${text}"`);

      // ping → 動作確認
      if (lower === "ping") {
        if (!replyToken) return new Response("OK", { status: 200 });
        await safeReply(env, reqId, replyToken, "pong");
        return new Response("OK", { status: 200 });
      }

      // setup → groupId を KV に保存(グループ内で実行)
      if (lower === "setup") {
        const source = event.source || {};
        if (source.type === "group" && source.groupId) {
          await env.KV.put("LINE_GROUP_ID", source.groupId);
          console.log(`[${reqId}] saved LINE_GROUP_ID=${source.groupId}`);

          if (replyToken) {
            await safeReply(env, reqId, replyToken, "✅ このグループを送信先に設定しました!");
          }
          return new Response("OK", { status: 200 });
        } else {
          console.log(`[${reqId}] setup called but not in group`);
          if (replyToken) {
            await safeReply(env, reqId, replyToken, "⚠️ setup はグループ内で送ってください(グループIDが取れません)");
          }
          return new Response("OK", { status: 200 });
        }
      }

      // その他のテキストは無視(必要ならここで返信してもOK)
      console.log(`[${reqId}] text ignored`);
    } else {
      console.log(`[${reqId}] non-text message or non-message event ignored`);
    }

    return new Response("OK", { status: 200 });
  },

  // Cron Trigger(毎日送信)
  async scheduled(event, env, ctx) {
    const runId = crypto.randomUUID();
    console.log(`[${runId}] ===== scheduled start =====`);
    console.log(`[${runId}] cron=${event.cron}`);

    const groupId = await env.KV.get("LINE_GROUP_ID");
    console.log(`[${runId}] groupId=${groupId}`);

    if (!groupId) {
      console.log(`[${runId}] no groupId saved -> skip`);
      return;
    }

    const meetUrl = env.MEET_URL;
    console.log(`[${runId}] hasMeetUrl=${!!meetUrl}`);

    if (!meetUrl) {
      console.log(`[${runId}] MEET_URL missing -> skip`);
      return;
    }

    const msg = `今日のGoogle Meetはこちら👇\n${meetUrl}`;
    await safePush(env, runId, groupId, msg);

    console.log(`[${runId}] ===== scheduled end =====`);
  },
};

// --- LINE Reply(失敗しても例外で落とさずログを残す) ---
async function safeReply(env, reqId, replyToken, text) {
  if (!env.LINE_ACCESS_TOKEN) {
    console.log(`[${reqId}] LINE_ACCESS_TOKEN missing (secret not set?)`);
    return;
  }

  try {
    const res = await fetch("https://api.line.me/v2/bot/message/reply", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${env.LINE_ACCESS_TOKEN}`,
      },
      body: JSON.stringify({
        replyToken,
        messages: [{ type: "text", text }],
      }),
    });

    const body = await res.text();
    console.log(`[${reqId}] reply status=${res.status} body=${body}`);
  } catch (e) {
    console.log(`[${reqId}] reply fetch error`, e);
  }
}

// --- LINE Push(Cronなど任意タイミング用) ---
async function safePush(env, runId, to, text) {
  if (!env.LINE_ACCESS_TOKEN) {
    console.log(`[${runId}] LINE_ACCESS_TOKEN missing (secret not set?)`);
    return;
  }

  try {
    const res = await fetch("https://api.line.me/v2/bot/message/push", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${env.LINE_ACCESS_TOKEN}`,
      },
      body: JSON.stringify({
        to,
        messages: [{ type: "text", text }],
      }),
    });

    const body = await res.text();
    console.log(`[${runId}] push status=${res.status} body=${body}`);
  } catch (e) {
    console.log(`[${runId}] push fetch error`, e);
  }
}

/wrangler.toml

name = "line-meet-bot"
main = "src/index.js"
compatibility_date = "2026-01-12"

動作確認

1. LINEグループにBotを招待
2. グループで ping → pong が返る
3. グループで setup → 「設定しました」が返る
(※KV未設定だとここで無反応になりがち)
4. Cronを 0 2 * * * に設定
5. 翌日から毎日11:00(JST)にMeet URLが投稿される

投稿日: 2026年1月12日
カテゴリ: 自動化
タグ: プログラミング
coiai

coiai

この記事もおすすめ

「最新版を使えばいい」は本当に正しい?Dependabotを使って“pin + 自動更新”にする理由

「最新版を使えばいい」は本当に正しい?Dependabotを使って“pin + 自動更新”にする理由

依存ライブラリやGitHub Actions、Terraform Providerのバージョン管理をしていると、よく出てくるのが次の考え方です。 新しいバージョンが出たら、常に最新版へ上げればいいのでは? 一見すると合理的です。 実際、古いバージョンを長期間使い続けるより、継続的にアップデートした方が、脆弱性や非互換の問題を後回しにしにくくなります。 ただし、実際の開発運用では、 「バージョンを固定しない」ことと「常に新しい状態を保つ」ことは別です。 むしろ、 バージョンは固定し、Dependabotで自動的に更新PRを作る という運用の方が、安全性と更新頻度を両立しやすくなります。 今回は、実際の開発チームで出た「pinするべきか、しないべきか」という議論をベースに、Dependabotをどう使うべきか整理します。 きっかけは「そもそもpinしなくていいのでは?」という疑問 あるプロジェクトで、GitHub ActionsやTerraform Providerのバージョンを更新する話が出ました。 その中で、 理想的には何もpinしない方がいい。新しいバージョンが出たら常にアップグレードすればいいのでは? という意見が出ました。 この考え方自体は間違っていません。 目標としては、 依存関係を古いまま放置しない というのが正しいです。 ただし、その目標を実現する方法として「pinしない」を選ぶと、別の問題が出てきます。 そこで出てきたのが、 「pinしない」のではなく、「pin + automated bumps」にしよう という考え方です。 つまり、 バージョンは固定する。しかし、更新作業は自動化する。 これがDependabotを使う理由です。 GitHub Actionsでは、floating tagがリスクになる たとえばGitHub Actionsでは、次のような指定をすることがあります。 これは分かりやすく、一般的な書き方です。 ただし、v4 のようなタグは、厳密には「特定のコードそのもの」を表しているわけではありません。 タグが別のコミットを指すように変更されれば、同じ @v4 という記述でも、実際に実行されるコードが変わる可能性があります。 CI/CDで実行されるActionは、ビルド環境やデプロイ権限にアクセスすることがあります。 そのため、これはサプライチェーンセキュリティの観点でも重要です。 より厳密に管理するなら、特定のコミットSHAに固定します。 こうすると、同じ設定から実行されるコードが勝手に変わることはありません。 ただし問題があります。 固定しただけでは、そのバージョンは永遠に古くなっていきます。 そこでDependabotを使います。 新しいバージョンが出たら、 という流れにします。 つまり、 固定することで再現性を確保し、Dependabotで鮮度を維持する [&hellip;]

デフォルトサムネイル

GMKtec EVO-X2にUbuntuを入れたら有線LANが認識されなかった話

オンプレミスAIサーバー(ローカルLLM + 社内文書RAG)の構築を始めました。機材はGMKtec EVO-X2、AMD Ryzen AI Max+ 395搭載のミニPCです。128GBのユニファイドメモリをGPUに大きく割り当てられるので、ローカルLLM用途では今いちばん面白いマシンだと思っています。 この記事は初日の作業ログです。結論から言うと「インストールUSBが起動しない」「有線LANが認識されない」という2つの罠を踏んだので、同じ構成を組む方のために記録しておきます。 OSはLinuxかWindowsか 最初に迷うのがここですが、サーバー用途ならUbuntu Server一択でした。 日常操作はすべてブラウザ(Open WebUI)経由なので、サーバーOSにGUIは不要です。管理画面が欲しければCockpitを入れれば、Webブラウザからメトリクス確認・サービス再起動・ターミナルまで使えます。「CUIのサーバー + Web管理画面」が現代の標準形だと思います。 BIOS設定 ― EVO-X2で一番大事なのはVRAM割り当て 電源ON直後にDel連打でBIOS(AMI)へ入ります。設定したのは3つです。 罠その1: インストールUSBが起動しない Ubuntu Server 24.04のISOをMacから dd でUSBメモリに書き込み、いざブート……しません。Boot Overrideに出てくる「UEFI OS」を選んでもWindowsが立ち上がってしまいます。 切り分けの過程が学びでした。 原因はおそらく、dd の書き込みが完全に終わる前に抜いてしまったことです。dd は進捗表示が止まってからもバッファの書き出しが続くので、完了統計が表示されるまで待たなければいけません。パーティションテーブルはディスク先頭に書かれるため、途中で切れていても一見正常に見えるのが厄介なところです。 教訓: インストールUSB作成はbalenaEtcherを使いましょう。 書き込み後に自動でベリファイ(照合)まで走るので、「書けたつもり」が原理的に起きません。Etcherで書き直したら一発で起動しました。 インストール時の選択 罠その2: 有線LANが認識されない インストール完了、ログイン成功。しかし ip a を見ると有線LANにIPがありません。それどころか、よく見ると有線のインターフェース自体が存在しません。 カーネルログを見て原因が確定しました。 EVO-X2の2.5GbEチップ(Realtek RTL8125D)が新しすぎて、Ubuntu 24.04の標準カーネル6.8のドライバが知らないチップだったのです。インストール中にネットに繋がらなかったのもこれが原因でした。ケーブルを何度挿し直しても無駄だったわけです。 教訓: 「繋がらない」時はケーブルを疑う前に dmesg を読みましょう。 カーネルログには大抵、答えがそのまま書いてあります。 解決: HWEカーネル 新しいチップに対応するには、新しいカーネルを入れれば解決します。Ubuntuには**HWE(Hardware [&hellip;]

FSD v2.1 Feature-Sliced Design を導入する

FSD v2.1 Feature-Sliced Design を導入する

この記事は何? 公式のドキュメントやチュートリアル、exsample を読んで理解するのが一番いいですが、備忘録として残しておきます。特に後述の Skills, Steiger については開発する際にあった方が便利なので、それについても記事にします。 FSDとは? FSD (Feature-Sliced Design) はフロントエンドのアーキテクチャの一つです。 FSDとは責務で分けていくアーキテクチャと違い、ページやエンティティで分けていくアーキテクチャというとわかりやすいと思います。 コードの分離の仕方について話す前に、FSDで前提となる この3つについて理解する必要があります。 レイヤー より細かく分けていく方法がありますが、この記事では導入だけ触れたいので、3つのレイヤーのみ説明します。 です。この流れは上(app)に向かえば向かうほどアプリケーション固有、下(shared)に向かうほど汎用的になります。 原則として上位レイヤーは下位レイヤーを利用できるが、下位レイヤーから上位レイヤーへ依存できません。 始め方としては小さく初めて、必要になったらレイヤーを増やしてくようです。 スライス ブログアプリであればレイヤーの pages には といったページに分けられるため、このような構成になります。このblog-list, blog-detail, user がスライスです。これらはページやエンティティによって分けられます。 セグメント これらのmodel, ui といった部分がエンティティです。これらは責務にあたります。sharedに関しては直下にスライスではなくセグメントが入ります。 FSD 2.1 で重要なのは Pages First です。とにかくPagesから作って、細分化の必要ができたら、下位レイヤーに抽出します。 Steiger Steiger とはアーキテクチャリンターです。以下でインストールできます。 AIコーディングのために Claude Skills https://github.com/feature-sliced/skills?utm_source=chatgpt.com このリポジトリが公式に公開されています。 これで Skills を追加できるので、claude にFSDの設計知識を共有できます。

フロントエンドのアーキテクチャについて最初から考える

フロントエンドのアーキテクチャについて最初から考える

これは何? この記事は基本に立ち戻りアーキテクチャの選定を試みた際の備忘録です。Feature-Sliced Design(FSD)や Vertical Slice Architecture など、モダンなアーキテクチャがありますが、どのような判断をもとに選定するのかを考え直します。 アーキテクチャの地図 アーキテクチャを理解するために、以下のような学習のための地図を作りました。設計原則、アーキテクチャ、個別の設計パターンへと詳細になっていきます。 そもそも、、、 このコードは何を担当しているのかを考えるのが最初の問です。 商品一覧画面を作るときに といったタスクを書き出します。 これら全てを一つの巨大なコンポーネントとして作ることもできますが、管理が大変になるのは皆さんもお分かりだと思います。参考:https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html?utm_source=chatgpt.com ここからどこまでを同じモジュールとして管理するのか?という設計の問題が始まります。 Cohesion と Coupling CohesionとCouplingはそれぞれ、凝集度、結合度というようです。https://qiita.com/dsudo/items/ee3fee1f558c7f1b359f Cohesionは関係するコードがまとまっているか? Couplingは別のコードにどれだけ依存しているか? です。 例えば以下のようなコードがあります。 技術的な種類ごとに整理されていますが、商品一覧のコーディングをするのにそれぞれ別のディレクトリを行き来する必要があります。一方で、以下のように書いた場合はどうでしょうか? この書き方なら、商品に依存するコードがまとまっています。 この書き方が Feature-based Architecture, Vertical Slice Architecture に繋がっていきます。 コロケーションについて colocation Place code as close to where it&#8217;s relevant as possible 同じ種類のコードを集めることではなく、一緒に変更されるコードを近くに置くことです。 https://kentcdodds.com/blog/colocation?utm_source=chatgpt.com Layered Architecture について ここからアーキテクチャの話です。 Layered Architectureでは、技術的責務でアプリケーションを分離します。 これのメリットは責務がわかりやすいところです。 しかし、この方法では前述までとは変わり、機能を変更するときに、横断して変更する可能性があります。 つまり、技術的には分かれている一方で、機能は複数に分散するわけです。 [&hellip;]

この記事を書いた会社

株式会社coiaiは、「想像できることを美しく実現」を掲げ、XR・Web・アプリ・システム開発およびDX支援を行う会社です。 創業2022年、東京都練馬区に本社を置き、要件のヒアリングからPoC(概念実証)、本番運用まで一貫して伴走します。 まずはお気軽にご相談ください。

商号株式会社 coiai創業2022年1月設立2025年1月23日資本金1,500,000円(設立時点)本社所在地東京都練馬区関町北 3-6-9代表者代表取締役 竹村 啓佑 / 代表取締役 服部 陽良

主なご相談内容

  • Webサイト・Webアプリの設計・開発
  • スマートフォンアプリ(iOS / Android)の開発
  • XR・AR・VRを活用した体験設計・開発
  • DX推進・業務システムの企画・開発
  • PoC(概念実証)から本番運用までの伴走支援
  • マーケティング・コンサルティング
会社概要・役員紹介を見る

詳しい会社情報は会社概要ページでご覧いただけます。

資料請求・無料相談

導入要件のヒアリングからPoC、本番運用まで伴走します。まずはお気軽にご相談ください。

フォームで問い合わせメールで相談

お問い合わせの前に 個人情報保護方針 をご確認ください。

株式会社coiai株式会社coiai

XR, Web, システム開発, DX — 想像できることを美しく実現

メールでお問い合わせ

サービス

on-premise AI

基幹システム Anchor

XR開発

会社情報

会社概要

ブログ

お問い合わせ

法務

プライバシーポリシー


所在地

〒177-0051 東京都練馬区関町北3-6-9

営業時間

平日 10:00-18:00

土日祝 休み

info@coiai.net

© 2023–2026 株式会社coiai. All rights reserved.