init
This commit is contained in:
354
Notion-bot
Normal file
354
Notion-bot
Normal 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 }) });
|
||||||
|
}
|
||||||
1275
ngaccountant
Normal file
1275
ngaccountant
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user