Skip to content

Instantly share code, notes, and snippets.

@skve
Created September 19, 2025 16:20
Show Gist options
  • Select an option

  • Save skve/6a5cbf04d9c2d59f59524bf4bafb9cbb to your computer and use it in GitHub Desktop.

Select an option

Save skve/6a5cbf04d9c2d59f59524bf4bafb9cbb to your computer and use it in GitHub Desktop.
Motivational Slack Bot
import { log } from "@interfere/observability/log";
import { WebClient } from "@slack/web-api";
import { generateText } from "ai";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { Context, Effect, Layer, Schema } from "effect";
import { NextResponse } from "next/server";
import { env } from "@/env";
// Configure dayjs with timezone support
dayjs.extend(utc);
dayjs.extend(timezone);
export const runtime = "nodejs";
const PostResult = Schema.Struct({
ok: Schema.Boolean,
days: Schema.Number,
text: Schema.String,
});
type PostResult = Schema.Schema.Type<typeof PostResult>;
class SlackClient extends Context.Tag("services/SlackClient")<
SlackClient,
WebClient
>() {}
const SlackLayer = Layer.succeed(
SlackClient,
new WebClient(env.SLACK_BOT_TOKEN)
);
/**
* Calculate days between start date and now in a specific timezone
* Uses day.js for reliable timezone handling
*/
const computeDay = (now: Date, startISO: string, tz: string): number => {
// Parse the start date as a date in the target timezone
const startDate = dayjs.tz(startISO, tz).startOf("day");
// Get the current date in the target timezone
const currentDate = dayjs(now).tz(tz).startOf("day");
// Calculate the difference in days
const diffDays = currentDate.diff(startDate, "day");
// Add 1 because we count the first day as Day 1
return Math.max(1, diffDays + 1);
};
// Examples for inspiration (not preferred; model may create or adapt)
const INSPIRATION_QUOTES = [
'"Discipline equals freedom." - Jocko Willink',
'"The obstacle is the way." - Marcus Aurelius',
'"Less is more." - Ludwig Mies van der Rohe',
'"Form follows function." - Louis Sullivan',
'"Less, but better." - Dieter Rams',
'"The details are not the details. They make the design." - Charles Eames',
'"Above all else show the data." - Edward Tufte',
'"Simplicity is the ultimate sophistication." - Leonardo da Vinci',
'"Ship early, ship often." - Eric S. Raymond',
'"Done is better than perfect." - Sheryl Sandberg',
'"The best time to plant a tree was 20 years ago. The second best time is now." - Chinese Proverb',
'"Build something 100 people love, not something 1 million people kind of like." - Brian Chesky',
'"Ideas are worthless. Execution is everything." - Scott Adams',
'"If you\'re not embarrassed by v1, you launched too late." - Reid Hoffman',
'"The biggest risk is not taking any risk." - Mark Zuckerberg',
'"Your work is going to fill a large part of your life, do what you believe is great work." - Steve Jobs',
] as const;
const RANDOM_SORT_MIDPOINT = 0.5;
const EXAMPLE_QUOTES_SAMPLE_SIZE = 5;
const generateAIQuote = async (dayNumber: number): Promise<string | null> => {
// Randomly select example quotes for variety
const shuffled = [...INSPIRATION_QUOTES].sort(
() => Math.random() - RANDOM_SORT_MIDPOINT
);
const selectedExamples = shuffled.slice(0, EXAMPLE_QUOTES_SAMPLE_SIZE);
const list = selectedExamples.join("\n");
// Use day number as a seed for variety
const themes = [
"building",
"shipping",
"design",
"engineering",
"teamwork",
"innovation",
"quality",
"craftsmanship",
"persistence",
"focus",
"iteration",
"simplicity",
"excellence",
"growth",
"momentum",
];
const theme = themes[dayNumber % themes.length];
// Additional variety factors
const styleOptions = [
"bold and direct",
"philosophical and thoughtful",
"practical and actionable",
"minimalist and zen-like",
"energetic and motivating",
];
const style = styleOptions[Math.floor(Math.random() * styleOptions.length)];
const prompt = [
`Generate a unique, inspiring quote for Day ${dayNumber} of a startup journey.`,
`Theme focus: ${theme}`,
`Style: ${style}`,
"Requirements:",
'- Create something fresh and varied - avoid repetitive phrases about "progress" or "steps"',
"- Can be about engineering, design, startups, excellence, or general wisdom",
"- Keep it concise (under 100 chars)",
"- IMPORTANT: Attribute to a real person (tech leaders, designers, philosophers, etc.). Do not create an original saying.",
`- Match the "${style}" tone`,
"Examples for style reference (note the mix of attributed and non-attributed):",
list,
'"Make it happen." (no attribution)',
'"Quality is non-negotiable." (no attribution)',
"",
'Return ONLY the quote text. Include " - Name" ONLY if citing a real person.',
].join("\n");
const result = await generateText({
model: "openai/gpt-5-mini",
prompt,
temperature: 1,
});
const out = typeof result.text === "string" ? result.text.trim() : "";
if (!out) {
return null;
}
return out;
};
const program = Effect.gen(function* () {
const tz = "America/New_York";
const days = computeDay(new Date(), "2025-06-30", tz);
// Try AI-generated quote; if none, omit quote entirely
const maybeQuote = yield* Effect.tryPromise({
try: () => generateAIQuote(days),
catch: (e) => log.error("Failed to generate AI quote", e),
});
const text = maybeQuote
? `Today is *Day ${days}* of Interfere. Make it count.\n\n> ${maybeQuote}`
: `Today is *Day ${days}* of Interfere. Make it count.`;
const slack = yield* SlackClient;
const res = yield* Effect.tryPromise({
try: () =>
slack.chat.postMessage({
channel: env.SLACK_CHANNEL_ID,
text,
mrkdwn: true,
}),
catch: (e: unknown) => e,
});
if (!res.ok) {
return yield* Effect.fail(
new Error(typeof res.error === "string" ? res.error : "Slack API error")
);
}
const payload: PostResult = { ok: true, days, text };
return payload;
});
export async function GET(_request: Request) {
const authHeader = _request.headers.get("authorization");
if (authHeader !== `Bearer ${env.CRON_SECRET}`) {
return NextResponse.json(
{ ok: false, error: "Unauthorized" },
{ status: 401 }
);
}
const result = await Effect.runPromise(
Effect.provide(program, SlackLayer)
).then(
(ok) => ({ status: 200, body: ok }),
(err) => ({
status: 500,
body: {
ok: false,
error:
(err && (err.data || err.message)) ??
"Failed to post message to Slack",
},
})
);
return NextResponse.json(result.body, { status: result.status });
}
@skve
Copy link
Author

skve commented Sep 19, 2025

Fire this off every day with Vercel cron jobs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment