export default { async fetch(request, env, ctx) { if (request.method !== "POST") return new Response("OK"); const bodyText = await request.text(); let body; try { body = JSON.parse(bodyText); } catch { return new Response("OK"); } const events = body.events || []; if (!events.length) return new Response("OK"); for (const ev of events) { try { if (ev.type !== "message") continue; if (!ev.message) continue; const msgType = ev.message.type; if (msgType !== "image" && msgType !== "video") continue; const userId = ev.source?.userId; const replyToken = ev.replyToken; const messageId = ev.message.id; if (!userId || !replyToken || !messageId) continue; // ① 同意チェック(GASに問い合わせ) const consent = await checkConsent(https://script.google.com/macros/s/AKfycby9uV4t2JIwWurD43kqnbqZU2ZVu6tf1xrFCyb9P8QtV3VD284NfciGcpZicFFIJDisHA/exec); if (!consent.ok || !consent.consent) { // ② 未同意:即reply(保存しない / GAS保存処理呼ばない) const text = `投稿された写真・動画・コメントは、当コンテンツ(LINE・Web・SNS等)において 無償で使用・編集される場合があります。 内容に同意のうえ、投稿してください。 ${https://docs.google.com/forms/d/e/1FAIpQLSeleeTdBkOjSj8TcX6kyRikY8Q3ArOLT9uvyADMt2FCuAk2ww/viewform?usp=dialog}`; await replyText(env., replyToken, text); continue; } // ③ 同意済み:緑枠の案内(仕様そのまま) await replyText( QYg9tG33UQplDm55Awv7mbzUkgnK3MiMA4D+x3bGG/BwUZtip1OsWqHRihsAkLQRGrWY6iE7M5k6diLaoMtEbkkp6QxIAV+malS5qI3AkgZk0h6htKsu9sSayk2MAltlIVqlSjo1p0WE3jhLiIy66QdB04t89/1O/w1cDnyilFU=, replyToken, "📸 写真はこのトークにそのまま送ってください(何枚でもOK)" ); // ④ 画像/動画本体をLINEから取得 const contentResp = await fetch(`https://api-data.line.me/v2/bot/message/${messageId}/content`, { headers: { Authorization: `Bearer ${env.LINE_CHANNEL_ACCESS_TOKEN}` }, }); if (!contentResp.ok) continue; const ct = contentResp.headers.get("content-type") || ""; const arrayBuffer = await contentResp.arrayBuffer(); const u8 = new Uint8Array(arrayBuffer); const mimeType = ct.includes("image") ? ct : ct.includes("video") ? ct : (msgType === "video" ? "video/mp4" : guessImageMime(u8)); // ⑤ ファイル名 const now = new Date(); const ts = formatJST(now); const ext = mimeType.includes("jpeg") ? "jpg" : mimeType.includes("png") ? "png" : mimeType.includes("gif") ? "gif" : mimeType.includes("mp4") ? "mp4" : "bin"; const fileName = `${ts}_${userId}_${messageId}.${ext}`; // ⑥ base64化 const contentBase64 = toBase64(u8); // ⑦ EXIF撮影日時(今回は「空」で送る:後で完成させる) // まずは動く完成(Drive保存 + receivedAtログ)優先 const shotAt = ""; // ⑧ GASへ保存依頼(doPost) await fetch(env.GAS_EXEC_URL, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId, messageId, mimeType, fileName, contentBase64, shotAt, // 空OK(後でEXIF実装) }), }); } catch (e) { continue; } } return new Response("OK"); }, }; async function checkConsent(gasExecUrl, userId) { try { const url = `${gasExecUrl}?mode=consent&userId=${encodeURIComponent(userId)}`; const r = await fetch(url, { method: "GET" }); const j = await r.json(); return { ok: !!j.ok, consent: !!j.consent, consentAt: j.consentAt || "" }; } catch { return { ok: false, consent: false, consentAt: "" }; } } async function replyText(token, replyToken, text) { await fetch("https://api.line.me/v2/bot/message/reply", { method: "POST", headers: { "content-type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ replyToken, messages: [{ type: "text", text }], }), }); } function guessImageMime(u8) { if (u8.length > 3 && u8[0] === 0xff && u8[1] === 0xd8) return "image/jpeg"; if (u8.length > 4 && u8[0] === 0x89 && u8[1] === 0x50 && u8[2] === 0x4e && u8[3] === 0x47) return "image/png"; if (u8.length > 3 && u8[0] === 0x47 && u8[1] === 0x49 && u8[2] === 0x46) return "image/gif"; return "application/octet-stream"; } function toBase64(u8) { let s = ""; const chunk = 0x8000; for (let i = 0; i < u8.length; i += chunk) { s += String.fromCharCode.apply(null, u8.subarray(i, i + chunk)); } return btoa(s); } function formatJST(d) { const t = new Date(d.getTime() + 9 * 60 * 60 * 1000); const pad = (n) => String(n).padStart(2, "0"); return `${t.getUTCFullYear()}${pad(t.getUTCMonth()+1)}${pad(t.getUTCDate())}_${pad(t.getUTCHours())}${pad(t.getUTCMinutes())}${pad(t.getUTCSeconds())}`; }