This commit is contained in:
2026-04-02 21:17:11 +08:00
commit 5b9126c8a9
3 changed files with 1630 additions and 0 deletions

354
Notion-bot Normal file
View File

@@ -0,0 +1,354 @@
/**
* 配置区域
*/
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 }) });
}