المستوى المطلوب: مبتدئ
لو فريقك من 6 مطورين بيكتب standup يدوي كل يوم في Slack، أنت بتضيّع 18 دقيقة جماعية يومياً. البوت اللي هتبنيه هنا بيسأل كل واحد 3 أسئلة الساعة 9 صباحاً، يجمّع الردود، ويعرضها كرسالة واحدة منظّمة في القناة. التكلفة: 0 دولار. الزمن: 70 سطر JavaScript.
اعمل Daily Standup Bot على Cloudflare Workers
المشكلة باختصار
الـ Daily Standup مهم، لكن تنفيذه يدوي بيخلق 3 مشاكل في وقت واحد. واحد بينسى يكتب، التاني بيكتب رواية في كل خانة، والتالت بيكتب بعد ما الميتنج خلص. الفريق اللي بيشتغل remote بيدفع الثمن أكتر — وقت ضائع وشفافية ناقصة. الحل مش "إجبار الناس"، الحل نظام: يسأل، يستنى، يلخّص.
مثال للمبتدئ: ناظر المدرسة وصندوق الورق
تخيّل ناظر مدرسة بدل ما يطوف على كل فصل ويسأل المعلمين شخصياً، عمل سؤال موحد بيتعلّق على كل باب الساعة 8، وكل معلم بيحط ورقة في صندوق بإجابته. الناظر بيفتح الصندوق الساعة 8:30 ويقرا كل الردود مرّة واحدة. ده بالظبط اللي هنبنيه: الناظر هو Cloudflare Worker، السؤال هو DM في Slack، الصندوق هو KV Storage. كل واحد بيرد لما يقدر، والملخّص بيتنشر في وقت محدّد.
التعريف العلمي: Cron Trigger و KV Storage
Cloudflare Worker هو سكربت JavaScript بيشتغل على edge servers في أكتر من 300 مدينة. بيقبل تشغيل بطريقتين: HTTP request (Webhook) أو Cron schedule. الـ Cron Trigger بيشغّل دالة scheduled() تلقائياً حسب توقيت محدّد بصيغة Unix cron. الـ KV Storage هو Key-Value store موزّع، latency متوسط 12ms عالمياً، مجاني لحد 100 ألف read يومي و 1000 write. ده يعني إن البوت بتاعك ممكن يخدم فريق 50 شخص بصفر تكلفة فعلية.
الخطوات الست لبناء البوت
- أنشئ Slack App: روح
api.slack.com/apps← Create New App ← From Scratch. سمّيه "Standup Bot" واختار workspace فريقك. - فعّل Bot Permissions: في OAuth & Permissions، ضيف الصلاحيات:
chat:write,im:write,im:history,users:read. ثبّت الـ App في الـ workspace واخد الـ Bot Token (بيبدأ بـxoxb-). - أنشئ Cloudflare Worker: سجّل في
dash.cloudflare.com(مجاني)، اعمل Worker جديد باسمstandup-bot. - اربط KV Namespace: من تبويب Storage، اعمل KV namespace اسمه
STANDUPثم اربطه بالـ Worker كمتغيّر بنفس الاسم. - الصق الكود: تحت في الجزء الجاي، وضع الـ Bot Token كـ Secret اسمه
SLACK_TOKENمن dashboard الـ Worker. - اعمل Cron Trigger: من Triggers، ضيف
0 6 * * 0-4للسؤال الساعة 9 ص بتوقيت القاهرة، و0 7 * * 0-4للملخّص الساعة 10 ص (الأحد للخميس).
الكود الكامل (70 سطر)
const TEAM = ["U01ABCD", "U02EFGH", "U03IJKL"]; // Slack User IDs
const QUESTIONS = [
"عملت إيه إمبارح؟",
"هتعمل إيه النهارده؟",
"في حاجة بتعطّلك؟"
];
const CHANNEL = "C04XYZ12"; // قناة الفريق
async function slack(method, token, payload) {
const r = await fetch(`https://slack.com/api/${method}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify(payload)
});
return r.json();
}
async function askEveryone(env) {
const today = new Date().toISOString().slice(0, 10);
const text = `*Standup ${today}*\n` +
QUESTIONS.map((q, i) => `${i + 1}. ${q}`).join("\n") +
`\n\nرد على الرسالة دي بإجاباتك في رسالة واحدة.`;
for (const user of TEAM) {
await slack("chat.postMessage", env.SLACK_TOKEN, { channel: user, text });
}
}
async function collectAndPost(env) {
const today = new Date().toISOString().slice(0, 10);
const replies = [];
for (const user of TEAM) {
const answer = await env.STANDUP.get(`${today}:${user}`);
if (!answer) { replies.push(`*<@${user}>* — ما ردّش`); continue; }
const info = await slack("users.info", env.SLACK_TOKEN, { user });
replies.push(`*${info.user.real_name}*\n${answer}`);
}
const summary = `:sunny: *Standup ${today}*\n\n` + replies.join("\n\n");
await slack("chat.postMessage", env.SLACK_TOKEN, { channel: CHANNEL, text: summary });
}
export default {
async scheduled(event, env) {
const hour = new Date().getUTCHours();
if (hour === 6) await askEveryone(env);
if (hour === 7) await collectAndPost(env);
},
async fetch(req, env) {
const body = await req.json();
if (body.type === "url_verification") {
return Response.json({ challenge: body.challenge });
}
const ev = body.event;
if (ev?.type === "message" && ev.channel_type === "im" && !ev.bot_id) {
const today = new Date().toISOString().slice(0, 10);
await env.STANDUP.put(`${today}:${ev.user}`, ev.text, { expirationTtl: 86400 });
}
return new Response("ok");
}
};
بعد ما تلصق الكود، روح Slack App ← Event Subscriptions، فعّله، وحط رابط الـ Worker كـ Request URL. ضيف message.im في Subscribe to bot events.
السيناريو الواقعي والأرقام
فريق من 8 مطورين، الـ standup الكلامي اليومي بياخد متوسط 14 دقيقة من كل واحد قبل الميتنج (تجهيز + تبديل سياق) + 22 دقيقة وقت الميتنج نفسه = 36 دقيقة × 8 = 4.8 ساعة جماعية يومياً. بعد البوت: كل مطور بيرد على DM في 2 دقيقة، وميتنج اختياري للنقاش بس 8 دقايق لمن يحتاج. النتيجة: من 4.8 ساعة لـ 1.3 ساعة جماعية يومياً. توفير 73%.
التكلفة على Cloudflare: Workers Free plan بيدعم 100 ألف request يومياً مجاناً. البوت بيستهلك تقريباً 30 request في اليوم (8 رسائل صباح + 8 ردود + 8 users.info + ملخّص). الفاتورة الحقيقية: صفر دولار ما دام الفريق أقل من 50 شخص، حسب صفحة Cloudflare KV Limits.
Trade-offs لازم تعرفها
- المكسب: توفير وقت، history منظّم في الـ channel، شفافية للـ remote teams، صفر تكلفة بنية تحتية.
- الخسارة: غياب الـ face time. لو فريقك جديد ولسّه ما اتبنتش الثقة، الـ standup الكلامي بيكشف معلومات السلوك (نبرة الصوت، التردّد) واللي مش بتظهر في النص.
- قيد KV consistency: الـ KV عنده eventual consistency بزمن انتشار قد يصل لـ 60 ثانية بين الـ regions. ده مش مشكلة في حالتنا لأن الـ write والـ read بيحصلوا في أوقات منفصلة (الكتابة الصبح، القراءة بعد ساعة).
- قيد Cron timing: Cloudflare بيشغّل الـ Cron مع تأخير ممكن يوصل لدقيقتين أحياناً (مش real-time). ده مقبول لـ standup مش لـ alarming.
متى لا تستخدم هذه الطريقة
لا تستخدمها لو فريقك أقل من 4 أشخاص — الميتنج 5 دقائق وانتهى، البوت overhead. لا تستخدمها لو الفريق بيعمل pair programming بشكل يومي، الـ standup الكلامي بيكشف معلومات تنسيق مش بتنكتب في النص. ولا تستخدمها لو بتشتغل في صناعة منظّمة (بنوك، صحة)، لأن الـ Cloudflare KV مش معتمد لتخزين بيانات حساسة، استخدم بنية On-prem في الحالة دي.
الخطوة التالية
افتح dash.cloudflare.com دلوقتي، أنشئ Worker جديد، الصق الكود من فوق، حط SLACK_TOKEN في Secrets، فعّل Cron Triggers. لو الـ Worker مشتغلش، افتح Logs من نفس الـ dashboard، شوف الـ error message. خلال 30 دقيقة فريقك هيكون عنده standup أوتوماتيكي بيرسل أول DM بكره الصبح.
المصادر
- Cloudflare Workers Cron Triggers — developers.cloudflare.com/workers/configuration/cron-triggers
- Cloudflare KV Platform Limits — developers.cloudflare.com/kv/platform/limits
- Slack Web API: chat.postMessage — api.slack.com/methods/chat.postMessage
- Slack Events API — api.slack.com/apis/events-api
- Slack OAuth Scopes — api.slack.com/scopes