@remindme bot support (#1159)

* @remindme bot support

support reminders via @remindme bot, just like @delete bot

* minor cleanup

* minor query cleanup

* add db migration

* various fixes and updates:

* hasNewNotes implementation
* actually return notification component in ui
* delete reminder and job on item delete
* other goodies

* refactor to use prisma for deleting existing reminder

* * switch to deleteMany to delete existing Reminders upon edit/delete of post to satisfy prisma

* update wording in form toast for remindme bot usage

* update wording in the push notification sent

* transactional reminder inserts and expirein

* set expirein on @delete too

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
SatsAllDay 2024-05-19 16:52:02 -04:00 committed by GitHub
parent b7353ddd69
commit 852d2cf304
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 280 additions and 24 deletions

View File

@ -16,7 +16,7 @@ import { parse } from 'tldts'
import uu from 'url-unshort'
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '@/lib/validate'
import { notifyItemParents, notifyUserSubscribers, notifyZapped, notifyTerritorySubscribers, notifyMention } from '@/lib/webPush'
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '@/lib/item'
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand, getReminderCommand, hasReminderCommand } from '@/lib/item'
import { datePivot, whenRange } from '@/lib/time'
import { imageFeesInfo, uploadIdsFromText } from './image'
import assertGofacYourself from './ofac'
@ -754,6 +754,10 @@ export default {
if (old.bio) {
throw new GraphQLError('cannot delete bio', { extensions: { code: 'BAD_INPUT' } })
}
// clean up any pending reminders, if triggered on this item and haven't been executed
if (hasReminderCommand(old.text)) {
await deleteReminderAndJob({ me, item: old, models })
}
return await deleteItemByAuthor({ models, id, item: old })
},
@ -1190,6 +1194,16 @@ export default {
}
const deleteJobs = await models.$queryRawUnsafe(`SELECT startafter FROM pgboss.job WHERE name = 'deleteItem' AND data->>'id' = '${item.id}'`)
return deleteJobs[0]?.startafter ?? null
},
reminderScheduledAt: async (item, args, { me, models }) => {
const meId = me?.id ?? ANON_USER_ID
if (meId !== item.userId || meId === ANON_USER_ID) {
// don't show reminders on an item if it isn't yours
// don't support reminders for ANON
return null
}
const reminderJobs = await models.$queryRawUnsafe(`SELECT startafter FROM pgboss.job WHERE name = 'reminder' AND data->>'itemId' = '${item.id}' AND data->>'userId' = '${meId}'`)
return reminderJobs[0]?.startafter ?? null
}
}
}
@ -1302,6 +1316,12 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
}
await enqueueDeletionJob(item, models)
if (hasReminderCommand(old.text)) {
// delete any reminder jobs that were created from a prior version of the item
await deleteReminderAndJob({ me, item, models })
}
await createReminderAndJob({ me, item, models })
item.comments = []
return item
}
@ -1346,6 +1366,8 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
await enqueueDeletionJob(item, models)
await createReminderAndJob({ me, item, models })
notifyUserSubscribers({ models, item })
notifyTerritorySubscribers({ models, item })
@ -1362,8 +1384,56 @@ const enqueueDeletionJob = async (item, models) => {
const deleteCommand = getDeleteCommand(item.text)
if (deleteCommand) {
await models.$queryRawUnsafe(`
INSERT INTO pgboss.job (name, data, startafter)
VALUES ('deleteItem', jsonb_build_object('id', ${item.id}), now() + interval '${deleteCommand.number} ${deleteCommand.unit}s');`)
INSERT INTO pgboss.job (name, data, startafter, expirein)
VALUES (
'deleteItem',
jsonb_build_object('id', ${item.id}),
now() + interval '${deleteCommand.number} ${deleteCommand.unit}s',
interval '${deleteCommand.number} ${deleteCommand.unit}s' + interval '1 minute')`)
}
}
const deleteReminderAndJob = async ({ me, item, models }) => {
if (me?.id && me.id !== ANON_USER_ID) {
await models.$transaction([
models.$queryRawUnsafe(`
DELETE FROM pgboss.job
WHERE name = 'reminder'
AND data->>'itemId' = '${item.id}'
AND data->>'userId' = '${me.id}'
AND state <> 'completed'`),
models.reminder.deleteMany({
where: {
itemId: Number(item.id),
userId: Number(me.id),
remindAt: {
gt: new Date()
}
}
})])
}
}
const createReminderAndJob = async ({ me, item, models }) => {
// disallow anon to use reminder
if (!me || me.id === ANON_USER_ID) {
return
}
const reminderCommand = getReminderCommand(item.text)
if (reminderCommand) {
await models.$transaction([
models.$queryRawUnsafe(`
INSERT INTO pgboss.job (name, data, startafter, expirein)
VALUES (
'reminder',
jsonb_build_object('itemId', ${item.id}, 'userId', ${me.id}),
now() + interval '${reminderCommand.number} ${reminderCommand.unit}s',
interval '${reminderCommand.number} ${reminderCommand.unit}s' + interval '1 minute')`),
// use a raw query instead of the model to reuse the built-in `now + interval` support instead of doing it via JS
models.$queryRawUnsafe(`
INSERT INTO "Reminder" ("userId", "itemId", "remindAt")
VALUES (${me.id}, ${item.id}, now() + interval '${reminderCommand.number} ${reminderCommand.unit}s')`)
])
}
}

View File

@ -304,6 +304,15 @@ export default {
LIMIT ${LIMIT})`
)
queries.push(
`(SELECT "Reminder".id::text, "Reminder"."remindAt" AS "sortTime", NULL as "earnedSats", 'Reminder' AS type
FROM "Reminder"
WHERE "Reminder"."userId" = $1
AND "Reminder"."remindAt" < $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
const notifications = await models.$queryRawUnsafe(
`SELECT id, "sortTime", "earnedSats", type,
"sortTime" AS "minSortTime"
@ -381,6 +390,12 @@ export default {
TerritoryPost: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
Reminder: {
item: async (n, args, { models, me }) => {
const { itemId } = await models.reminder.findUnique({ where: { id: Number(n.id) } })
return await getItem(n, { id: itemId }, { models, me })
}
},
TerritoryTransfer: {
sub: async (n, args, { models, me }) => {
const transfer = await models.territoryTransfer.findUnique({ where: { id: Number(n.id) }, include: { sub: true } })

View File

@ -473,6 +473,20 @@ export default {
return true
}
const newReminder = await models.reminder.findFirst({
where: {
userId: me.id,
remindAt: {
gt: lastChecked,
lt: new Date()
}
}
})
if (newReminder) {
foundNotes()
return true
}
// update checkedNotesAt to prevent rechecking same time period
models.user.update({
where: { id: me.id },

View File

@ -72,6 +72,7 @@ export default gql`
updatedAt: Date!
deletedAt: Date
deleteScheduledAt: Date
reminderScheduledAt: Date
title: String
searchTitle: String
url: String

View File

@ -121,10 +121,16 @@ export default gql`
sortTime: Date!
}
type Reminder {
id: ID!
item: Item!
sortTime: Date!
}
union Notification = Reply | Votification | Mention
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
| TerritoryPost | TerritoryTransfer
| TerritoryPost | TerritoryTransfer | Reminder
type Notifications {
lastChecked: Date

View File

@ -8,7 +8,7 @@ import useCrossposter from './use-crossposter'
import { bountySchema } from '@/lib/validate'
import { SubSelectInitial } from './sub-select'
import { useCallback } from 'react'
import { normalizeForwards, toastDeleteScheduled } from '@/lib/form'
import { normalizeForwards, toastUpsertSuccessMessages } from '@/lib/form'
import { MAX_TITLE_LENGTH } from '@/lib/constants'
import { useMe } from './me'
import { useToast } from './toast'
@ -56,6 +56,7 @@ export function BountyForm ({
) {
id
deleteScheduledAt
reminderScheduledAt
}
}
`
@ -89,7 +90,7 @@ export function BountyForm ({
const prefix = sub?.name ? `/~${sub.name}` : ''
await router.push(prefix + '/recent')
}
toastDeleteScheduled(toaster, data, 'upsertBounty', !!item, values.text)
toastUpsertSuccessMessages(toaster, data, 'upsertBounty', !!item, values.text)
}, [upsertBounty, router]
)

View File

@ -3,7 +3,7 @@ import { gql, useMutation } from '@apollo/client'
import styles from './reply.module.css'
import { commentSchema } from '@/lib/validate'
import { useToast } from './toast'
import { toastDeleteScheduled } from '@/lib/form'
import { toastUpsertSuccessMessages } from '@/lib/form'
import { FeeButtonProvider } from './fee-button'
import { ItemButtonBar } from './post'
@ -15,6 +15,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
upsertComment(id: $id, text: $text) {
text
deleteScheduledAt
reminderScheduledAt
}
}`, {
update (cache, { data: { upsertComment } }) {
@ -43,7 +44,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
if (error) {
throw new Error({ message: error.toString() })
}
toastDeleteScheduled(toaster, data, 'upsertComment', true, values.text)
toastUpsertSuccessMessages(toaster, data, 'upsertComment', true, values.text)
if (onSuccess) {
onSuccess()
}

View File

@ -9,7 +9,7 @@ import Item from './item'
import { discussionSchema } from '@/lib/validate'
import { SubSelectInitial } from './sub-select'
import { useCallback } from 'react'
import { normalizeForwards, toastDeleteScheduled } from '@/lib/form'
import { normalizeForwards, toastUpsertSuccessMessages } from '@/lib/form'
import { MAX_TITLE_LENGTH } from '@/lib/constants'
import { useMe } from './me'
import useCrossposter from './use-crossposter'
@ -37,6 +37,7 @@ export function DiscussionForm ({
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, hash: $hash, hmac: $hmac) {
id
deleteScheduledAt
reminderScheduledAt
}
}`
)
@ -69,7 +70,7 @@ export function DiscussionForm ({
const prefix = sub?.name ? `/~${sub.name}` : ''
await router.push(prefix + '/recent')
}
toastDeleteScheduled(toaster, data, 'upsertDiscussion', !!item, values.text)
toastUpsertSuccessMessages(toaster, data, 'upsertDiscussion', !!item, values.text)
}, [upsertDiscussion, router, item, sub, crossposter]
)

View File

@ -17,7 +17,7 @@ import Avatar from './avatar'
import { jobSchema } from '@/lib/validate'
import { MAX_TITLE_LENGTH, MEDIA_URL } from '@/lib/constants'
import { useToast } from './toast'
import { toastDeleteScheduled } from '@/lib/form'
import { toastUpsertSuccessMessages } from '@/lib/form'
import { ItemButtonBar } from './post'
import { useFormikContext } from 'formik'
@ -51,6 +51,7 @@ export default function JobForm ({ item, sub }) {
url: $url, maxBid: $maxBid, status: $status, logo: $logo, hash: $hash, hmac: $hmac) {
id
deleteScheduledAt
reminderScheduledAt
}
}`
)
@ -83,7 +84,7 @@ export default function JobForm ({ item, sub }) {
} else {
await router.push(`/~${sub.name}/recent`)
}
toastDeleteScheduled(toaster, data, 'upsertJob', !!item, values.text)
toastUpsertSuccessMessages(toaster, data, 'upsertJob', !!item, values.text)
}, [upsertJob, router, logoId]
)

View File

@ -9,7 +9,7 @@ import Item from './item'
import AccordianItem from './accordian-item'
import { linkSchema } from '@/lib/validate'
import Moon from '@/svgs/moon-fill.svg'
import { normalizeForwards, toastDeleteScheduled } from '@/lib/form'
import { normalizeForwards, toastUpsertSuccessMessages } from '@/lib/form'
import { useToast } from './toast'
import { SubSelectInitial } from './sub-select'
import { MAX_TITLE_LENGTH } from '@/lib/constants'
@ -76,6 +76,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
upsertLink(sub: $sub, id: $id, title: $title, url: $url, text: $text, boost: $boost, forward: $forward, hash: $hash, hmac: $hmac) {
id
deleteScheduledAt
reminderScheduledAt
}
}`
)
@ -108,7 +109,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
const prefix = sub?.name ? `/~${sub.name}` : ''
await router.push(prefix + '/recent')
}
toastDeleteScheduled(toaster, data, 'upsertLink', !!item, values.text)
toastUpsertSuccessMessages(toaster, data, 'upsertLink', !!item, values.text)
}, [upsertLink, router]
)

View File

@ -52,7 +52,8 @@ function Notification ({ n, fresh }) {
(type === 'SubStatus' && <SubStatus n={n} />) ||
(type === 'FollowActivity' && <FollowActivity n={n} />) ||
(type === 'TerritoryPost' && <TerritoryPost n={n} />) ||
(type === 'TerritoryTransfer' && <TerritoryTransfer n={n} />)
(type === 'TerritoryTransfer' && <TerritoryTransfer n={n} />) ||
(type === 'Reminder' && <Reminder n={n} />)
}
</NotificationLayout>
)
@ -451,6 +452,23 @@ function TerritoryTransfer ({ n }) {
)
}
function Reminder ({ n }) {
return (
<>
<small className='fw-bold text-info ms-2'>you asked to be reminded of this {n.item.title ? 'post' : 'comment'}</small>
{n.item.title
? <div className='ms-2'><Item item={n.item} /></div>
: (
<div className='pb-2'>
<RootProvider root={n.item.root}>
<Comment item={n.item} noReply includeParent clickToContext rootText='replying on:' />
</RootProvider>
</div>
)}
</>
)
}
export function NotificationAlert () {
const [showAlert, setShowAlert] = useState(false)
const [hasSubscription, setHasSubscription] = useState(false)

View File

@ -8,7 +8,7 @@ import { datePivot } from '@/lib/time'
import { pollSchema } from '@/lib/validate'
import { SubSelectInitial } from './sub-select'
import { useCallback } from 'react'
import { normalizeForwards, toastDeleteScheduled } from '@/lib/form'
import { normalizeForwards, toastUpsertSuccessMessages } from '@/lib/form'
import useCrossposter from './use-crossposter'
import { useMe } from './me'
import { useToast } from './toast'
@ -31,6 +31,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
options: $options, boost: $boost, forward: $forward, hash: $hash, hmac: $hmac, pollExpiresAt: $pollExpiresAt) {
id
deleteScheduledAt
reminderScheduledAt
}
}`
)
@ -65,7 +66,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
const prefix = sub?.name ? `/~${sub.name}` : ''
await router.push(prefix + '/recent')
}
toastDeleteScheduled(toaster, data, 'upsertPoll', !!item, values.text)
toastUpsertSuccessMessages(toaster, data, 'upsertPoll', !!item, values.text)
}, [upsertPoll, router]
)

View File

@ -9,7 +9,7 @@ import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineIt
import { commentsViewedAfterComment } from '@/lib/new-comments'
import { commentSchema } from '@/lib/validate'
import { useToast } from './toast'
import { toastDeleteScheduled } from '@/lib/form'
import { toastUpsertSuccessMessages } from '@/lib/form'
import { ItemButtonBar } from './post'
import { useShowModal } from './modal'
import { Button } from 'react-bootstrap'
@ -54,6 +54,7 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
upsertComment(text: $text, parentId: $parentId, hash: $hash, hmac: $hmac) {
...CommentFields
deleteScheduledAt
reminderScheduledAt
comments {
...CommentsRecursive
}
@ -98,7 +99,7 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
const onSubmit = useCallback(async ({ amount, hash, hmac, ...values }, { resetForm }) => {
const { data } = await upsertComment({ variables: { parentId, hash, hmac, ...values } })
toastDeleteScheduled(toaster, data, 'upsertComment', false, values.text)
toastUpsertSuccessMessages(toaster, data, 'upsertComment', false, values.text)
resetForm({ text: '' })
setReply(replyOpen || false)
}, [upsertComment, setReply, parentId])

View File

@ -141,6 +141,13 @@ export const NOTIFICATIONS = gql`
autoWithdraw
}
}
... on Reminder {
id
sortTime
item {
...ItemFullFields
}
}
}
}
} `

View File

@ -1,4 +1,4 @@
import { hasDeleteMention } from './item'
import { hasDeleteMention, hasReminderMention } from './item'
/**
* Normalize an array of forwards by converting the pct from a string to a number
@ -13,7 +13,12 @@ export const normalizeForwards = (forward) => {
return forward.filter(fwd => fwd.nym || fwd.user?.name).map(fwd => ({ nym: fwd.nym ?? fwd.user?.name, pct: Number(fwd.pct) }))
}
export const toastDeleteScheduled = (toaster, upsertResponseData, dataKey, isEdit, itemText) => {
export const toastUpsertSuccessMessages = (toaster, upsertResponseData, dataKey, isEdit, itemText) => {
toastDeleteScheduled(toaster, upsertResponseData, dataKey, isEdit, itemText)
toastReminderScheduled(toaster, upsertResponseData, dataKey, isEdit, itemText)
}
const toastDeleteScheduled = (toaster, upsertResponseData, dataKey, isEdit, itemText) => {
const data = upsertResponseData[dataKey]
if (!data) return
@ -44,3 +49,35 @@ export const toastDeleteScheduled = (toaster, upsertResponseData, dataKey, isEdi
toaster.success(message, { persistOnNavigate: itemType !== 'comment' })
}
}
const toastReminderScheduled = (toaster, upsertResponseData, dataKey, isEdit, itemText) => {
const data = upsertResponseData[dataKey]
if (!data) return
const reminderMentioned = hasReminderMention(itemText)
const reminderScheduled = !!data.reminderScheduledAt
if (!reminderMentioned) return
if (reminderMentioned && !reminderScheduled) {
// There's a reminder mention but the reminder wasn't scheduled
toaster.warning('it looks like you tried to use the remindme bot but it didn\'t work. make sure you use the correct format: "@remindme in n units" e.g. "@remindme in 2 hours"', 10000)
return
}
// when we reached this code, we know that a reminder was scheduled
const reminderScheduledAt = new Date(data.reminderScheduledAt)
if (reminderScheduledAt) {
const itemType = {
upsertDiscussion: 'discussion post',
upsertLink: 'link post',
upsertPoll: 'poll',
upsertBounty: 'bounty',
upsertJob: 'job',
upsertComment: 'comment'
}[dataKey] ?? 'item'
const message = `you will be reminded of ${itemType === 'comment' ? 'your comment' : isEdit ? `this ${itemType}` : `your new ${itemType}`} at ${reminderScheduledAt.toLocaleString()}`
// only persist this on navigation for posts, not comments
toaster.success(message, { persistOnNavigate: itemType !== 'comment' })
}
}

View File

@ -17,6 +17,10 @@ const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|mon
const deleteMentionPattern = /\B@delete/i
const reminderPattern = /\B@remindme\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
const reminderMentionPattern = /\B@remindme/i
export const hasDeleteMention = (text) => deleteMentionPattern.test(text ?? '')
export const getDeleteCommand = (text) => {
@ -28,6 +32,17 @@ export const getDeleteCommand = (text) => {
export const hasDeleteCommand = (text) => !!getDeleteCommand(text)
export const hasReminderMention = (text) => reminderMentionPattern.test(text ?? '')
export const getReminderCommand = (text) => {
if (!text) return false
const matches = [...text.matchAll(reminderPattern)]
const commands = matches?.map(match => ({ number: match[1], unit: match[2] }))
return commands.length ? commands[commands.length - 1] : undefined
}
export const hasReminderCommand = (text) => !!getReminderCommand(text)
export const deleteItemByAuthor = async ({ models, id, item }) => {
if (!item) {
item = await models.item.findUnique({ where: { id: Number(id) } })

View File

@ -361,3 +361,16 @@ export async function notifyStreakLost (userId, streak) {
console.error(err)
}
}
export async function notifyReminder ({ userId, item, itemId }) {
try {
await sendUserNotification(userId, {
title: 'this is your requested reminder',
body: `you asked to be reminded of this ${item ? item.title ? 'post' : 'comment' : 'item'}`,
tag: `REMIND-ITEM-${item?.id ?? itemId}`,
item: item ?? { id: itemId }
})
} catch (err) {
console.error(err)
}
}

View File

@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "Reminder" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"itemId" INTEGER NOT NULL,
"remindAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Reminder_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Reminder.userId_reminderAt_index" ON "Reminder"("userId", "remindAt");
-- AddForeignKey
ALTER TABLE "Reminder" ADD CONSTRAINT "Reminder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Reminder" ADD CONSTRAINT "Reminder_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -124,6 +124,7 @@ model User {
AncestorReplies Reply[] @relation("AncestorReplyUser")
Replies Reply[]
walletLogs WalletLog[]
Reminder Reminder[]
@@index([photoId])
@@index([createdAt], map: "users.created_at_index")
@ -163,10 +164,10 @@ model Wallet {
}
model WalletLog {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
wallet WalletType
level LogLevel
message String
@ -421,6 +422,7 @@ model Item {
pollExpiresAt DateTime?
Ancestors Reply[] @relation("AncestorReplyItem")
Replies Reply[]
Reminder Reminder[]
@@index([uploadId])
@@index([lastZapAt])
@ -853,6 +855,18 @@ model TerritoryTransfer {
@@index([createdAt, oldUserId], map: "TerritoryTransfer.oldUserId_index")
}
model Reminder {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
userId Int
itemId Int
remindAt DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
@@index([userId, remindAt], map: "Reminder.userId_reminderAt_index")
}
enum EarnType {
POST
COMMENT

View File

@ -24,6 +24,7 @@ import { territoryBilling, territoryRevenue } from './territory.js'
import { ofac } from './ofac.js'
import { autoWithdraw } from './autowithdraw.js'
import { saltAndHashEmails } from './saltAndHashEmails.js'
import { remindUser } from './reminder.js'
const { loadEnvConfig } = nextEnv
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
@ -102,6 +103,7 @@ async function work () {
await boss.work('territoryRevenue', jobWrapper(territoryRevenue))
await boss.work('ofac', jobWrapper(ofac))
await boss.work('saltAndHashEmails', jobWrapper(saltAndHashEmails))
await boss.work('reminder', jobWrapper(remindUser))
console.log('working jobs')
}

18
worker/reminder.js Normal file
View File

@ -0,0 +1,18 @@
import { notifyReminder } from '@/lib/webPush'
export async function remindUser ({ data: { itemId, userId }, models }) {
let item
try {
item = await models.item.findUnique({ where: { id: itemId } })
} catch (err) {
console.error('failed to lookup item by id', err)
}
try {
if (item) {
await notifyReminder({ userId, item, itemId })
} else {
await notifyReminder({ userId, itemId })
}
} catch (err) {
console.error('failed to send push notification for reminder', err)
}
}