From e80fa99433ad977aa5b530a7a2d80c703e35edcc Mon Sep 17 00:00:00 2001 From: Soxasora Date: Sun, 22 Dec 2024 17:05:16 +0100 Subject: [PATCH 01/11] daily summary of stacked and spent sats --- api/resolvers/notifications.js | 39 +++++++++++++++++++ api/resolvers/user.js | 13 +++++++ api/typeDefs/notifications.js | 10 ++++- api/typeDefs/user.js | 2 + components/notifications.js | 17 ++++++++ fragments/notifications.js | 7 ++++ fragments/users.js | 1 + lib/apollo.js | 1 + lib/webPush.js | 13 +++++++ pages/settings/index.js | 6 +++ .../migration.sql | 2 + prisma/schema.prisma | 1 + svgs/lightning.svg | 2 +- worker/satsummary.js | 14 +++++++ 14 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20241222143219_daily_sats_summary_notifications/migration.sql create mode 100644 worker/satsummary.js diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index a7af37bc5..1a1e8050e 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -361,6 +361,22 @@ export default { LIMIT ${LIMIT})` ) + if (meFull.noteDailyStats) { + queries.push( + `(SELECT CONCAT('stats_', date_trunc('day', t))::text as id, + t AS "sortTime", + NULL as "earnedSats", + 'DailyStats' AS type + FROM user_stats_days + WHERE "user_stats_days"."id" = $1 + AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day') + AND t <= $2 + GROUP BY t, msats_stacked, msats_spent + ORDER BY "sortTime" DESC + LIMIT ${LIMIT})` + ) + } + const notifications = await models.$queryRawUnsafe( `SELECT id, "sortTime", "earnedSats", type, "sortTime" AS "minSortTime" @@ -486,6 +502,29 @@ export default { return res.length ? res[0].type : null } }, + DailyStats: { + date: async (n, args, { models }) => { + return new Date(n.id.replace('stats_', '')) + }, + stacked: async (n, args, { me, models }) => { + const res = await models.$queryRaw` + SELECT ((msats_stacked+msats_rewards)::float)/1000.0 as sats_stacked + FROM user_stats_days + WHERE id = ${Number(me.id)} + AND t = ${new Date(n.id.replace('stats_', ''))}::timestamp + ` + return res.length ? res[0].sats_stacked : 0 + }, + spent: async (n, args, { me, models }) => { + const res = await models.$queryRaw` + SELECT (msats_spent::float)/1000.0 as sats_spent + FROM user_stats_days + WHERE id = ${Number(me.id)} + AND t = ${new Date(n.id.replace('stats_', ''))}::timestamp + ` + return res.length ? res[0].sats_spent : 0 + } + }, Earn: { sources: async (n, args, { me, models }) => { const [sources] = await models.$queryRawUnsafe(` diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 03f6a8d13..a1c62816f 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -523,6 +523,19 @@ export default { } } + if (user.noteDailyStats) { + const dailyStats = await models.dailyStats.findFirst({ + where: { + id: me.id + } + }) + + if (dailyStats) { + foundNotes() + return true + } + } + const subStatus = await models.sub.findFirst({ where: { userId: me.id, diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js index 4eabb3564..9b1dac3b1 100644 --- a/api/typeDefs/notifications.js +++ b/api/typeDefs/notifications.js @@ -82,6 +82,14 @@ export default gql` type: String! } + type DailyStats { + id: ID! + sortTime: Date! + date: Date! + stacked: Int + spent: Int + } + type Earn { id: ID! earnedSats: Int! @@ -155,7 +163,7 @@ export default gql` union Notification = Reply | Votification | Mention | Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral - | Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus + | Streak | DailyStats | FollowActivity | ForwardedVotification | Revenue | SubStatus | TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification | ReferralReward diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index e61cb4b76..000765cdc 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -94,6 +94,7 @@ export default gql` nostrPubkey: String nostrRelays: [String!] noteAllDescendants: Boolean! + noteDailyStats: Boolean noteCowboyHat: Boolean! noteDeposits: Boolean!, noteWithdrawals: Boolean!, @@ -168,6 +169,7 @@ export default gql` nostrPubkey: String nostrRelays: [String!] noteAllDescendants: Boolean! + noteDailyStats: Boolean! noteCowboyHat: Boolean! noteDeposits: Boolean! noteWithdrawals: Boolean! diff --git a/components/notifications.js b/components/notifications.js index a4937c3bf..91fe0ce3f 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -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 LightningIcon from '@/svgs/lightning.svg' import BaldIcon from '@/svgs/bald.svg' import GunIcon from '@/svgs/revolver.svg' import HorseIcon from '@/svgs/horse.svg' @@ -57,6 +58,7 @@ function Notification ({ n, fresh }) { (type === 'WithdrawlPaid' && ) || (type === 'Referral' && ) || (type === 'Streak' && ) || + (type === 'DailyStats' && ) || (type === 'Votification' && ) || (type === 'ForwardedVotification' && ) || (type === 'Mention' && ) || @@ -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 === 'DailyStats') return {} if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` } if (!n.item) return {} @@ -200,6 +203,20 @@ function Streak ({ n }) { ) } +function DailyStats ({ n }) { // WIP + // TODO ADD SCALES OF JUSTICE AS ICON + // stacked and spent + return ( +
+
+
+ you stacked {numWithUnits(n.stacked, { abbreviate: false })} and spent {numWithUnits(n.spent, { abbreviate: false })} +
on {dayMonthYear(new Date(n.date))}
+
+
+ ) +} + 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))}` diff --git a/fragments/notifications.js b/fragments/notifications.js index 0915ccdf1..d08d9448f 100644 --- a/fragments/notifications.js +++ b/fragments/notifications.js @@ -88,6 +88,13 @@ export const NOTIFICATIONS = gql` days type } + ... on DailyStats { + id + sortTime + date + stacked + spent + } ... on Earn { id sortTime diff --git a/fragments/users.js b/fragments/users.js index b96ec9327..e38b76b80 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -82,6 +82,7 @@ export const SETTINGS_FIELDS = gql` noteWithdrawals noteInvites noteJobIndicator + noteDailyStats noteCowboyHat noteForwardedSats hideInvoiceDesc diff --git a/lib/apollo.js b/lib/apollo.js index 6e2eeab1a..ed7b8e050 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -63,6 +63,7 @@ function getClient (uri) { 'WithdrawlPaid', 'Referral', 'Streak', + 'DailyStats', 'FollowActivity', 'ForwardedVotification', 'Revenue', diff --git a/lib/webPush.js b/lib/webPush.js index 88b20047d..3084285c4 100644 --- a/lib/webPush.js +++ b/lib/webPush.js @@ -46,6 +46,7 @@ const createUserFilter = (tag) => { EARN: 'noteEarning', DEPOSIT: 'noteDeposits', WITHDRAWAL: 'noteWithdrawals', + DAILY_STATS: 'noteDailyStats', STREAK: 'noteCowboyHat' } const key = tagMap[tag.split('-')[0]] @@ -392,6 +393,18 @@ export async function notifyStreakLost (userId, streak) { } } +export async function notifyDailyStats (userId, stats) { // WIP + try { + await sendUserNotification(userId, { + title: 'WIP sats stats', + body: `you stacked ${numWithUnits(stats.stacked, { abbreviate: false })} and spent ${numWithUnits(stats.spent, { abbreviate: false })}`, + tag: 'DAILY_STATS' + }) + } catch (err) { + console.error(err) + } +} + export async function notifyReminder ({ userId, item, itemId }) { try { await sendUserNotification(userId, { diff --git a/pages/settings/index.js b/pages/settings/index.js index d5941ea18..69e666fcd 100644 --- a/pages/settings/index.js +++ b/pages/settings/index.js @@ -138,6 +138,7 @@ export default function Settings ({ ssrData }) { noteInvites: settings?.noteInvites, noteJobIndicator: settings?.noteJobIndicator, noteCowboyHat: settings?.noteCowboyHat, + noteDailyStats: settings?.noteDailyStats, noteForwardedSats: settings?.noteForwardedSats, hideInvoiceDesc: settings?.hideInvoiceDesc, autoDropBolt11s: settings?.autoDropBolt11s, @@ -330,6 +331,11 @@ export default function Settings ({ ssrData }) { name='noteJobIndicator' groupClassName='mb-0' /> + + diff --git a/worker/satsummary.js b/worker/satsummary.js new file mode 100644 index 000000000..075f670c1 --- /dev/null +++ b/worker/satsummary.js @@ -0,0 +1,14 @@ +import { notifyDailyStats } from '@/lib/webPush' +export async function summarizeDailySats ({ data: { userId }, models }) { + let dailyStats + try { + dailyStats = await models.dailyStats.findUnique({ where: { id: userId } }) + } catch (err) { + console.error('failed to lookup daily stats by user', err) + } + try { + await notifyDailyStats({ userId, dailyStats }) + } catch (err) { + console.error('failed to send push notification for daily stats', err) + } +} From 057fc59875cc6487f15b5b5420023d11b4e45477 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Sun, 22 Dec 2024 17:32:37 +0100 Subject: [PATCH 02/11] scales of justice as icon --- components/notifications.js | 5 ++--- svgs/scales-of-justice.svg | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 svgs/scales-of-justice.svg diff --git a/components/notifications.js b/components/notifications.js index 91fe0ce3f..7bf081b40 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -13,7 +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 LightningIcon from '@/svgs/lightning.svg' +import JusticeIcon from '@/svgs/scales-of-justice.svg' // WIP import BaldIcon from '@/svgs/bald.svg' import GunIcon from '@/svgs/revolver.svg' import HorseIcon from '@/svgs/horse.svg' @@ -204,11 +204,10 @@ function Streak ({ n }) { } function DailyStats ({ n }) { // WIP - // TODO ADD SCALES OF JUSTICE AS ICON // stacked and spent return (
-
+
you stacked {numWithUnits(n.stacked, { abbreviate: false })} and spent {numWithUnits(n.spent, { abbreviate: false })}
on {dayMonthYear(new Date(n.date))}
diff --git a/svgs/scales-of-justice.svg b/svgs/scales-of-justice.svg new file mode 100644 index 000000000..17056650a --- /dev/null +++ b/svgs/scales-of-justice.svg @@ -0,0 +1 @@ + \ No newline at end of file From d1663ee1b7d20e9efb22abadc4a2c681891e955e Mon Sep 17 00:00:00 2001 From: Soxasora Date: Mon, 23 Dec 2024 13:41:56 +0100 Subject: [PATCH 03/11] cleanup: query follows satistics design; refactor: DailyStats to SatSummary --- api/resolvers/notifications.js | 21 ++++++++++--------- api/resolvers/user.js | 6 +++--- api/typeDefs/notifications.js | 4 ++-- api/typeDefs/user.js | 4 ++-- components/notifications.js | 6 +++--- fragments/notifications.js | 2 +- fragments/users.js | 2 +- lib/apollo.js | 2 +- lib/webPush.js | 4 ++-- pages/settings/index.js | 4 ++-- .../migration.sql | 2 +- prisma/schema.prisma | 2 +- worker/satsummary.js | 8 +++---- 13 files changed, 34 insertions(+), 33 deletions(-) diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 1a1e8050e..5b2928d59 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -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: { @@ -361,12 +362,12 @@ export default { LIMIT ${LIMIT})` ) - if (meFull.noteDailyStats) { + if (meFull.noteSatSummary) { queries.push( `(SELECT CONCAT('stats_', date_trunc('day', t))::text as id, t AS "sortTime", NULL as "earnedSats", - 'DailyStats' AS type + 'SatSummary' AS type FROM user_stats_days WHERE "user_stats_days"."id" = $1 AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day') @@ -502,27 +503,27 @@ export default { return res.length ? res[0].type : null } }, - DailyStats: { + SatSummary: { date: async (n, args, { models }) => { return new Date(n.id.replace('stats_', '')) }, - stacked: async (n, args, { me, models }) => { - const res = await models.$queryRaw` - SELECT ((msats_stacked+msats_rewards)::float)/1000.0 as sats_stacked + stacked: async (n, args, { me, models }) => { // msats_rewards is already counted in msats_stacked + const [{ stacked }] = await models.$queryRaw` + SELECT sum(msats_stacked) as stacked FROM user_stats_days WHERE id = ${Number(me.id)} AND t = ${new Date(n.id.replace('stats_', ''))}::timestamp ` - return res.length ? res[0].sats_stacked : 0 + return (stacked && msatsToSats(stacked)) || 0 }, spent: async (n, args, { me, models }) => { - const res = await models.$queryRaw` - SELECT (msats_spent::float)/1000.0 as sats_spent + const [{ spent }] = await models.$queryRaw` + SELECT sum(msats_spent) as spent FROM user_stats_days WHERE id = ${Number(me.id)} AND t = ${new Date(n.id.replace('stats_', ''))}::timestamp ` - return res.length ? res[0].sats_spent : 0 + return (spent && msatsToSats(spent)) || 0 } }, Earn: { diff --git a/api/resolvers/user.js b/api/resolvers/user.js index a1c62816f..19f360ac8 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -523,14 +523,14 @@ export default { } } - if (user.noteDailyStats) { - const dailyStats = await models.dailyStats.findFirst({ + if (user.noteSatSummary) { + const satSummary = await models.satSummary.findFirst({ where: { id: me.id } }) - if (dailyStats) { + if (satSummary) { foundNotes() return true } diff --git a/api/typeDefs/notifications.js b/api/typeDefs/notifications.js index 9b1dac3b1..c70e207fa 100644 --- a/api/typeDefs/notifications.js +++ b/api/typeDefs/notifications.js @@ -82,7 +82,7 @@ export default gql` type: String! } - type DailyStats { + type SatSummary { id: ID! sortTime: Date! date: Date! @@ -163,7 +163,7 @@ export default gql` union Notification = Reply | Votification | Mention | Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral - | Streak | DailyStats | FollowActivity | ForwardedVotification | Revenue | SubStatus + | Streak | SatSummary | FollowActivity | ForwardedVotification | Revenue | SubStatus | TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification | ReferralReward diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 000765cdc..0b3504383 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -94,7 +94,7 @@ export default gql` nostrPubkey: String nostrRelays: [String!] noteAllDescendants: Boolean! - noteDailyStats: Boolean + noteSatSummary: Boolean noteCowboyHat: Boolean! noteDeposits: Boolean!, noteWithdrawals: Boolean!, @@ -169,7 +169,7 @@ export default gql` nostrPubkey: String nostrRelays: [String!] noteAllDescendants: Boolean! - noteDailyStats: Boolean! + noteSatSummary: Boolean! noteCowboyHat: Boolean! noteDeposits: Boolean! noteWithdrawals: Boolean! diff --git a/components/notifications.js b/components/notifications.js index 7bf081b40..a60b20502 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -58,7 +58,7 @@ function Notification ({ n, fresh }) { (type === 'WithdrawlPaid' && ) || (type === 'Referral' && ) || (type === 'Streak' && ) || - (type === 'DailyStats' && ) || + (type === 'SatSummary' && ) || (type === 'Votification' && ) || (type === 'ForwardedVotification' && ) || (type === 'Mention' && ) || @@ -164,7 +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 === 'DailyStats') return {} + if (type === 'SatSummary') return {} if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` } if (!n.item) return {} @@ -203,7 +203,7 @@ function Streak ({ n }) { ) } -function DailyStats ({ n }) { // WIP +function SatSummary ({ n }) { // WIP // stacked and spent return (
diff --git a/fragments/notifications.js b/fragments/notifications.js index d08d9448f..5cd15577f 100644 --- a/fragments/notifications.js +++ b/fragments/notifications.js @@ -88,7 +88,7 @@ export const NOTIFICATIONS = gql` days type } - ... on DailyStats { + ... on SatSummary { id sortTime date diff --git a/fragments/users.js b/fragments/users.js index e38b76b80..61f2d4925 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -82,7 +82,7 @@ export const SETTINGS_FIELDS = gql` noteWithdrawals noteInvites noteJobIndicator - noteDailyStats + noteSatSummary noteCowboyHat noteForwardedSats hideInvoiceDesc diff --git a/lib/apollo.js b/lib/apollo.js index ed7b8e050..f412fbfae 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -63,7 +63,7 @@ function getClient (uri) { 'WithdrawlPaid', 'Referral', 'Streak', - 'DailyStats', + 'SatSummary', 'FollowActivity', 'ForwardedVotification', 'Revenue', diff --git a/lib/webPush.js b/lib/webPush.js index 3084285c4..b0123ea1b 100644 --- a/lib/webPush.js +++ b/lib/webPush.js @@ -46,7 +46,7 @@ const createUserFilter = (tag) => { EARN: 'noteEarning', DEPOSIT: 'noteDeposits', WITHDRAWAL: 'noteWithdrawals', - DAILY_STATS: 'noteDailyStats', + DAILY_STATS: 'noteSatSummary', STREAK: 'noteCowboyHat' } const key = tagMap[tag.split('-')[0]] @@ -393,7 +393,7 @@ export async function notifyStreakLost (userId, streak) { } } -export async function notifyDailyStats (userId, stats) { // WIP +export async function notifySatSummary (userId, stats) { // WIP try { await sendUserNotification(userId, { title: 'WIP sats stats', diff --git a/pages/settings/index.js b/pages/settings/index.js index 69e666fcd..4e82b1bad 100644 --- a/pages/settings/index.js +++ b/pages/settings/index.js @@ -138,7 +138,7 @@ export default function Settings ({ ssrData }) { noteInvites: settings?.noteInvites, noteJobIndicator: settings?.noteJobIndicator, noteCowboyHat: settings?.noteCowboyHat, - noteDailyStats: settings?.noteDailyStats, + noteSatSummary: settings?.noteSatSummary, noteForwardedSats: settings?.noteForwardedSats, hideInvoiceDesc: settings?.hideInvoiceDesc, autoDropBolt11s: settings?.autoDropBolt11s, @@ -333,7 +333,7 @@ export default function Settings ({ ssrData }) { /> Date: Mon, 23 Dec 2024 14:34:51 +0100 Subject: [PATCH 04/11] hasNewNotes now supports noteSatSummary; web push notifications placeholder --- api/resolvers/user.js | 15 ++++++++------- worker/index.js | 2 ++ worker/satSummary.js | 23 +++++++++++++++++++++++ worker/satsummary.js | 14 -------------- 4 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 worker/satSummary.js delete mode 100644 worker/satsummary.js diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 19f360ac8..c1fabe55f 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -524,13 +524,14 @@ export default { } if (user.noteSatSummary) { - const satSummary = await models.satSummary.findFirst({ - where: { - id: me.id - } - }) - - if (satSummary) { + 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 } diff --git a/worker/index.js b/worker/index.js index 16d48c59c..7d19be691 100644 --- a/worker/index.js +++ b/worker/index.js @@ -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 { summarizeDailySats } from './satSummary' import { nip57 } from './nostr' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from '@/lib/lnd' @@ -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('satSummary', jobWrapper(summarizeDailySats)) await boss.work('nip57', jobWrapper(nip57)) await boss.work('views-*', jobWrapper(views)) await boss.work('rankViews', jobWrapper(rankViews)) diff --git a/worker/satSummary.js b/worker/satSummary.js new file mode 100644 index 000000000..dda8b52b3 --- /dev/null +++ b/worker/satSummary.js @@ -0,0 +1,23 @@ +import { notifySatSummary } from '@/lib/webPush' +export async function summarizeDailySats ({ data: { userId }, models }) { + try { + const stats = await models.$queryRaw` + SELECT + sum(msats_stacked) as stacked, sum(msats_spent) as spent, + FROM user_stats_days + WHERE id = ${userId} + AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day') + AND t <= date_trunc('day', CURRENT_DATE) + LIMIT 1 + ` + + if (stats.length) { + await notifySatSummary({ + userId, + satSummary: stats[0] + }) + } + } catch (err) { + console.error('failed to process daily stats', err) + } +} diff --git a/worker/satsummary.js b/worker/satsummary.js deleted file mode 100644 index d4c19f187..000000000 --- a/worker/satsummary.js +++ /dev/null @@ -1,14 +0,0 @@ -import { notifySatSummary } from '@/lib/webPush' -export async function summarizeDailySats ({ data: { userId }, models }) { - let satSummary - try { - satSummary = await models.satSummary.findUnique({ where: { id: userId } }) - } catch (err) { - console.error('failed to lookup daily stats by user', err) - } - try { - await notifySatSummary({ userId, satSummary }) - } catch (err) { - console.error('failed to send push notification for daily stats', err) - } -} From 6e4e4ff637e6a42a9d6188151cea79b1c4ef22a2 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Mon, 23 Dec 2024 14:58:38 +0100 Subject: [PATCH 05/11] enhance: slimmer query, satsummary notification links to full satistics --- api/resolvers/notifications.js | 16 +++++++--------- components/notifications.js | 9 ++++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 5b2928d59..2c2f8130b 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -364,15 +364,13 @@ export default { if (meFull.noteSatSummary) { queries.push( - `(SELECT CONCAT('stats_', date_trunc('day', t))::text as id, - t AS "sortTime", - NULL as "earnedSats", - 'SatSummary' AS type + `(SELECT 'stats_' || date_trunc('day', t)::text as id, + t AS "sortTime", NULL as "earnedSats", 'SatSummary' AS type FROM user_stats_days - WHERE "user_stats_days"."id" = $1 + WHERE id = $1 AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day') AND t <= $2 - GROUP BY t, msats_stacked, msats_spent + GROUP BY t ORDER BY "sortTime" DESC LIMIT ${LIMIT})` ) @@ -505,14 +503,14 @@ export default { }, SatSummary: { date: async (n, args, { models }) => { - return new Date(n.id.replace('stats_', '')) + return new Date(n.sortTime) }, stacked: async (n, args, { me, models }) => { // msats_rewards is already counted in msats_stacked const [{ stacked }] = await models.$queryRaw` SELECT sum(msats_stacked) as stacked FROM user_stats_days WHERE id = ${Number(me.id)} - AND t = ${new Date(n.id.replace('stats_', ''))}::timestamp + AND t = ${n.sortTime}::timestamp ` return (stacked && msatsToSats(stacked)) || 0 }, @@ -521,7 +519,7 @@ export default { SELECT sum(msats_spent) as spent FROM user_stats_days WHERE id = ${Number(me.id)} - AND t = ${new Date(n.id.replace('stats_', ''))}::timestamp + AND t = ${n.sortTime}::timestamp ` return (spent && msatsToSats(spent)) || 0 } diff --git a/components/notifications.js b/components/notifications.js index a60b20502..39d3811ec 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -13,7 +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 JusticeIcon from '@/svgs/scales-of-justice.svg' // WIP +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' @@ -164,7 +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 {} + if (type === 'SatSummary') return { href: '/satistics?inc=stacked,spent' } if (type === 'TerritoryTransfer') return { href: `/~${n.sub.name}` } if (!n.item) return {} @@ -203,11 +203,10 @@ function Streak ({ n }) { ) } -function SatSummary ({ n }) { // WIP - // stacked and spent +function SatSummary ({ n }) { return (
-
+
you stacked {numWithUnits(n.stacked, { abbreviate: false })} and spent {numWithUnits(n.spent, { abbreviate: false })}
on {dayMonthYear(new Date(n.date))}
From 379566819ad7607d4941b4bbeb5d8d6f900a6aaf Mon Sep 17 00:00:00 2001 From: Soxasora Date: Mon, 23 Dec 2024 16:13:56 +0100 Subject: [PATCH 06/11] 2 days interval workaround for different timezones; restore svg --- api/resolvers/notifications.js | 4 ++-- svgs/lightning.svg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 2c2f8130b..3fa2bae8e 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -368,11 +368,11 @@ export default { t AS "sortTime", NULL as "earnedSats", 'SatSummary' AS type FROM user_stats_days WHERE id = $1 - AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day') + AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '2 day') AND t <= $2 GROUP BY t ORDER BY "sortTime" DESC - LIMIT ${LIMIT})` + LIMIT 1)` ) } diff --git a/svgs/lightning.svg b/svgs/lightning.svg index b2bfa8af0..78af8e0af 100644 --- a/svgs/lightning.svg +++ b/svgs/lightning.svg @@ -1,3 +1,3 @@ - + From b73a0cd62acbe33063ec7e044fb31430831467ee Mon Sep 17 00:00:00 2001 From: Soxasora Date: Mon, 23 Dec 2024 19:04:11 +0100 Subject: [PATCH 07/11] push notifications, add dailySatSummary to pgboss scheduler --- lib/webPush.js | 2 +- .../20241223184742_satsummary_job/migration.sql | 16 ++++++++++++++++ worker/index.js | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20241223184742_satsummary_job/migration.sql diff --git a/lib/webPush.js b/lib/webPush.js index b0123ea1b..348011dd4 100644 --- a/lib/webPush.js +++ b/lib/webPush.js @@ -396,7 +396,7 @@ export async function notifyStreakLost (userId, streak) { export async function notifySatSummary (userId, stats) { // WIP try { await sendUserNotification(userId, { - title: 'WIP sats stats', + title: 'your daily sat summary is ready', body: `you stacked ${numWithUnits(stats.stacked, { abbreviate: false })} and spent ${numWithUnits(stats.spent, { abbreviate: false })}`, tag: 'DAILY_STATS' }) diff --git a/prisma/migrations/20241223184742_satsummary_job/migration.sql b/prisma/migrations/20241223184742_satsummary_job/migration.sql new file mode 100644 index 000000000..84e81725f --- /dev/null +++ b/prisma/migrations/20241223184742_satsummary_job/migration.sql @@ -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; diff --git a/worker/index.js b/worker/index.js index 7d19be691..8978a5d95 100644 --- a/worker/index.js +++ b/worker/index.js @@ -132,7 +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('satSummary', jobWrapper(summarizeDailySats)) + await boss.work('dailySatSummary', jobWrapper(summarizeDailySats)) await boss.work('nip57', jobWrapper(nip57)) await boss.work('views-*', jobWrapper(views)) await boss.work('rankViews', jobWrapper(rankViews)) From fb61520155938494bbedd434c0e46e0cf3b79228 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Mon, 23 Dec 2024 19:18:57 +0100 Subject: [PATCH 08/11] cleanup: typo, old comments --- api/resolvers/notifications.js | 2 +- api/typeDefs/user.js | 2 +- lib/webPush.js | 2 +- worker/satSummary.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 3fa2bae8e..fe5a45112 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -505,7 +505,7 @@ export default { date: async (n, args, { models }) => { return new Date(n.sortTime) }, - stacked: async (n, args, { me, models }) => { // msats_rewards is already counted in msats_stacked + stacked: async (n, args, { me, models }) => { const [{ stacked }] = await models.$queryRaw` SELECT sum(msats_stacked) as stacked FROM user_stats_days diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 0b3504383..a2806f278 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -94,7 +94,7 @@ export default gql` nostrPubkey: String nostrRelays: [String!] noteAllDescendants: Boolean! - noteSatSummary: Boolean + noteSatSummary: Boolean! noteCowboyHat: Boolean! noteDeposits: Boolean!, noteWithdrawals: Boolean!, diff --git a/lib/webPush.js b/lib/webPush.js index 348011dd4..404732b9d 100644 --- a/lib/webPush.js +++ b/lib/webPush.js @@ -393,7 +393,7 @@ export async function notifyStreakLost (userId, streak) { } } -export async function notifySatSummary (userId, stats) { // WIP +export async function notifySatSummary (userId, stats) { try { await sendUserNotification(userId, { title: 'your daily sat summary is ready', diff --git a/worker/satSummary.js b/worker/satSummary.js index dda8b52b3..e591c5283 100644 --- a/worker/satSummary.js +++ b/worker/satSummary.js @@ -18,6 +18,6 @@ export async function summarizeDailySats ({ data: { userId }, models }) { }) } } catch (err) { - console.error('failed to process daily stats', err) + console.error('failed to process daily sat summary', err) } } From 82553a22b9b48ed67ad3bc034586a79030fa51b6 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Mon, 23 Dec 2024 20:10:43 +0100 Subject: [PATCH 09/11] hotfix: if stacked and spent are 0, don't show/push notification --- api/resolvers/notifications.js | 1 + worker/satSummary.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index fe5a45112..d2fe5a28a 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -371,6 +371,7 @@ export default { 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)` ) diff --git a/worker/satSummary.js b/worker/satSummary.js index e591c5283..0f179731f 100644 --- a/worker/satSummary.js +++ b/worker/satSummary.js @@ -8,6 +8,8 @@ export async function summarizeDailySats ({ data: { userId }, models }) { WHERE id = ${userId} AND 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 LIMIT 1 ` From 421772ecd07f3b9e20341714b2921f5b993db985 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Tue, 24 Dec 2024 12:46:14 +0100 Subject: [PATCH 10/11] hotfix: new push notification query, adjusted names and props --- lib/webPush.js | 4 ++-- worker/index.js | 4 ++-- worker/satSummary.js | 18 +++++++++--------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/webPush.js b/lib/webPush.js index 404732b9d..537925b70 100644 --- a/lib/webPush.js +++ b/lib/webPush.js @@ -393,11 +393,11 @@ export async function notifyStreakLost (userId, streak) { } } -export async function notifySatSummary (userId, stats) { +export async function notifySatSummary (userId, stacked, spent) { try { await sendUserNotification(userId, { title: 'your daily sat summary is ready', - body: `you stacked ${numWithUnits(stats.stacked, { abbreviate: false })} and spent ${numWithUnits(stats.spent, { abbreviate: false })}`, + body: `you stacked ${numWithUnits(stacked, { abbreviate: false })} and spent ${numWithUnits(spent, { abbreviate: false })}`, tag: 'DAILY_STATS' }) } catch (err) { diff --git a/worker/index.js b/worker/index.js index 8978a5d95..c21c9e7d3 100644 --- a/worker/index.js +++ b/worker/index.js @@ -14,7 +14,7 @@ import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client' import { indexItem, indexAllItems } from './search' import { timestampItem } from './ots' import { computeStreaks, checkStreak } from './streak' -import { summarizeDailySats } from './satSummary' +import { dailySatSummary } from './satSummary' import { nip57 } from './nostr' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from '@/lib/lnd' @@ -132,7 +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(summarizeDailySats)) + await boss.work('dailySatSummary', jobWrapper(dailySatSummary)) await boss.work('nip57', jobWrapper(nip57)) await boss.work('views-*', jobWrapper(views)) await boss.work('rankViews', jobWrapper(rankViews)) diff --git a/worker/satSummary.js b/worker/satSummary.js index 0f179731f..ee85fcb39 100644 --- a/worker/satSummary.js +++ b/worker/satSummary.js @@ -1,23 +1,23 @@ import { notifySatSummary } from '@/lib/webPush' -export async function summarizeDailySats ({ data: { userId }, models }) { +export async function dailySatSummary ({ models }) { try { const stats = await models.$queryRaw` SELECT - sum(msats_stacked) as stacked, sum(msats_spent) as spent, + id as userId, sum(msats_stacked) as stacked, sum(msats_spent) as spent FROM user_stats_days - WHERE id = ${userId} - AND t >= date_trunc('day', CURRENT_DATE - INTERVAL '1 day') + 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 - LIMIT 1 ` if (stats.length) { - await notifySatSummary({ - userId, - satSummary: stats[0] - }) + for (const stat of stats) { + const user = await models.user.findUnique({ where: { id: stat.userid } }) + if (user && user.noteSatSummary) { + await notifySatSummary(stat.userid, stat.stacked || 0, stat.spent || 0) + } + } } } catch (err) { console.error('failed to process daily sat summary', err) From 0bc187223b91d2b00b0d12587774ca4529a6d0b7 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Tue, 24 Dec 2024 13:52:25 +0100 Subject: [PATCH 11/11] hotfix: convert msats to sats for push notification --- worker/satSummary.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worker/satSummary.js b/worker/satSummary.js index ee85fcb39..6ec133256 100644 --- a/worker/satSummary.js +++ b/worker/satSummary.js @@ -1,4 +1,5 @@ import { notifySatSummary } from '@/lib/webPush' +import { msatsToSats } from '@/lib/format' export async function dailySatSummary ({ models }) { try { const stats = await models.$queryRaw` @@ -15,7 +16,11 @@ export async function dailySatSummary ({ models }) { for (const stat of stats) { const user = await models.user.findUnique({ where: { id: stat.userid } }) if (user && user.noteSatSummary) { - await notifySatSummary(stat.userid, stat.stacked || 0, stat.spent || 0) + await notifySatSummary( + stat.userid, + (stats.stacked && msatsToSats(stats.stacked)) || 0, + (stats.spent && msatsToSats(stats.spent)) || 0 + ) } } }