Files
Cloudflare-worker/Notion-bot
2026-04-02 21:17:11 +08:00

355 lines
12 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 配置区域
*/
const CONFIG = {
// Notion 配置
NOTION_DATABASE_ID: "修改为对应的NOTION_DATABASE_ID",
NOTION_API_KEY: "修改为对应的NOTION_API_KEY",
// Telegram 配置
TELEGRAM_BOT_TOKEN: "修改为对应的TELEGRAM_BOT_TOKEN",
ALLOWED_USER_IDS: ["修改为对应的ALLOWED_USER_IDS"],
// 基础配置
CITY: "Portland",
STATE_FULL: "Oregon",
ZIP_CODE: "97212"
};
export default {
async fetch(request, env, ctx) {
if (request.method === "GET") {
const data = await handleRedfinAddress(null, true);
return new Response(JSON.stringify(data, null, 2), {
headers: { "Content-Type": "application/json; charset=utf-8" }
});
}
if (request.method !== "POST") return new Response("Bot is running!", { status: 200 });
try {
const update = await request.json();
// --- 1. 鉴权 ---
const userId = getUserId(update);
if (userId && !CONFIG.ALLOWED_USER_IDS.includes(String(userId))) {
return new Response("Unauthorized", { status: 200 });
}
// --- 2. 消息处理 ---
if (update.message && update.message.text) {
const text = update.message.text.trim();
const chatId = update.message.chat.id;
if (text === "/start") {
await sendWelcomeMenu(chatId);
}
else if (text === "📦 立即取号" || text === "取号" || text === "/get") {
await handleGetAccount(chatId);
}
else if (text === "📊 查询库存" || text === "库存" || text === "/stock") {
await handleCheckStock(chatId);
}
else if (text === "🏠 随机地址" || text === "地址" || text === "/address") {
await handleRedfinAddress(chatId);
}
}
// --- 3. 按钮回调处理 ---
else if (update.callback_query) {
await handleCallback(update.callback_query);
}
} catch (e) {
console.error(e);
}
return new Response("OK", { status: 200 });
}
};
// --- 辅助函数 ---
function getUserId(update) {
if (update.message) return update.message.from.id;
if (update.callback_query) return update.callback_query.from.id;
return null;
}
// 菜单
async function sendWelcomeMenu(chatId) {
const text = "👋 欢迎回来!\n\n请点击下方按钮开始操作";
const keyboard = {
keyboard: [
[{ text: "📦 立即取号" }, { text: "📊 查询库存" }],
[{ text: "🏠 随机地址" }]
],
resize_keyboard: true,
is_persistent: true
};
await sendMessage(chatId, text, keyboard);
}
// --- 核心业务逻辑 ---
// 1. 处理地址抓取 (👑 终极降维打击版Overpass API 数据库直连)
async function handleRedfinAddress(chatId, isJsonOnly = false) {
try {
// 💡 放弃搜索,改为“数据库框选”
// (45.530,-122.665,45.555,-122.615) 是完美覆盖 Portland 97212 的地理矩形坐标
// 查询指令:在这个矩形内,找出所有带有 addr:housenumber (门牌号) 和 addr:street (街道) 的真实房屋节点,一次提取 300 个
const bbox = "45.530,-122.665,45.555,-122.615";
const overpassQuery = `[out:json];node(${bbox})["addr:housenumber"]["addr:street"];out 300;`;
// 使用官方与备用节点,防止单点故障
const endpoints = [
"https://overpass-api.de/api/interpreter",
"https://lz4.overpass-api.de/api/interpreter"
];
const apiUrl = `${endpoints[Math.floor(Math.random() * endpoints.length)]}?data=${encodeURIComponent(overpassQuery)}`;
const response = await fetch(apiUrl, {
headers: {
// 生成随机 User-Agent 防止被识别为同一来源限制
"User-Agent": `TelegramAddressBot_CF_${Math.floor(Math.random()*100000)}`,
"Accept": "application/json"
}
});
if (!response.ok) {
throw new Error(`API 拒绝响应: ${response.status}`);
}
const data = await response.json();
const houses = data.elements;
if (!houses || houses.length === 0) {
throw new Error("区域内未提取到有效建筑数据");
}
// --- 成功获取!---
// 从一次性拿到的 300 套真实房屋中,随机挑一套
const randomHouse = houses[Math.floor(Math.random() * houses.length)];
const tags = randomHouse.tags;
// 精准组装地址
const street = `${tags["addr:housenumber"]} ${tags["addr:street"]}`;
const city = tags["addr:city"] || CONFIG.CITY;
const state = "OR";
const zip = tags["addr:postcode"] || CONFIG.ZIP_CODE;
const fullAddr = `${street}, ${city}, ${state} ${zip}`;
const result = {
address: street,
city: city,
state: state,
zip: zip,
fullAddress: fullAddr
};
if (!isJsonOnly && chatId) {
const msg = `🏠 <b>随机房源地址</b>\n\n📍 <code>${result.fullAddress}</code>`;
await sendMessage(chatId, msg);
}
return result;
} catch (e) {
if (chatId) {
await sendMessage(chatId, `❌ <b>获取失败:</b> ${e.message} (请稍后再试)`);
}
return { error: e.message };
}
}
// 2. 处理取号
async function handleGetAccount(chatId) {
const [page, stockCount] = await Promise.all([
queryNotionUnused(),
getStockCount()
]);
if (!page) {
return sendMessage(chatId, "⚠️ <b>库存不足</b>:当前没有“未使用”的账号。");
}
const props = page.properties;
const pageId = page.id;
const account = props["账号"]?.formula?.string || props["账号"]?.rich_text?.[0]?.plain_text || props["账号"]?.title?.[0]?.plain_text || "无账号";
const password = props["密码"]?.formula?.string || props["密码"]?.rich_text?.[0]?.plain_text || "无密码";
// 读取创建时间
let createTimeRaw = props["创建时间"]?.created_time || props["创建时间"]?.date?.start || "未知";
let createTimeStr = "未知";
if (createTimeRaw !== "未知") {
const dateObj = new Date(createTimeRaw);
createTimeStr = dateObj.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour12: false,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
}).replace(/\//g, '-');
}
const messageText =
`📦 <b>提取成功!</b> (📊 库存: ${stockCount})\n\n` +
`账号:<code>${account}</code>\n` +
`密码:<code>${password}</code>\n` +
`组合:<code>${account}----${password}</code>\n` +
`Xbox创建时间${createTimeStr}\n\n` +
`👇 <b>请选择操作:</b>`;
const inlineKeyboard = {
inline_keyboard: [
[
{ text: "✅ 兑换码", callback_data: `redeem|${pageId}` },
{ text: "🔘 其他", callback_data: `other|${pageId}` }
],
[
{ text: "❌ 取消", callback_data: `cancel|${pageId}` }
]
]
};
await sendMessage(chatId, messageText, inlineKeyboard);
}
// 3. 处理库存查询
async function handleCheckStock(chatId) {
const count = await getStockCount();
const text = `📊 <b>库存查询结果</b>\n\n` +
`当前剩余可用白号:<b>${count}</b> 个`;
await sendMessage(chatId, text);
}
// 4. 处理按钮回调
async function handleCallback(query) {
const [action, pageId] = query.data.split("|");
const chatId = query.message.chat.id;
const messageId = query.message.message_id;
if (action === "cancel") {
await deleteMessage(chatId, messageId);
return;
}
let originalText = query.message.text || "";
originalText = originalText.replace(/👇.*/s, "").trim();
// 🔄 重新给账号、密码、组合、地址加上 <code> 标签
originalText = originalText.replace(/(账号:)(.+)/, '$1<code>$2</code>');
originalText = originalText.replace(/(密码:)(.+)/, '$1<code>$2</code>');
originalText = originalText.replace(/(组合:)(.+)/, '$1<code>$2</code>');
originalText = originalText.replace(/(📍)(.+)/, '$1<code>$2</code>');
let newStatus = action === "redeem" ? "兑换码" : "其他";
let statusText = action === "redeem" ? "✅ <b>已标记为:兑换码</b>" : "🔘 <b>已标记为:其他</b>";
const nowStr = new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
hour12: false
});
const success = await updateNotionPage(pageId, newStatus);
if (success) {
const finalText = `${originalText}\n\n${statusText}\n🕒 <b>使用时间:</b>${nowStr}`;
await editMessage(chatId, messageId, finalText);
await answerCallback(query.id, "操作成功!");
} else {
await answerCallback(query.id, "❌ Notion 更新失败");
}
}
// --- Notion API ---
async function queryNotionUnused() {
const url = `https://api.notion.com/v1/databases/${CONFIG.NOTION_DATABASE_ID}/query`;
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${CONFIG.NOTION_API_KEY}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
},
body: JSON.stringify({
"filter": { "property": "状态", "select": { "is_empty": true } }, // 仅筛选空状态
"sorts": [{ "property": "创建时间", "direction": "ascending" }],
"page_size": 1
})
});
const data = await response.json();
if (data.results && data.results.length > 0) return data.results[0];
return null;
}
async function getStockCount() {
const url = `https://api.notion.com/v1/databases/${CONFIG.NOTION_DATABASE_ID}/query`;
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${CONFIG.NOTION_API_KEY}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
},
body: JSON.stringify({
"filter": { "property": "状态", "select": { "is_empty": true } }, // 仅筛选空状态
"page_size": 100
})
});
const data = await response.json();
return data.results ? data.results.length : 0;
} catch (e) {
return "未知";
}
}
async function updateNotionPage(pageId, statusName) {
const url = `https://api.notion.com/v1/pages/${pageId}`;
const now = new Date().toISOString();
const response = await fetch(url, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${CONFIG.NOTION_API_KEY}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json"
},
body: JSON.stringify({
"properties": {
"状态": { "select": { "name": statusName } },
"使用日期": { "date": { "start": now } }
}
})
});
return response.ok;
}
// --- Telegram API ---
async function sendMessage(chatId, text, replyMarkup = null) {
const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/sendMessage`;
const body = { chat_id: chatId, text: text, parse_mode: "HTML" };
if (replyMarkup) body.reply_markup = replyMarkup;
await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
}
async function deleteMessage(chatId, messageId) {
const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/deleteMessage`;
await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, message_id: messageId }) });
}
async function editMessage(chatId, messageId, text) {
const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/editMessageText`;
await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, message_id: messageId, text: text, parse_mode: "HTML" }) });
}
async function answerCallback(id, text) {
const url = `https://api.telegram.org/bot${CONFIG.TELEGRAM_BOT_TOKEN}/answerCallbackQuery`;
await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ callback_query_id: id, text: text }) });
}