From f11073300aedbba0887850ac04a992b52a60b10a Mon Sep 17 00:00:00 2001
From: Jessica Ho
Date: Sun, 6 Aug 2023 08:23:07 +0000
Subject: [PATCH 1/6] sendNotif in lib/server
---
src/hooks.server.ts | 7 ++
src/lib/server/db.ts | 10 ++-
src/lib/server/twilio.ts | 66 +++++++++++++++-
src/routes/circle/+page.svelte | 2 +-
src/routes/household/+page.svelte | 6 +-
.../household/[householdId]/+page.svelte | 4 +-
src/routes/invites/+page.svelte | 6 +-
src/routes/reminder/+server.ts | 77 -------------------
src/routes/twilio/+server.ts | 3 +-
9 files changed, 88 insertions(+), 93 deletions(-)
delete mode 100644 src/routes/reminder/+server.ts
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 5cb8b3d..0a2aa14 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -1,11 +1,18 @@
import type { Handle, RequestEvent } from '@sveltejs/kit';
import { PrismaClient } from '@prisma/client';
import type { User, PhoneContactPermissions } from '@prisma/client';
+import * as cron from 'node-cron';
+import { sendNotif } from '$lib/server/twilio';
+
const prisma = new PrismaClient();
import { redirect } from '@sveltejs/kit';
import type { MaybePromise, ResolveOptions } from '@sveltejs/kit/types/internal';
+cron.schedule('*/1 * * * *', function () {
+ sendNotif();
+});
+
const setLocal = async (
user: (User & { phonePermissions: PhoneContactPermissions }) | null,
phone: string,
diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts
index 2fe1945..258cc7b 100644
--- a/src/lib/server/db.ts
+++ b/src/lib/server/db.ts
@@ -423,10 +423,14 @@ async function saveUser(
allowInvites,
allowReminders
} = req;
+ // Get the current date in the user's timezone
+ const userLocalDate = new Date().toLocaleString('en-US', { timeZone });
- const d = new Date();
- const day = d.getDay();
- const diff = d.getDate() - day + notifStartDay;
+ // Convert the user's date to a JavaScript Date object
+ const d = new Date(userLocalDate);
+
+ // Calculate the desired date based on the user's timezone
+ const diff = d.getDate() - d.getDay() + notifStartDay;
d.setDate(diff);
d.setHours(notifHr);
d.setMinutes(notifMin);
diff --git a/src/lib/server/twilio.ts b/src/lib/server/twilio.ts
index 941b702..117a406 100644
--- a/src/lib/server/twilio.ts
+++ b/src/lib/server/twilio.ts
@@ -94,12 +94,24 @@ const msgToSend = async (
msg = `Thanks for subscribing to reminders and friend availability notifications from ${url}! You can disable this at any time on your Profile page or by responding STOP.`;
break;
}
+ case 'reminder': {
+ const { phone } = msgComps;
+ msg = `Hi! It's your periodic reminder to update your schedule: https://playdate.help/login/${phone}`;
+ break;
+ }
+ default:
+ throw error(400, {
+ message: `Message type ${type} not supported`
+ });
}
return msg;
};
-export const sendMsg = async (request: Request, initiator: User | null) => {
- const { phone, sendAt, type, ...rest } = await request.json();
+export const sendMsg = async (
+ request: { phone: string; sendAt?: Date; type: string },
+ initiator: User | null
+) => {
+ const { phone, sendAt, type, ...rest } = request;
if (!phone || !type) {
throw error(400, {
message: `Missing a ${!phone ? 'phone number' : 'type of message to send'}`
@@ -229,3 +241,53 @@ export const getMsg = async (url: URL) => {
return response;
};
+
+export async function sendNotif() {
+ const nowLocal = new Date();
+ const users = await prisma.user.findMany({
+ select: {
+ id: true,
+ phone: true,
+ reminderDatetime: true,
+ reminderIntervalDays: true,
+ timeZone: true,
+ phonePermissions: {
+ select: {
+ allowReminders: true,
+ blocked: true
+ }
+ }
+ }
+ });
+ users.forEach(async (user) => {
+ const { id, phone, reminderDatetime, reminderIntervalDays, phonePermissions, timeZone } = user;
+ const { allowReminders, blocked } = phonePermissions;
+ if (!allowReminders || blocked) return;
+
+ const options = {
+ timeZone
+ };
+
+ const formattedDate = nowLocal.toLocaleString('en-US', options);
+ const now = new Date(formattedDate);
+ const timeDifference = Math.abs(now.getTime() - reminderDatetime.getTime()); // Get the absolute time difference in milliseconds
+ const minuteInMillis = 60 * 1000; // 1 minute in milliseconds
+ if (timeDifference < minuteInMillis) {
+ // currently within a minute of when user should be reminded
+ // send notif
+ await sendMsg({ phone, type: 'reminder' }, null);
+
+ // update reminder date for next notif
+ const newReminderDate = new Date(reminderDatetime);
+ newReminderDate.setDate(reminderDatetime.getDate() + reminderIntervalDays);
+ await prisma.user.update({
+ where: {
+ id
+ },
+ data: {
+ reminderDatetime: newReminderDate
+ }
+ });
+ }
+ });
+}
diff --git a/src/routes/circle/+page.svelte b/src/routes/circle/+page.svelte
index 4dcb2ee..160fa6d 100644
--- a/src/routes/circle/+page.svelte
+++ b/src/routes/circle/+page.svelte
@@ -104,7 +104,7 @@
: 'line-through'}"
>
{parent.firstName}
- {parent.lastName}
+ {parent.lastName ?? ''}
{/each}
diff --git a/src/routes/household/+page.svelte b/src/routes/household/+page.svelte
index c3147df..75717d0 100644
--- a/src/routes/household/+page.svelte
+++ b/src/routes/household/+page.svelte
@@ -308,7 +308,7 @@
{householdInvites[0].fromUser.firstName}
- {householdInvites[0].fromUser.lastName}
+ {householdInvites[0].fromUser.lastName ?? ''}
-
{adult.firstName} {adult.lastName}
+
{adult.firstName} {adult.lastName ?? ''}
Pronouns: {PRONOUNS[adult.pronouns]}
Phone: {adult.phone}
diff --git a/src/routes/invites/+page.svelte b/src/routes/invites/+page.svelte
index 8af252d..b98729d 100644
--- a/src/routes/invites/+page.svelte
+++ b/src/routes/invites/+page.svelte
@@ -80,7 +80,7 @@
{household.name}
{#each household.parents as parent}
-
{parent.firstName} {parent.lastName}
+
{parent.firstName} {parent.lastName ?? ''}
{/each}
{household.phone}
@@ -132,12 +132,12 @@
{invite.household.name}
{#each invite.household.parents as parent}
-
{parent.firstName} {parent.lastName}: {parent.phone}
+
{parent.firstName} {parent.lastName ?? ''}: {parent.phone}
{/each}
{#each invite.household.children as child}
-
{child.firstName} {child.lastName}
+
{child.firstName} {child.lastName ?? ''}
{/each}
diff --git a/src/routes/reminder/+server.ts b/src/routes/reminder/+server.ts
deleted file mode 100644
index 475da70..0000000
--- a/src/routes/reminder/+server.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { PrismaClient } from '@prisma/client';
-import { json } from '@sveltejs/kit';
-import * as cron from 'node-cron';
-
-const prisma = new PrismaClient();
-
-export function POST({ fetch }: { fetch: any }) {
- /**
- goes through each user in the db
- If it's time to send them a notif, we'll just do so right there. No need to schedule it.
- */
- async function sendNotif() {
- const nowLocal = new Date();
- const users = await prisma.user.findMany({
- select: {
- id: true,
- phone: true,
- reminderDatetime: true,
- reminderIntervalDays: true,
- timeZone: true,
- phonePermissions: {
- select: {
- allowReminders: true,
- blocked: true
- }
- }
- }
- });
- users.forEach(async (user) => {
- const { id, phone, reminderDatetime, reminderIntervalDays, phonePermissions, timeZone } =
- user;
- const { allowReminders, blocked } = phonePermissions;
- if (!allowReminders || blocked) return;
-
- const options = {
- timeZone
- };
-
- const formattedDate = nowLocal.toLocaleString('en-US', options);
- const now = new Date(formattedDate);
- const timeDifference = Math.abs(now.getTime() - reminderDatetime.getTime()); // Get the absolute time difference in milliseconds
- const minuteInMillis = 60 * 1000; // 1 minute in milliseconds
- if (timeDifference < minuteInMillis) {
- // currently within a minute of when user should be reminded
- // send notif
- await fetch('/twilio', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- msg: `Hi! It's your periodic reminder to update your schedule: https://playdate.help/login/${phone}`,
- phone
- })
- });
-
- // update reminder date for next notif
- const newReminderDate = new Date(reminderDatetime);
- newReminderDate.setDate(reminderDatetime.getDate() + reminderIntervalDays);
- await prisma.user.update({
- where: {
- id
- },
- data: {
- reminderDatetime: newReminderDate
- }
- });
- }
- });
- }
-
- cron.schedule('*/1 * * * *', function () {
- sendNotif();
- });
-
- return json('ok');
-}
diff --git a/src/routes/twilio/+server.ts b/src/routes/twilio/+server.ts
index a89dbc2..3850525 100644
--- a/src/routes/twilio/+server.ts
+++ b/src/routes/twilio/+server.ts
@@ -10,8 +10,7 @@ export async function POST({
}) {
const sessionToken = cookies.get('session');
const { user } = await getProfileFromSession(sessionToken);
-
- return sendMsg(request, user);
+ return sendMsg(await request.json(), user);
}
export function GET({ url }: { url: URL }) {
From c0428d0256f11674ca87c04e40a8ae442764e84e Mon Sep 17 00:00:00 2001
From: Jessica Ho
Date: Sun, 6 Aug 2023 08:28:37 +0000
Subject: [PATCH 2/6] GHA fixes
---
.github/workflows/test.yml | 4 ++++
package.json | 4 ++--
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 91bcd69..446cdff 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,6 +4,7 @@ on: [push, pull_request]
env:
DATABASE_PRISMA_URL: postgres://postgres:postgres@localhost:5432/test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
+ PUBLIC_URL: http://localhost:5173
jobs:
build:
runs-on: ubuntu-latest
@@ -42,6 +43,9 @@ jobs:
- name: Build in node mode
run: yarn nodebuild
+ - name: Install playwright
+ run: npx playwright install
+
- name: Install Playwright dependencies (just chromium for now)
run: npx playwright install-deps chromium
diff --git a/package.json b/package.json
index e94d958..38d5d3e 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,8 @@
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
- "reset": "prisma migrate reset && yarn dev",
- "postinstall": "prisma generate",
+ "reset": "prisma migrate reset && yarn dev",
+ "postinstall": "prisma migrate dev",
"build:prev": "vite build && vite preview",
"seed": "prisma db seed"
},
From b4eef1dd18d486a6c6a014b3b4d89f4f3b6abe30 Mon Sep 17 00:00:00 2001
From: Jessica Ho
Date: Mon, 7 Aug 2023 07:38:36 +0000
Subject: [PATCH 3/6] handle expired link better
---
.github/workflows/test.yml | 45 +++++++++----------
prisma/seed.ts | 13 ++++++
.../login/[phone]/[token]/+page.server.ts | 24 +++-------
tests/login.spec.ts | 17 +++++++
4 files changed, 57 insertions(+), 42 deletions(-)
create mode 100644 tests/login.spec.ts
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6158937..f1c4edc 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -31,26 +31,25 @@ jobs:
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- - name: Checkout repository
- uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v3
- with:
- node-version: ${{ matrix.node-version }}
- cache: 'yarn'
-
- - name: Install dependencies
- run: yarn install --frozen-lockfile
-
- - name: Run postinstall (db stuff)
- run: yarn postinstall
-
- - name: Build in node mode
- run: yarn nodebuild
-
- - name: Install playwright & dependencies
- run: yarn exec playwright install --with-deps
-
- - name: Run tests
- run: yarn test
-
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'yarn'
+
+ - name: Install dependencies
+ run: yarn install --frozen-lockfile
+
+ - name: Run postinstall (db stuff)
+ run: yarn postinstall
+
+ - name: Build in node mode
+ run: yarn nodebuild
+
+ - name: Install playwright & dependencies
+ run: yarn exec playwright install --with-deps
+
+ - name: Run tests
+ run: yarn test
diff --git a/prisma/seed.ts b/prisma/seed.ts
index cb5c436..440e866 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -94,6 +94,19 @@ async function main() {
.deleteMany()
.catch(() => console.log('No friend request table to delete'));
+ const expiredLink = {
+ token: '3e99472f1003794c',
+ phone: '+12015550121',
+ expires: new Date('8/5/2020')
+ };
+ await prisma.magicLink.upsert({
+ where: {
+ id: 1
+ },
+ update: expiredLink,
+ create: expiredLink
+ });
+
// User 1
await prisma.user.upsert({
where: {
diff --git a/src/routes/login/[phone]/[token]/+page.server.ts b/src/routes/login/[phone]/[token]/+page.server.ts
index 7268c3d..1db3bdf 100644
--- a/src/routes/login/[phone]/[token]/+page.server.ts
+++ b/src/routes/login/[phone]/[token]/+page.server.ts
@@ -14,28 +14,16 @@ export const load = (async ({ params, cookies }) => {
}
});
} catch {
- return new Response(
- JSON.stringify({
- message: "Can't verify token"
- }),
- {
- status: 403
- }
- );
+ console.error("Can't verify token");
+ throw redirect(308, `/?phone=${params.phone}`);
}
// check DB's expiration date
const { phone, expires } = magicLinkInfo as { phone: string; expires: Date };
if (expires < new Date()) {
- return new Response(
- JSON.stringify({
- message: 'Token has expired'
- }),
- {
- status: 403
- }
- );
+ console.error('Token has expired');
+ throw redirect(308, `/?phone=${params.phone}`);
}
let crypto;
@@ -43,9 +31,7 @@ export const load = (async ({ params, cookies }) => {
crypto = await import('node:crypto');
} catch (err) {
console.error('crypto support is disabled!');
- return {
- token: null
- };
+ throw redirect(308, `/?phone=${params.phone}`);
}
const sessionCreatedAt = new Date();
diff --git a/tests/login.spec.ts b/tests/login.spec.ts
new file mode 100644
index 0000000..0146306
--- /dev/null
+++ b/tests/login.spec.ts
@@ -0,0 +1,17 @@
+import { test, expect } from '@playwright/test';
+import { run } from '../prisma/seed';
+
+const host = 'http://localhost:5173';
+
+test.beforeEach(async () => {
+ await run();
+});
+
+test.only('Redirect to login page w/ prefilled phone num on expired magic link', async ({
+ page
+}) => {
+ await page.goto('http://localhost:5173/login/12015550121/3e99472f1003794c');
+
+ await page.waitForURL(`${host}?phone=12015550121`, { waitUntil: 'networkidle' });
+ await expect(page).toHaveURL(`${host}?phone=12015550121`);
+});
From a0cea2259db165412e96d1c7a1a63cac2784d8ad Mon Sep 17 00:00:00 2001
From: Jessica Ho
Date: Mon, 7 Aug 2023 07:51:30 +0000
Subject: [PATCH 4/6] reminder magiclink
---
src/lib/server/login.ts | 36 ++++++++++++++++++++++++++++++++++++
src/lib/server/twilio.ts | 24 +++++++++++++++++++++++-
src/routes/login/+server.ts | 34 +---------------------------------
3 files changed, 60 insertions(+), 34 deletions(-)
create mode 100644 src/lib/server/login.ts
diff --git a/src/lib/server/login.ts b/src/lib/server/login.ts
new file mode 100644
index 0000000..bdc502f
--- /dev/null
+++ b/src/lib/server/login.ts
@@ -0,0 +1,36 @@
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+export const generate = async () => {
+ const createdAt = new Date();
+ const expires = new Date();
+ expires.setHours(createdAt.getHours() + 1);
+
+ let crypto;
+ try {
+ crypto = await import('node:crypto');
+ } catch (err) {
+ console.error('crypto support is disabled!');
+ return {
+ token: null
+ };
+ }
+ const token = crypto.randomBytes(8).toString('hex');
+ return {
+ token,
+ createdAt,
+ expires
+ };
+};
+
+export async function save(token: string, phone: string, createdAt: Date, expires: Date) {
+ await prisma.magicLink.create({
+ data: {
+ token,
+ phone,
+ expires,
+ createdAt
+ }
+ });
+}
diff --git a/src/lib/server/twilio.ts b/src/lib/server/twilio.ts
index 117a406..70492a6 100644
--- a/src/lib/server/twilio.ts
+++ b/src/lib/server/twilio.ts
@@ -4,6 +4,7 @@ import { error, json } from '@sveltejs/kit';
import Twilio from 'twilio';
import { PrismaClient, type User } from '@prisma/client';
import { circleNotif } from './sanitize';
+import { generate, save } from './login';
const prisma = new PrismaClient();
const MessagingResponse = Twilio.twiml.MessagingResponse;
@@ -96,7 +97,27 @@ const msgToSend = async (
}
case 'reminder': {
const { phone } = msgComps;
- msg = `Hi! It's your periodic reminder to update your schedule: https://playdate.help/login/${phone}`;
+ const { token, createdAt, expires } = await generate();
+
+ if (!token) {
+ console.error('token generation failed');
+ throw error(500, {
+ message: 'Token generation failed'
+ });
+ }
+
+ // save these attrs to DB
+ save(token, phone, createdAt, expires)
+ .then(async () => {
+ await prisma.$disconnect();
+ })
+ .catch(async (e) => {
+ console.error(e);
+ await prisma.$disconnect();
+ process.exit(1);
+ });
+
+ msg = `Hi! It's your periodic reminder to update your schedule: ${url}/login/${phone}/${token}`;
break;
}
default:
@@ -272,6 +293,7 @@ export async function sendNotif() {
const now = new Date(formattedDate);
const timeDifference = Math.abs(now.getTime() - reminderDatetime.getTime()); // Get the absolute time difference in milliseconds
const minuteInMillis = 60 * 1000; // 1 minute in milliseconds
+ console.log(user.phone, now, reminderDatetime);
if (timeDifference < minuteInMillis) {
// currently within a minute of when user should be reminded
// send notif
diff --git a/src/routes/login/+server.ts b/src/routes/login/+server.ts
index a998c83..bf3cf0a 100644
--- a/src/routes/login/+server.ts
+++ b/src/routes/login/+server.ts
@@ -1,42 +1,10 @@
+import { generate, save } from '$lib/server/login';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error']
});
-const generate = async () => {
- const createdAt = new Date();
- const expires = new Date();
- expires.setHours(createdAt.getHours() + 1);
-
- let crypto;
- try {
- crypto = await import('node:crypto');
- } catch (err) {
- console.error('crypto support is disabled!');
- return {
- token: null
- };
- }
- const token = crypto.randomBytes(8).toString('hex');
- return {
- token,
- createdAt,
- expires
- };
-};
-
-async function save(token: string, phone: string, createdAt: Date, expires: Date) {
- await prisma.magicLink.create({
- data: {
- token,
- phone,
- expires,
- createdAt
- }
- });
-}
-
export async function POST({ request }: { request: Request }) {
const { phone } = await request.json();
if (!phone) {
From 68ff0932612ca3e3da3f25559a63c5a4e935b14e Mon Sep 17 00:00:00 2001
From: Jessica Ho
Date: Mon, 7 Aug 2023 09:01:52 +0000
Subject: [PATCH 5/6] send on past reminderDates too
---
src/lib/server/twilio.ts | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/src/lib/server/twilio.ts b/src/lib/server/twilio.ts
index 70492a6..5ee39af 100644
--- a/src/lib/server/twilio.ts
+++ b/src/lib/server/twilio.ts
@@ -291,9 +291,34 @@ export async function sendNotif() {
const formattedDate = nowLocal.toLocaleString('en-US', options);
const now = new Date(formattedDate);
+
+ // It would be better to send the notifications late than never.
+ if (reminderDatetime < now) {
+ const sameDay = new Date(now);
+ sameDay.setDate(reminderDatetime.getDate());
+
+ const diff = Math.abs(sameDay.getTime() - reminderDatetime.getTime()) / (1000 * 60);
+
+ if (diff <= 30) {
+ await sendMsg({ phone, type: 'reminder' }, null);
+
+ // update reminder date for next notif -- x days from today
+ const newReminderDate = new Date(now);
+ newReminderDate.setDate(newReminderDate.getDate() + reminderIntervalDays);
+ await prisma.user.update({
+ where: {
+ id
+ },
+ data: {
+ reminderDatetime: newReminderDate
+ }
+ });
+ }
+ return;
+ }
+
const timeDifference = Math.abs(now.getTime() - reminderDatetime.getTime()); // Get the absolute time difference in milliseconds
const minuteInMillis = 60 * 1000; // 1 minute in milliseconds
- console.log(user.phone, now, reminderDatetime);
if (timeDifference < minuteInMillis) {
// currently within a minute of when user should be reminded
// send notif
From 22b6c1399aa64ddbc603d5e91a5f4074ccdb1e29 Mon Sep 17 00:00:00 2001
From: Jessica Ho
Date: Mon, 7 Aug 2023 20:40:36 +0000
Subject: [PATCH 6/6] handle overlapping cron jobs
---
src/lib/server/twilio.ts | 41 +++++++++++++++++++++++++++++++++++++---
1 file changed, 38 insertions(+), 3 deletions(-)
diff --git a/src/lib/server/twilio.ts b/src/lib/server/twilio.ts
index 5ee39af..494aad7 100644
--- a/src/lib/server/twilio.ts
+++ b/src/lib/server/twilio.ts
@@ -117,7 +117,9 @@ const msgToSend = async (
process.exit(1);
});
- msg = `Hi! It's your periodic reminder to update your schedule: ${url}/login/${phone}/${token}`;
+ msg = `Hi! It's your periodic reminder to update your schedule: ${url}/login/${phone.slice(
+ 1
+ )}/${token}`;
break;
}
default:
@@ -263,13 +265,19 @@ export const getMsg = async (url: URL) => {
return response;
};
+function shuffleArr(arr: any[]) {
+ for (let i = arr.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+}
+
export async function sendNotif() {
const nowLocal = new Date();
const users = await prisma.user.findMany({
select: {
id: true,
phone: true,
- reminderDatetime: true,
reminderIntervalDays: true,
timeZone: true,
phonePermissions: {
@@ -280,8 +288,12 @@ export async function sendNotif() {
}
}
});
+
+ // randomize the order of 'users'
+ shuffleArr(users);
+
users.forEach(async (user) => {
- const { id, phone, reminderDatetime, reminderIntervalDays, phonePermissions, timeZone } = user;
+ const { id, phone, reminderIntervalDays, phonePermissions, timeZone } = user;
const { allowReminders, blocked } = phonePermissions;
if (!allowReminders || blocked) return;
@@ -292,6 +304,29 @@ export async function sendNotif() {
const formattedDate = nowLocal.toLocaleString('en-US', options);
const now = new Date(formattedDate);
+ // sleep for a random amount of time between 1 and 2 seconds prior to each message.
+ const min = 1000;
+ const max = 2000;
+ await new Promise((r) => setTimeout(r, Math.floor(Math.random() * (max - min + 1)) + min));
+
+ // Re-query the database for the reminderDateTime before the time comparison logic.
+ // Using the cached data from the initial query greatly increases the odds of duplicate messages.
+ const userRequery = await prisma.user.findUnique({
+ where: {
+ phone
+ },
+ select: {
+ reminderDatetime: true
+ }
+ });
+
+ if (!userRequery)
+ throw error(500, {
+ message: `Couldn't requery user with phone ${phone}`
+ });
+
+ const { reminderDatetime } = userRequery;
+
// It would be better to send the notifications late than never.
if (reminderDatetime < now) {
const sameDay = new Date(now);