feat: polish Discord daily check-ins and add reminder buttons
This commit is contained in:
@@ -28,6 +28,7 @@ test("reminders follow local time, send only unfinished habits, and deduplicate
|
||||
await Promise.all([sendDueReminders(f.db, config, transport(sent), now), sendDueReminders(f.db, config, transport(sent), now)]);
|
||||
expect(sent).toHaveLength(1); expect(sent[0]).not.toContain("Private habit name");
|
||||
expect(JSON.parse(sent[0]!).allowed_mentions.parse).toEqual([]);
|
||||
expect(JSON.parse(sent[0]!).components).toBeUndefined();
|
||||
await sendDueReminders(f.db, config, transport(sent), now); expect(sent).toHaveLength(1);
|
||||
f.setTime("2026-09-05T18:00:00Z");
|
||||
await f.json(`/habits/${habit.id}/days/2026-09-05/progress`, "PUT", { done: true });
|
||||
@@ -53,7 +54,7 @@ test("quiet hours defer until their end and DST overlap sends once", async () =>
|
||||
test("opt-out during channel creation cancels the message", async () => {
|
||||
const { f } = await setup(); let posts = 0;
|
||||
try {
|
||||
await sendDueReminders(f.db, config, async url => {
|
||||
await sendDueReminders(f.db, { ...config, commandsAvailable: true }, async url => {
|
||||
if (!url.endsWith("/users/@me/channels")) posts++;
|
||||
f.db.update(reminderSettings).set({ enabled: false }).run();
|
||||
return Response.json({ id: "223456789012345678" });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { reminderCheckIn } from "../discord/daily";
|
||||
import { createHash } from "node:crypto";
|
||||
import { and, eq, inArray, lt } from "drizzle-orm";
|
||||
import type { AppDatabase } from "../auth";
|
||||
@@ -7,7 +8,7 @@ import { localDate } from "../habits/calendar";
|
||||
import { entriesForDate, inQuietHours, timesForDate } from "./contracts";
|
||||
import type { DiscordFetch } from "../sharing/routes";
|
||||
|
||||
export type ReminderConfig = { token: string; origin: string };
|
||||
export type ReminderConfig = { token: string; origin: string; commandsAvailable?: boolean };
|
||||
function localClock(timestamp: number, timezone: string) {
|
||||
const parts = new Intl.DateTimeFormat("en-GB", { timeZone: timezone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(timestamp);
|
||||
return `${parts.find(part => part.type === "hour")!.value}:${parts.find(part => part.type === "minute")!.value}`;
|
||||
@@ -34,7 +35,7 @@ export async function sendDueReminders(db: AppDatabase, config: ReminderConfig,
|
||||
// Avoid sending an artificial "today" after a backwards timezone change.
|
||||
if (service.today !== localDate(timestamp, currentUser.timezone)) return null;
|
||||
const remaining = service.list().filter(habit => { const day = service.day(habit.id, service.today); return day.due && !day.complete; }).length;
|
||||
return remaining ? { date: service.today, remaining, times, entries: entriesForDate(prefs, service.today) } : null;
|
||||
return remaining ? { date: service.today, remaining, timezone: currentUser.timezone, times, entries: entriesForDate(prefs, service.today) } : null;
|
||||
}
|
||||
const initial = due(); if (!initial) continue;
|
||||
const claims = db.transaction(tx => {
|
||||
@@ -84,6 +85,7 @@ export async function sendDueReminders(db: AppDatabase, config: ReminderConfig,
|
||||
try { response = await request(`https://discord.com/api/v10/channels/${destination.id}/messages`, {
|
||||
method: "POST", headers, redirect: "error", signal: AbortSignal.timeout(15000),
|
||||
body: JSON.stringify({ content: `${prefix}A little time for yourself: ${latest.remaining} ${latest.remaining === 1 ? "habit remains" : "habits remain"} today. Check in: ${config.origin}/`,
|
||||
...(config.commandsAvailable ? { components: reminderCheckIn(db, user.id, latest.timezone, now()) } : {}),
|
||||
allowed_mentions: { parse: [] }, nonce: createHash("sha256").update(ids.sort().join(":")).digest("hex").slice(0, 24), enforce_nonce: true }),
|
||||
}); } catch { update("uncertain"); continue; }
|
||||
if (response.status === 429) { await defer(response); return; }
|
||||
|
||||
Reference in New Issue
Block a user