Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: daily stacked and spent sats notification #1756

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
39 changes: 39 additions & 0 deletions api/resolvers/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { pushSubscriptionSchema, validateSchema } from '@/lib/validate'
import { replyToSubscription } from '@/lib/webPush'
import { getSub } from './sub'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { msatsToSats } from '@/lib/format'

export default {
Query: {
Expand Down Expand Up @@ -361,6 +362,21 @@ export default {
LIMIT ${LIMIT})`
)

if (meFull.noteSatSummary) {
queries.push(
`(SELECT 'stats_' || date_trunc('day', t)::text as id,
t AS "sortTime", NULL as "earnedSats", 'SatSummary' AS type
FROM user_stats_days
WHERE id = $1
AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '2 day')
AND t <= $2
GROUP BY t
HAVING sum(msats_stacked) != 0 OR sum(msats_spent) != 0
ORDER BY "sortTime" DESC
LIMIT 1)`
)
}

const notifications = await models.$queryRawUnsafe(
`SELECT id, "sortTime", "earnedSats", type,
"sortTime" AS "minSortTime"
Expand Down Expand Up @@ -486,6 +502,29 @@ export default {
return res.length ? res[0].type : null
}
},
SatSummary: {
date: async (n, args, { models }) => {
return new Date(n.sortTime)
},
stacked: async (n, args, { me, models }) => {
const [{ stacked }] = await models.$queryRaw`
SELECT sum(msats_stacked) as stacked
FROM user_stats_days
WHERE id = ${Number(me.id)}
AND t = ${n.sortTime}::timestamp
`
return (stacked && msatsToSats(stacked)) || 0
},
spent: async (n, args, { me, models }) => {
const [{ spent }] = await models.$queryRaw`
SELECT sum(msats_spent) as spent
FROM user_stats_days
WHERE id = ${Number(me.id)}
AND t = ${n.sortTime}::timestamp
`
return (spent && msatsToSats(spent)) || 0
}
},
Earn: {
sources: async (n, args, { me, models }) => {
const [sources] = await models.$queryRawUnsafe(`
Expand Down
14 changes: 14 additions & 0 deletions api/resolvers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,20 @@ export default {
}
}

if (user.noteSatSummary) {
const [satSummary] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM user_stats_days
WHERE "user_stats_days"."id" = $1
AND t > $2
LIMIT 1)`, me.id, lastChecked)
if (satSummary.exists) {
foundNotes()
return true
}
}

const subStatus = await models.sub.findFirst({
where: {
userId: me.id,
Expand Down
10 changes: 9 additions & 1 deletion api/typeDefs/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ export default gql`
type: String!
}

type SatSummary {
id: ID!
sortTime: Date!
date: Date!
stacked: Int
spent: Int
}

type Earn {
id: ID!
earnedSats: Int!
Expand Down Expand Up @@ -155,7 +163,7 @@ export default gql`

union Notification = Reply | Votification | Mention
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
| Streak | SatSummary | FollowActivity | ForwardedVotification | Revenue | SubStatus
| TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification
| ReferralReward

Expand Down
2 changes: 2 additions & 0 deletions api/typeDefs/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export default gql`
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
noteSatSummary: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!,
noteWithdrawals: Boolean!,
Expand Down Expand Up @@ -168,6 +169,7 @@ export default gql`
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
noteSatSummary: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!
noteWithdrawals: Boolean!
Expand Down
15 changes: 15 additions & 0 deletions components/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import HandCoin from '@/svgs/hand-coin-fill.svg'
import UserAdd from '@/svgs/user-add-fill.svg'
import { LOST_BLURBS, FOUND_BLURBS, UNKNOWN_LINK_REL } from '@/lib/constants'
import CowboyHatIcon from '@/svgs/cowboy.svg'
import ScalesIcon from '@/svgs/scales-of-justice.svg'
import BaldIcon from '@/svgs/bald.svg'
import GunIcon from '@/svgs/revolver.svg'
import HorseIcon from '@/svgs/horse.svg'
Expand Down Expand Up @@ -57,6 +58,7 @@ function Notification ({ n, fresh }) {
(type === 'WithdrawlPaid' && <WithdrawlPaid n={n} />) ||
(type === 'Referral' && <Referral n={n} />) ||
(type === 'Streak' && <Streak n={n} />) ||
(type === 'SatSummary' && <SatSummary n={n} />) ||
(type === 'Votification' && <Votification n={n} />) ||
(type === 'ForwardedVotification' && <ForwardedVotification n={n} />) ||
(type === 'Mention' && <Mention n={n} />) ||
Expand Down Expand Up @@ -162,6 +164,7 @@ const defaultOnClick = n => {
if (type === 'Referral') return { href: '/referrals/month' }
if (type === 'ReferralReward') return { href: '/referrals/month' }
if (type === 'Streak') return {}
if (type === 'SatSummary') return { href: '/satistics?inc=stacked,spent' }
if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` }

if (!n.item) return {}
Expand Down Expand Up @@ -200,6 +203,18 @@ function Streak ({ n }) {
)
}

function SatSummary ({ n }) {
return (
<div className='d-flex'>
<div style={{ fontSize: '2rem' }}><ScalesIcon className='fill-grey' fill='gray' height={40} width={40} /></div>
<div className='ms-1 p-1'>
<span className='fw-bold'>you stacked {numWithUnits(n.stacked, { abbreviate: false })} and spent {numWithUnits(n.spent, { abbreviate: false })}</span>
<div><small style={{ lineHeight: '140%', display: 'inline-block' }}>on {dayMonthYear(new Date(n.date))}</small></div>
</div>
</div>
)
}

function EarnNotification ({ n }) {
const time = n.minSortTime === n.sortTime ? dayMonthYear(new Date(n.minSortTime)) : `${dayMonthYear(new Date(n.minSortTime))} to ${dayMonthYear(new Date(n.sortTime))}`

Expand Down
7 changes: 7 additions & 0 deletions fragments/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ export const NOTIFICATIONS = gql`
days
type
}
... on SatSummary {
id
sortTime
date
stacked
spent
}
... on Earn {
id
sortTime
Expand Down
1 change: 1 addition & 0 deletions fragments/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const SETTINGS_FIELDS = gql`
noteWithdrawals
noteInvites
noteJobIndicator
noteSatSummary
noteCowboyHat
noteForwardedSats
hideInvoiceDesc
Expand Down
1 change: 1 addition & 0 deletions lib/apollo.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function getClient (uri) {
'WithdrawlPaid',
'Referral',
'Streak',
'SatSummary',
'FollowActivity',
'ForwardedVotification',
'Revenue',
Expand Down
13 changes: 13 additions & 0 deletions lib/webPush.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const createUserFilter = (tag) => {
EARN: 'noteEarning',
DEPOSIT: 'noteDeposits',
WITHDRAWAL: 'noteWithdrawals',
DAILY_STATS: 'noteSatSummary',
STREAK: 'noteCowboyHat'
}
const key = tagMap[tag.split('-')[0]]
Expand Down Expand Up @@ -392,6 +393,18 @@ export async function notifyStreakLost (userId, streak) {
}
}

export async function notifySatSummary (userId, stacked, spent) {
try {
await sendUserNotification(userId, {
title: 'your daily sat summary is ready',
body: `you stacked ${numWithUnits(stacked, { abbreviate: false })} and spent ${numWithUnits(spent, { abbreviate: false })}`,
tag: 'DAILY_STATS'
})
} catch (err) {
console.error(err)
}
}

export async function notifyReminder ({ userId, item, itemId }) {
try {
await sendUserNotification(userId, {
Expand Down
6 changes: 6 additions & 0 deletions pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export default function Settings ({ ssrData }) {
noteInvites: settings?.noteInvites,
noteJobIndicator: settings?.noteJobIndicator,
noteCowboyHat: settings?.noteCowboyHat,
noteSatSummary: settings?.noteSatSummary,
noteForwardedSats: settings?.noteForwardedSats,
hideInvoiceDesc: settings?.hideInvoiceDesc,
autoDropBolt11s: settings?.autoDropBolt11s,
Expand Down Expand Up @@ -330,6 +331,11 @@ export default function Settings ({ ssrData }) {
name='noteJobIndicator'
groupClassName='mb-0'
/>
<Checkbox
label='daily stacked and spent sats summary is available'
name='noteSatSummary'
groupClassName='mb-0'
/>
<Checkbox
label='I find or lose cowboy essentials (e.g. cowboy hat)'
name='noteCowboyHat'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "noteSatSummary" BOOLEAN NOT NULL DEFAULT false;
16 changes: 16 additions & 0 deletions prisma/migrations/20241223184742_satsummary_job/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE OR REPLACE FUNCTION check_daily_sats_summary()
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
BEGIN
INSERT INTO pgboss.schedule (name, cron, timezone)
VALUES ('dailySatSummary', '0 0 * * *', 'America/Chicago') ON CONFLICT DO NOTHING;
return 0;
EXCEPTION WHEN OTHERS THEN
return 0;
END;
$$;

SELECT check_daily_sats_summary();
DROP FUNCTION IF EXISTS check_daily_sats_summary;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ model User {
nostrCrossposting Boolean @default(false)
slashtagId String? @unique(map: "users.slashtagId_unique")
noteCowboyHat Boolean @default(true)
noteSatSummary Boolean @default(false)
streak Int?
gunStreak Int?
horseStreak Int?
Expand Down
1 change: 1 addition & 0 deletions svgs/scales-of-justice.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { indexItem, indexAllItems } from './search'
import { timestampItem } from './ots'
import { computeStreaks, checkStreak } from './streak'
import { dailySatSummary } from './satSummary'
import { nip57 } from './nostr'
import fetch from 'cross-fetch'
import { authenticatedLndGrpc } from '@/lib/lnd'
Expand Down Expand Up @@ -131,6 +132,7 @@ async function work () {
await boss.work('earn', jobWrapper(earn))
await boss.work('streak', jobWrapper(computeStreaks))
await boss.work('checkStreak', jobWrapper(checkStreak))
await boss.work('dailySatSummary', jobWrapper(dailySatSummary))
await boss.work('nip57', jobWrapper(nip57))
await boss.work('views-*', jobWrapper(views))
await boss.work('rankViews', jobWrapper(rankViews))
Expand Down
30 changes: 30 additions & 0 deletions worker/satSummary.js
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

realized I didn't push this before, sorry

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { notifySatSummary } from '@/lib/webPush'
import { msatsToSats } from '@/lib/format'
export async function dailySatSummary ({ models }) {
try {
const stats = await models.$queryRaw`
SELECT
id as userId, sum(msats_stacked) as stacked, sum(msats_spent) as spent
FROM user_stats_days
WHERE t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day')
AND t <= date_trunc('day', CURRENT_DATE)
GROUP BY id
HAVING sum(msats_stacked) != 0 OR sum(msats_spent) != 0
`

if (stats.length) {
for (const stat of stats) {
const user = await models.user.findUnique({ where: { id: stat.userid } })
if (user && user.noteSatSummary) {
await notifySatSummary(
stat.userid,
(stats.stacked && msatsToSats(stats.stacked)) || 0,
(stats.spent && msatsToSats(stats.spent)) || 0
)
}
}
}
} catch (err) {
console.error('failed to process daily sat summary', err)
}
}
Loading