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