Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions api/paidAction/lib/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const getItemMentions = async ({ text }, { me, tx }) => {
return []
}

export async function performBotBehavior ({ text, id }, { me, tx }) {
export async function performBotBehavior ({ text, id }, { me, tx, lnd }) {
// delete any existing deleteItem or reminder jobs for this item
const userId = me?.id || USER_ID.anon
id = Number(id)
Expand All @@ -57,7 +57,15 @@ export async function performBotBehavior ({ text, id }, { me, tx }) {
await deleteReminders({ id, userId, models: tx })

if (text) {
const deleteAt = getDeleteAt(text)
// compute deleteAt with block-awareness
let deleteAt
try {
const { getHeight } = await import('ln-service')
const height = lnd ? (await getHeight({ lnd }))?.current_block_height : undefined
deleteAt = getDeleteAt(text, { currentBlockHeight: height })
} catch (e) {
deleteAt = getDeleteAt(text)
}
if (deleteAt) {
await tx.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
Expand All @@ -68,7 +76,18 @@ export async function performBotBehavior ({ text, id }, { me, tx }) {
${deleteAt}::TIMESTAMP WITH TIME ZONE + interval '1 minute')`
}

const remindAt = getRemindAt(text)
// compute remindAt with block-awareness. If absolute block height was specified,
// we need the current chain height. We try to read it via lnd if available.
let remindAt
try {
// lazy import to avoid bundling lnd client on client side
const { getHeight } = await import('ln-service')
const height = lnd ? (await getHeight({ lnd }))?.current_block_height : undefined
remindAt = getRemindAt(text, { currentBlockHeight: height })
} catch (e) {
// fallback to time-based parse only
remindAt = getRemindAt(text)
}
if (remindAt) {
await tx.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
Expand Down
95 changes: 81 additions & 14 deletions lib/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ export const defaultCommentSort = (pinned, bio, createdAt) => {
export const isJob = item => item.subName === 'jobs'

// a delete directive preceded by a non word character that isn't a backtick
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year|block)s?/gi

// support absolute block height deletes
const deleteBlockAtPattern = /\B@delete\s+at\s+block\s+(\d+)/gi

const deleteMentionPattern = /\B@delete/i

const reminderPattern = /\B@remindme\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
// support time-based and block-based relative reminders,
// where unit may be seconds/minutes/etc or blocks
const reminderPattern = /\B@remindme\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year|block)s?/gi

// support absolute block height reminders
const reminderBlockAtPattern = /\B@remindme\s+at\s+block\s+(\d+)/gi

const reminderMentionPattern = /\B@remindme/i

Expand All @@ -30,22 +38,81 @@ export const getDeleteCommand = (text) => {
return commands.length ? commands[commands.length - 1] : undefined
}

export const getDeleteAt = (text) => {
const command = getDeleteCommand(text)
if (command) {
const { number, unit } = command
return datePivot(new Date(), { [`${unit}s`]: number })
export const getDeleteAt = (text, opts = {}) => {
if (!text) return null

const relMatches = [...text.matchAll(deletePattern)]
const lastRel = relMatches.length ? relMatches[relMatches.length - 1] : null
const lastRelIndex = lastRel?.index ?? -1

const absMatches = [...text.matchAll(deleteBlockAtPattern)]
const lastAbs = absMatches.length ? absMatches[absMatches.length - 1] : null
const lastAbsIndex = lastAbs?.index ?? -1

if (lastRelIndex < 0 && lastAbsIndex < 0) return null

const now = new Date()

if (lastAbsIndex > lastRelIndex) {
const targetHeight = parseInt(lastAbs[1])
const { currentBlockHeight } = opts
if (Number.isInteger(currentBlockHeight)) {
const delta = targetHeight - currentBlockHeight
const minutes = Math.max(0, delta) * 10
return datePivot(now, { minutes })
}
return null
}

const number = parseInt(lastRel[1])
const unit = lastRel[2]
if (unit === 'block') {
const minutes = number * 10
return datePivot(now, { minutes })
}
return null
return datePivot(now, { [`${unit}s`]: number })
}

export const getRemindAt = (text) => {
const command = getReminderCommand(text)
if (command) {
const { number, unit } = command
return datePivot(new Date(), { [`${unit}s`]: number })
export const getRemindAt = (text, opts = {}) => {
if (!text) return null

// gather matches for relative (in N unit) including blocks
const relMatches = [...text.matchAll(reminderPattern)]
const lastRel = relMatches.length ? relMatches[relMatches.length - 1] : null
const lastRelIndex = lastRel?.index ?? -1

// gather matches for absolute (at block X)
const absMatches = [...text.matchAll(reminderBlockAtPattern)]
const lastAbs = absMatches.length ? absMatches[absMatches.length - 1] : null
const lastAbsIndex = lastAbs?.index ?? -1

// if neither present, nothing to do
if (lastRelIndex < 0 && lastAbsIndex < 0) return null

const now = new Date()

// prefer the last directive that appears in the text
if (lastAbsIndex > lastRelIndex) {
// absolute block target
const targetHeight = parseInt(lastAbs[1])
const { currentBlockHeight } = opts
if (Number.isInteger(currentBlockHeight)) {
const delta = targetHeight - currentBlockHeight
const minutes = Math.max(0, delta) * 10
return datePivot(now, { minutes })
}
// if we don't know current height, we can't compute accurately; return null
return null
}

// relative directive
const number = parseInt(lastRel[1])
const unit = lastRel[2]
if (unit === 'block') {
const minutes = number * 10
return datePivot(now, { minutes })
}
return null
return datePivot(now, { [`${unit}s`]: number })
}

export const hasDeleteCommand = (text) => !!getDeleteCommand(text)
Expand Down