příklady / integrace

Slack standup

Připomínka standupu, kterou služba každý všední den pošle do Slack webhooku.

cronsecretsslack

Cron služba, která každé všední ráno pošle připomínku standupu do Slack Incoming Webhooku. V naplánovaném čase odešle krátkou zprávu se třemi standupovými otázkami; přes běžné HTTP zobrazí stavovou stránku, na které si ověříte, že je služba nastavená, aniž by kdy prozradila adresu webhooku.

Adresa webhooku je uložená jako secret SLACK_WEBHOOK_URL, který se do prostředí služby vloží až za běhu a nikdy se nezapisuje do kódu. Když Slack zprávu nepřijme, služba výsledek zaznamená a přesto v pořádku doběhne, takže špatný webhook nikdy neoznačí naplánovaný běh jako selhaný.

kód
main.ts
// A weekday standup reminder for Slack.
//
// On its cron schedule this service posts a reminder to a Slack Incoming Webhook.
// Over plain HTTP it serves a small status page so you can confirm it is wired
// up, without ever revealing the webhook URL.
//
// Configure the SLACK_WEBHOOK_URL secret with your Incoming Webhook:
// https://api.slack.com/messaging/webhooks

// Read an environment variable, treating an unset or unreadable one as
// undefined. Frontback scopes each service's env access to its declared secrets, so
// a name that was never configured simply reads as "not set".
function readEnv(name: string): string | undefined {
  try {
    return Deno.env.get(name);
  } catch {
    return undefined;
  }
}

// The three classic standup prompts, sent as one reminder message.
const QUESTIONS = [
  "What did you get done yesterday?",
  "What are you working on today?",
  "Anything blocking you?",
];

// Build the Slack message payload. Slack renders the `text` field, so we keep
// the reminder to a single readable block with Slack's markdown.
function buildMessage(): { text: string } {
  const lines = ["*Standup time* :coffee:", "", ...QUESTIONS.map((q) => "• " + q)];
  return { text: lines.join("\n") };
}

// Post the reminder to Slack. A Slack Incoming Webhook answers 200 with the
// body "ok" when it accepts the message; anything else means it was not
// delivered. This never throws, so a misconfigured webhook cannot fail the run.
async function postToSlack(webhookUrl: string): Promise<{ delivered: boolean; status: number }> {
  const res = await fetch(webhookUrl, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(buildMessage()),
  });
  const body = (await res.text()).trim();
  return { delivered: res.ok && body === "ok", status: res.status };
}

// The schedule is defined in the code; each firing calls the handler below with
// the "X-Fb-Trigger: cron" header. run() powers the Run button for a test post.
export const crons = { "standup": "45 8 * * 1-5" };

export async function run(): Promise<string> {
  const webhookUrl = readEnv("SLACK_WEBHOOK_URL");
  if (!webhookUrl) return "SLACK_WEBHOOK_URL is not set";
  const result = await postToSlack(webhookUrl);
  return result.delivered ? "reminder posted to Slack" : `Slack answered ${result.status}`;
}

export default async (req: Request): Promise<Response> => {
  const webhookUrl = readEnv("SLACK_WEBHOOK_URL");

  // Cron trigger: the platform posts to the service with this header on schedule.
  if (req.headers.get("x-fb-trigger") === "cron") {
    if (!webhookUrl) {
      // Report the misconfiguration but answer 200. A 5xx would mark the run as
      // failed in the dashboard; a missing secret is an expected state.
      return Response.json({ ok: false, delivered: false, reason: "SLACK_WEBHOOK_URL is not set" });
    }
    const result = await postToSlack(webhookUrl);
    return Response.json({
      ok: true,
      delivered: result.delivered,
      slackStatus: result.status,
      note: result.delivered ? "reminder posted to Slack" : "Slack did not accept the message",
    });
  }

  // Plain HTTP: a status page. It shows whether the webhook is configured, but
  // never the URL itself, so the page is safe to share.
  const configured = Boolean(webhookUrl);
  const preview = QUESTIONS.map((q) => "<li>" + q + "</li>").join("");
  const html = `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Slack standup</title>
  <style>
    body { font: 16px/1.5 system-ui, sans-serif; max-width: 40rem; margin: 3rem auto; padding: 0 1rem; }
    code { background: #f4f4f5; padding: 0.1rem 0.3rem; border-radius: 3px; }
  </style>
</head>
<body>
  <h1>Slack standup</h1>
  <p>Webhook: <strong>${configured ? "configured" : "not configured"}</strong></p>
  ${
    configured
      ? "<p>A standup reminder is posted to Slack every weekday at 08:45.</p>"
      : "<p>Set the <code>SLACK_WEBHOOK_URL</code> secret to enable the reminder.</p>"
  }
  <h2>Reminder preview</h2>
  <ul>${preview}</ul>
</body>
</html>`;
  return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
};

Go beyond what seems possible.