master
Amir Hossein Moghiseh 2025-05-09 19:29:12 +03:30
parent 0d48549d36
commit e87735e7bd
5 changed files with 2179 additions and 64 deletions

1797
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,11 @@
"start": "wrangler dev" "start": "wrangler dev"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.25.4",
"wrangler": "^4.14.3" "wrangler": "^4.14.3"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.804.0",
"@aws-sdk/node-http-handler": "^3.370.0"
} }
} }

View File

@ -1,71 +1,386 @@
export default { var __defProp = Object.defineProperty;
async fetch(request, env, ctx) { var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
if (request.method !== 'POST') {
return new Response('Only POST supported', { status: 405 });
}
// src/worker.js
var __defProp2 = Object.defineProperty;
var __name2 = /* @__PURE__ */ __name((target, value) => __defProp2(target, "name", { value, configurable: true }), "__name");
var worker_default = {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url);
if (url.pathname.startsWith("/status/")) {
const eventId = url.pathname.split("/")[2];
return await handleStatusPage(eventId, env);
}
if (request.method !== "POST") return new Response("Only POST", { status: 405 });
const update = await request.json(); const update = await request.json();
const message = update.message; const message = update.message;
const callback = update.callback_query; const callback = update.callback_query;
const inlineQuery = update.inline_query;
if (message?.text?.startsWith('/create_event')) { if (message?.text === "/start") {
return new Response(JSON.stringify({ ok: true })); // Stub — use external admin UI await sendMessage(env.BOT_TOKEN, message.chat.id, "Welcome To Gebels Manager! Use /create_event to create an event or /list to see all events.");
return new Response("OK");
} }
if (callback) { if (message?.text === "/list") {
return handleCallback(callback, env); const events = await listEvents(env.EVENTS);
if (!events.length) {
await sendMessage(env.BOT_TOKEN, message.chat.id, "\u{1F4ED} No events found.");
return new Response("OK");
} }
for (const event of events) {
return new Response('OK', { status: 200 }); const text = `*${event.title}*
${event.description}
[\u2705 View Status](https://ancient-river-4662.siramirmoghi3.workers.dev/status/${event.id})`;
const markup = {
inline_keyboard: [[
{ text: "\u2705 Accept", callback_data: `accept_${event.id}` },
{ text: "\u274C Refuse", callback_data: `refuse_${event.id}` },
{ text: "🗑️ Delete", callback_data: `delete_${event.id}` },
]]
};
await sendPhoto(env.BOT_TOKEN, message.chat.id, event.image, text, "Markdown", markup);
} }
} return new Response("OK");
async function handleCallback(callback, env) {
const data = callback.data;
const [action, eventId] = data.split('_');
const user = callback.from.username || callback.from.first_name;
const acceptsRaw = await env.ACCEPTS.get(eventId);
const accepts = new Set(acceptsRaw ? JSON.parse(acceptsRaw) : []);
if (action === 'accept') {
accepts.add(user);
} else if (action === 'revoke') {
accepts.delete(user);
} }
if (message?.text === "/cancel") {
await env.STATES.delete(message.from.id.toString());
await sendMessage(env.BOT_TOKEN, message.chat.id, "\u274C Event creation canceled.");
return new Response("OK");
}
if (message?.text === "/create_event") {
await env.STATES.put(message.from.id.toString(), JSON.stringify({ step: "title" }));
await sendMessage(env.BOT_TOKEN, message.chat.id, "Please enter the *title* of the event:", "Markdown");
return new Response("OK");
}
if (inlineQuery) {
const query = inlineQuery.query.trim().toLowerCase();
const allEvents = await listEvents(env.EVENTS);
const filtered = allEvents.filter((e) => e.title.toLowerCase().includes(query)).sort((a, b) => b.createdAt - a.createdAt).slice(0, 5);
const results = await Promise.all(filtered.map(async (e) => {
return {
type: "photo",
id: e.id,
photo_file_id: e.image,
// Using file_id for cached photo
caption: `*${e.title}*
${e.description}
await env.ACCEPTS.put(eventId, JSON.stringify([...accepts])); [\u2705 View Status](https://ancient-river-4662.siramirmoghi3.workers.dev/status/${e.id})`,
parse_mode: "Markdown",
const eventRaw = await env.EVENTS.get(eventId); reply_markup: {
const event = JSON.parse(eventRaw); inline_keyboard: [[
{ text: "\u2705 Accept", callback_data: `accept_${e.id}` },
const caption = `*${event.title}*\n${event.description}\n\nAccepted: ${[...accepts].join(', ') || 'None'}`; { text: "\u274C Refuse", callback_data: `refuse_${e.id}` }
const reply_markup = { ]]
inline_keyboard: [[{ }
text: action === 'accept' ? '❌ Revoke' : '✅ Accept', };
callback_data: `${action === 'accept' ? 'revoke' : 'accept'}_${eventId}` }));
}]] return await respondInlineQuery(env.BOT_TOKEN, inlineQuery.id, results);
}
if (callback) return await handleCallback(callback, env);
if (message) {
const userId = message.from.id.toString();
const stateRaw = await env.STATES.get(userId);
if (!stateRaw) return new Response("No state");
const state = JSON.parse(stateRaw);
const { step, data = {} } = state;
if (step === "title") {
data.title = message.text;
await env.STATES.put(userId, JSON.stringify({ step: "description", data }));
await sendMessage(env.BOT_TOKEN, message.chat.id, "Enter the *description*:", "Markdown");
} else if (step === "description") {
data.description = message.text;
await env.STATES.put(userId, JSON.stringify({ step: "image", data }));
await sendMessage(env.BOT_TOKEN, message.chat.id, "Please send an *image* for the event.", "Markdown");
} else if (step === "image" && message.photo) {
const fileId = message.photo.at(-1).file_id;
await env.STATES.put(userId, JSON.stringify({ step: 'limit', data: { ...data, fileId } }));
await sendMessage(env.BOT_TOKEN, message.chat.id, `Please enter a valid number for the acceptance limit or enter 0 for unlimited`, "Markdown");
} else if (step === 'limit') {
const limit = parseInt(message.text);
if (isNaN(limit)) {
await sendMessage(env.BOT_TOKEN, message.chat.id, 'Please enter a valid number for the acceptance limit or enter 0 for unlimited.');
} else {
data.limit = (limit === 0) ? null : limit; // 0 means no limit
const createdAt = new Date().toISOString(); // Track creation time
const eventId = crypto.randomUUID();
const eventData = {
...data,
limit: data.limit,
image: data.fileId, // Move fileId here in the event creation
id: eventId,
creatorId: message.from.id,
createdAt, // Add the creation timestamp
}; };
const telegramRes = await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/editMessageCaption`, { // Store event in the database
method: 'POST', await env.EVENTS.put(eventId, JSON.stringify(eventData));
headers: { 'Content-Type': 'application/json' },
// Clear the state after event creation
await env.STATES.delete(userId);
await sendMessage(env.BOT_TOKEN, message.chat.id, `✅ Event created: *${data.title}*`, 'Markdown');
}
} else {
await sendMessage(env.BOT_TOKEN, message.chat.id, "Unexpected input. Try /create_event again.");
await env.STATES.delete(userId);
}
}
return new Response("OK");
} catch (err) {
console.error("Unhandled Exception:", {
message: err.message,
stack: err.stack,
name: err.name
});
return new Response("Internal Error", { status: 500 });
}
}
};
async function handleCallback(callback, env) {
const data = callback.data;
const [action, eventId] = data.split("_");
const user = callback.from.username || callback.from.first_name || "unknown";
const [rawAccepts, rawRefuses] = await Promise.all([
env.ACCEPTS.get(eventId),
env.REFUSES.get(eventId)
]);
const accepts = new Set(rawAccepts ? JSON.parse(rawAccepts) : []);
const refuses = new Set(rawRefuses ? JSON.parse(rawRefuses) : []);
accepts.delete(user);
refuses.delete(user);
const eventRaw = await env.EVENTS.get(eventId);
if (!eventRaw) return new Response("Event not found");
const event = JSON.parse(eventRaw);
if (action === "delete") {
// Only allow creator to delete the event
console.log(callback.from.id, event.creatorId);
if (callback.from.id !== event.creatorId) {
await sendMessage(env.BOT_TOKEN, callback.message.chat.id, "Only the event creator can delete this event.");
return new Response("Unauthorized");
}
// Delete event and all related data
await Promise.all([
env.EVENTS.delete(eventId),
env.ACCEPTS.delete(eventId),
env.REFUSES.delete(eventId)
]);
// Edit message or send update
await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/editMessageText`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
chat_id: callback.message.chat.id, chat_id: callback.message.chat.id,
message_id: callback.message.message_id, message_id: callback.message.message_id,
caption, text: "🗑️ Event deleted by creator."
parse_mode: 'Markdown',
reply_markup
}) })
}); });
return new Response("Event deleted");
}
if (action === "accept") {
// Check if the acceptance limit has been reached
if (accepts.has(user)) {
await sendMessage(env.BOT_TOKEN, callback.message.chat.id, `You have already accepted the event *${event.title}*`, 'Markdown');
return new Response('OK');
}
if (event.limit !== 0 && event.limit !== null && accepts.size >= event.limit) {
await sendMessage(env.BOT_TOKEN, callback.message.chat.id, `Sorry, the event *${event.title}* has reached its acceptance limit.`, 'Markdown');
return new Response('OK');
}
accepts.add(user);
} else if (action === "refuse") {
refuses.add(user);
}
await Promise.all([
env.ACCEPTS.put(eventId, JSON.stringify([...accepts])),
env.REFUSES.put(eventId, JSON.stringify([...refuses]))
]);
const creatorId = event.creatorId;
const creatorMessage = `Your event *${event.title}* has received a new ${action === "accept" ? "acceptance ✅" : "refusal ❌"}.`;
await sendMessage(env.BOT_TOKEN, creatorId, creatorMessage, "Markdown");
// Wait for all notifications to be sent
if (!eventRaw) return new Response("Event not found");
await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/answerCallbackQuery`, { await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/answerCallbackQuery`, {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
callback_query_id: callback.id, callback_query_id: callback.id,
text: action === 'accept' ? 'Accepted!' : 'Revoked!' text: `You selected ${action === "accept" ? "\u2705 Accept" : "\u274C Refuse"}`
}) })
}); });
return new Response("OK");
return new Response(JSON.stringify({ ok: true }));
} }
__name(handleCallback, "handleCallback");
async function sendMessage(token, chat_id, text, parse_mode) {
return fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id, text, parse_mode })
});
}
__name(sendMessage, "sendMessage");
async function respondInlineQuery(token, queryId, results) {
return fetch(`https://api.telegram.org/bot${token}/answerInlineQuery`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ inline_query_id: queryId, results, cache_time: 0 })
});
}
__name(respondInlineQuery, "respondInlineQuery");
async function listEvents(KV) {
const list = await KV.list();
return await Promise.all(
list.keys.map(async (k) => {
const raw = await KV.get(k.name);
return JSON.parse(raw);
})
);
}
__name(listEvents, "listEvents");
async function handleStatusPage(eventId, env) {
const [acceptsRaw, refusesRaw, eventRaw] = await Promise.all([
env.ACCEPTS.get(eventId),
env.REFUSES.get(eventId),
env.EVENTS.get(eventId)
]);
console.log("eventRaw", eventId, acceptsRaw, refusesRaw, eventRaw);
if (!eventRaw) return new Response("Event not found", { status: 404 });
const event = JSON.parse(eventRaw);
const accepts = acceptsRaw ? JSON.parse(acceptsRaw) : [];
const refuses = refusesRaw ? JSON.parse(refusesRaw) : [];
const fileResponse = await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/getFile?file_id=${event.image}`);
const fileData = await fileResponse.json();
const fileUrl = `https://api.telegram.org/file/bot${env.BOT_TOKEN}/${fileData.result.file_path}`;
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gebels - ${event.title}</title>
<link href="https://fonts.googleapis.com/css2?family=UnifrakturCook:wght@700&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
background: linear-gradient(to bottom right, #2e003e, #120024);
font-family: 'UnifrakturCook', cursive;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.card {
background: rgba(255, 255, 255, 0.1);
border: 2px solid #b47cff;
border-radius: 16px;
padding: 20px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(180, 124, 255, 0.3);
backdrop-filter: blur(10px);
}
h1 {
font-size: 2em;
margin-bottom: 10px;
color: #ffe86a;
}
p {
font-size: 1.1em;
margin-bottom: 20px;
}
h2 {
font-size: 1.3em;
margin-top: 20px;
color: #94f0ff;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
padding: 5px 10px;
margin: 5px 0;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 8px;
font-size: 1em;
}
a {
color: #ffe86a;
text-decoration: none;
}
a:hover {
text-decoration: underline;
color: #fff4c2;
}
</style>
</head>
<body>
<div class="card">
<h1>${event.title}</h1>
<img src="${fileUrl}" alt="${event.title}" style="width: 100%; border-radius: 8px; margin-bottom: 20px;" />
<p>${event.description}</p>
<h2>\u2705 Accepted</h2>
<ul>
${accepts.map((u) => `<li><a href="https://t.me/${u}" target="_blank">@${u}</a></li>`).join("")}
</ul>
<h2>\u274C Refused</h2>
<ul>
${refuses.map((u) => `<li><a href="https://t.me/${u}" target="_blank">@${u}</a></li>`).join("")}
</ul>
</div>
</body>
</html>
`;
return new Response(html, {
headers: { "Content-Type": "text/html" }
});
}
__name(handleStatusPage, "handleStatusPage");
async function sendPhoto(token, chat_id, photo, caption, parse_mode = "Markdown", reply_markup = null) {
const payload = {
chat_id,
photo,
caption,
parse_mode,
...reply_markup ? { reply_markup } : {}
};
return fetch(`https://api.telegram.org/bot${token}/sendPhoto`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
}
__name(sendPhoto, "sendPhoto");
__name2(handleCallback, "handleCallback");
export {
worker_default as default
};

View File

@ -1,6 +0,0 @@
name = "ancient-river-4662"
main = "worker.js"
compatibility_date = "2023-08-23"
[unsafe.metadata.observability]
enabled = true

View File

@ -2,6 +2,16 @@ name = "ancient-river-4662"
main = "src/worker.js" main = "src/worker.js"
workers_dev = true workers_dev = true
compatibility_date = "2025-05-08" compatibility_date = "2025-05-08"
account_id = "6a8d4d305440e990b98e950f6453f950"
kv_namespaces = [
{ binding = "EVENTS", id = "0fc103ecca3b41d9af481cfb01ed9d52" },
{ binding = "ACCEPTS", id = "831bf99c66a44ec5a35e10c0dc4d086c" },
{ binding = "REFUSES", id = "36e31a9aebf04480a321234917a150a2" },
{ binding = "STATES", id = "af6bc31086de41f38055ced84b7c9606" }
]
[observability] [observability]
enabled = true enabled = true