Fork anchor, strip all push notification code (APNs, FCM, WebPush), rename from anchor to mailship, add new database schema for subscriptions + events tables, and add HTTP API for email notification registration and NIP-9a relay push callbacks. - POST /subscription/email — register for email digests - DELETE /subscription/:key — unsubscribe - POST /notify/:id — NIP-9a relay push callback - GET /confirm?token=... — confirm email - GET /unsubscribe?token=... — unsubscribe Co-authored-by: mplorentz
138 lines
3.3 KiB
TypeScript
138 lines
3.3 KiB
TypeScript
import sanitizeHtml from 'sanitize-html'
|
|
import { CronExpressionParser } from 'cron-parser'
|
|
|
|
export function getCronDate(cronString: string, n: number) {
|
|
const interval = CronExpressionParser.parse(cronString, { tz: 'UTC' })
|
|
|
|
let date = interval.next().toDate()
|
|
|
|
for (let i = 0; i < Math.abs(n); i++) {
|
|
if (n > 0) {
|
|
date = interval.next().toDate()
|
|
} else {
|
|
date = interval.prev().toDate()
|
|
}
|
|
}
|
|
|
|
return date
|
|
}
|
|
|
|
export function displayDuration(seconds: number) {
|
|
const minute = 60
|
|
const hour = minute * 60
|
|
const day = hour * 24
|
|
const week = day * 7
|
|
const month = day * 30
|
|
const year = day * 365
|
|
|
|
if (seconds < minute) {
|
|
return `${Math.round(seconds)} seconds`
|
|
} else if (seconds < hour) {
|
|
const minutes = Math.round(seconds / minute)
|
|
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`
|
|
} else if (seconds < day) {
|
|
const hours = Math.round(seconds / hour)
|
|
return `${hours} ${hours === 1 ? 'hour' : 'hours'}`
|
|
} else if (seconds < week) {
|
|
const days = Math.round(seconds / day)
|
|
return `${days} ${days === 1 ? 'day' : 'days'}`
|
|
} else if (seconds < month) {
|
|
const weeks = Math.round(seconds / week)
|
|
return `${weeks} ${weeks === 1 ? 'week' : 'weeks'}`
|
|
} else if (seconds < year) {
|
|
const months = Math.round(seconds / month)
|
|
return `${months} ${months === 1 ? 'month' : 'months'}`
|
|
} else {
|
|
const years = Math.round(seconds / year)
|
|
return `${years} ${years === 1 ? 'year' : 'years'}`
|
|
}
|
|
}
|
|
|
|
interface ElementAttributes {
|
|
[key: string]: string
|
|
}
|
|
|
|
interface ElementChild {
|
|
outerHTML: string
|
|
}
|
|
|
|
export interface CustomElement {
|
|
tagName: string
|
|
attributes: ElementAttributes
|
|
children: ElementChild[]
|
|
_innerText: string
|
|
setAttribute(name: string, value: string): void
|
|
getAttribute(name: string): string | undefined
|
|
appendChild(child: ElementChild): void
|
|
href: string
|
|
target: string
|
|
innerText: string
|
|
innerHTML: string
|
|
outerHTML: string
|
|
}
|
|
|
|
export function createElement(tagName: string) {
|
|
const element: CustomElement = {
|
|
tagName: tagName.toLowerCase(),
|
|
attributes: {},
|
|
children: [],
|
|
_innerText: '',
|
|
|
|
setAttribute(name: string, value: string) {
|
|
this.attributes[name] = value
|
|
},
|
|
|
|
getAttribute(name: string): string | undefined {
|
|
return this.attributes[name]
|
|
},
|
|
|
|
appendChild(child: ElementChild) {
|
|
this.children.push(child)
|
|
},
|
|
|
|
get href() {
|
|
return this.attributes.href || ''
|
|
},
|
|
|
|
set href(value: string) {
|
|
this.attributes.href = value
|
|
},
|
|
|
|
get target() {
|
|
return this.attributes.target || ''
|
|
},
|
|
|
|
set target(value: string) {
|
|
this.attributes.target = value
|
|
},
|
|
|
|
get innerText() {
|
|
return this._innerText
|
|
},
|
|
|
|
set innerText(value: string) {
|
|
this._innerText = sanitizeHtml(value, {
|
|
allowedTags: [],
|
|
allowedAttributes: {},
|
|
})
|
|
},
|
|
|
|
get innerHTML() {
|
|
return this._innerText
|
|
},
|
|
|
|
set innerHTML(value: string) {
|
|
this._innerText = value
|
|
},
|
|
|
|
get outerHTML() {
|
|
const attributesString = Object.entries(this.attributes)
|
|
.map(([key, value]) => `${key}="${value}"`)
|
|
.join(' ')
|
|
|
|
return `<${this.tagName}${attributesString ? ' ' + attributesString : ''}>${this._innerText}</${this.tagName}>`
|
|
},
|
|
}
|
|
|
|
return element
|
|
}
|