This commit is contained in:
keyan 2022-03-17 15:13:19 -05:00
parent 8c513e93c8
commit 5ff856d061
11 changed files with 192 additions and 42 deletions

View File

@ -112,6 +112,13 @@ export default {
AND "maxBid" IS NOT NULL AND "maxBid" IS NOT NULL
AND status <> 'STOPPED' AND status <> 'STOPPED'
AND "statusUpdatedAt" <= $2) AND "statusUpdatedAt" <= $2)
UNION ALL
(SELECT "Earn".id::text, "Earn".created_at AS "sortTime", FLOOR(msats / 1000) as "earnedSats",
'Earn' AS type
FROM "Earn"
WHERE "Earn"."userId" = $1 AND
FLOOR(msats / 1000) > 0
AND created_at <= $2)
ORDER BY "sortTime" DESC ORDER BY "sortTime" DESC
OFFSET $3 OFFSET $3
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset) LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)

View File

@ -160,6 +160,7 @@ export default {
JOIN "Item" on "ItemAct"."itemId" = "Item".id JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct"."userId" <> ${user.id} AND "ItemAct".act <> 'BOOST' WHERE "ItemAct"."userId" <> ${user.id} AND "ItemAct".act <> 'BOOST'
AND "Item"."userId" = ${user.id}` AND "Item"."userId" = ${user.id}`
return sum || 0 return sum || 0
}, },
sats: async (user, args, { models, me }) => { sats: async (user, args, { models, me }) => {
@ -217,23 +218,33 @@ export default {
return true return true
} }
const where = { const job = await models.item.findFirst({
status: { where: {
not: 'STOPPED' status: {
}, not: 'STOPPED'
maxBid: { },
not: null maxBid: {
}, not: null
userId: user.id },
userId: user.id,
statusUpdatedAt: {
gt: user.checkedNotesAt || new Date(0)
}
}
})
if (job) {
return true
} }
if (user.checkedNotesAt) { const earn = await models.earn.findFirst({
where.statusUpdatedAt = { where: {
gt: user.checkedNotesAt userId: user.id,
createdAt: {
gt: user.checkedNotesAt || new Date(0)
}
} }
} })
const job = await models.item.findFirst({ where }) if (earn) {
if (job) {
return true return true
} }

View File

@ -97,6 +97,12 @@ export default {
WHERE "ItemAct"."userId" <> $1 AND "ItemAct".act <> 'BOOST' WHERE "ItemAct"."userId" <> $1 AND "ItemAct".act <> 'BOOST'
AND "Item"."userId" = $1 AND "ItemAct".created_at <= $2 AND "Item"."userId" = $1 AND "ItemAct".created_at <= $2
GROUP BY "Item".id)`) GROUP BY "Item".id)`)
queries.push(
`(SELECT ('earn' || "Earn".id) as id, "Earn".id as "factId", NULL as bolt11,
created_at as "createdAt", msats,
0 as "msatsFee", NULL as status, 'earn' as type
FROM "Earn"
WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2)`)
} }
if (include.has('spent')) { if (include.has('spent')) {

View File

@ -32,7 +32,13 @@ export default gql`
sortTime: String! sortTime: String!
} }
union Notification = Reply | Votification | Mention | Invitification | JobChanged type Earn {
earnedSats: Int!
sortTime: String!
}
union Notification = Reply | Votification | Mention
| Invitification | JobChanged | Earn
type Notifications { type Notifications {
lastChecked: String lastChecked: String

View File

@ -6,6 +6,7 @@ import { useRouter } from 'next/router'
import MoreFooter from './more-footer' import MoreFooter from './more-footer'
import Invite from './invite' import Invite from './invite'
import { ignoreClick } from '../lib/clicks' import { ignoreClick } from '../lib/clicks'
import Link from 'next/link'
function Notification ({ n }) { function Notification ({ n }) {
const router = useRouter() const router = useRouter()
@ -13,6 +14,10 @@ function Notification ({ n }) {
<div <div
className='clickToContext' className='clickToContext'
onClick={e => { onClick={e => {
if (n.__typename === 'Earn') {
return
}
if (ignoreClick(e)) { if (ignoreClick(e)) {
return return
} }
@ -48,33 +53,44 @@ function Notification ({ n }) {
</div> </div>
</> </>
) )
: ( : n.__typename === 'Earn'
<> ? (
{n.__typename === 'Votification' && <>
<small className='font-weight-bold text-success ml-2'> <div className='font-weight-bold text-boost ml-2'>
your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats you stacked {n.earnedSats} sats
</small>} </div>
{n.__typename === 'Mention' && <div className='ml-4'>
<small className='font-weight-bold text-info ml-2'> SN distributes the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boost, and posting fees.
you were mentioned in </div>
</small>} </>
{n.__typename === 'JobChanged' && )
<small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}> : (
{n.item.status === 'NOSATS' <>
? 'your job ran out of sats' {n.__typename === 'Votification' &&
: 'your job is active again'} <small className='font-weight-bold text-success ml-2'>
</small>} your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}> </small>}
{n.item.maxBid {n.__typename === 'Mention' &&
? <ItemJob item={n.item} /> <small className='font-weight-bold text-info ml-2'>
: n.item.title you were mentioned in
? <Item item={n.item} /> </small>}
: ( {n.__typename === 'JobChanged' &&
<div className='pb-2'> <small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}>
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext /> {n.item.status === 'NOSATS'
</div>)} ? 'your job ran out of sats'
</div> : 'your job is active again'}
</>)} </small>}
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
{n.item.maxBid
? <ItemJob item={n.item} />
: n.item.title
? <Item item={n.item} />
: (
<div className='pb-2'>
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
</div>)}
</div>
</>)}
</div> </div>
) )
} }

View File

@ -47,6 +47,10 @@ export const NOTIFICATIONS = gql`
...ItemFields ...ItemFields
} }
} }
... on Earn {
sortTime
earnedSats
}
} }
} }
} ` } `

View File

@ -88,6 +88,15 @@ function Satus ({ status }) {
} }
function Detail ({ fact }) { function Detail ({ fact }) {
if (fact.type === 'earn') {
return (
<>
<div className={satusClass(fact.status)}>
SN gives the sats it earns back to its best users daily. These sats come from <Link href='/~jobs' passHref><a>jobs</a></Link>, boost, and posting fees.
</div>
</>
)
}
if (!fact.item) { if (!fact.item) {
return ( return (
<> <>

View File

@ -0,0 +1,31 @@
-- CreateTable
CREATE TABLE "Earn" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"msats" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Earn.created_at_index" ON "Earn"("created_at");
-- CreateIndex
CREATE INDEX "Earn.userId_index" ON "Earn"("userId");
-- AddForeignKey
ALTER TABLE "Earn" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- charge the user for the auction item
CREATE OR REPLACE FUNCTION earn(user_id INTEGER, earn_msats INTEGER) RETURNS void AS $$
DECLARE
BEGIN
PERFORM ASSERT_SERIALIZED();
-- insert into earn
INSERT INTO "Earn" (msats, "userId") VALUES (earn_msats, user_id);
-- give the user the sats
UPDATE users SET msats = msats + earn_msats WHERE id = user_id;
END;
$$ LANGUAGE plpgsql;

View File

@ -41,11 +41,25 @@ model User {
upvotePopover Boolean @default(false) upvotePopover Boolean @default(false)
tipPopover Boolean @default(false) tipPopover Boolean @default(false)
Earn Earn[]
@@index([createdAt]) @@index([createdAt])
@@index([inviteId]) @@index([inviteId])
@@map(name: "users") @@map(name: "users")
} }
model Earn {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
msats Int
user User @relation(fields: [userId], references: [id])
userId Int
@@index([createdAt])
@@index([userId])
}
model LnAuth { model LnAuth {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at") createdAt DateTime @default(now()) @map(name: "created_at")

44
worker/earn.js Normal file
View File

@ -0,0 +1,44 @@
const serialize = require('../api/resolvers/serial')
// TODO: use a weekly trust measure or make trust decay
function earn ({ models }) {
return async function ({ name }) {
console.log('running', name)
// compute how much sn earned today
const [{ sum }] = await models.$queryRaw`
SELECT sum("ItemAct".sats)
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE ("ItemAct".act in ('BOOST', 'STREAM')
OR ("ItemAct".act = 'VOTE' AND "Item"."userId" = "ItemAct"."userId"))
AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'`
// calculate the total trust
const { sum: { trust } } = await models.user.aggregate({
sum: {
trust: true
}
})
// get earners { id, earnings }
const earners = await models.$queryRaw(`
SELECT id, FLOOR(${sum} * (trust/${trust}) * 1000) as earnings
FROM users
WHERE trust > 0`)
// for each earner, serialize earnings
// we do this for each earner because we don't need to serialize
// all earner updates together
earners.forEach(async earner => {
if (earner.earnings > 0) {
await serialize(models,
models.$executeRaw`SELECT earn(${earner.id}, ${earner.earnings})`)
}
})
console.log('done', name)
}
}
module.exports = { earn }

View File

@ -6,6 +6,7 @@ const { checkInvoice, checkWithdrawal } = require('./wallet')
const { repin } = require('./repin') const { repin } = require('./repin')
const { trust } = require('./trust') const { trust } = require('./trust')
const { auction } = require('./auction') const { auction } = require('./auction')
const { earn } = require('./earn')
const { ApolloClient, HttpLink, InMemoryCache } = require('@apollo/client') const { ApolloClient, HttpLink, InMemoryCache } = require('@apollo/client')
const { indexItem, indexAllItems } = require('./search') const { indexItem, indexAllItems } = require('./search')
const fetch = require('cross-fetch') const fetch = require('cross-fetch')
@ -43,6 +44,7 @@ async function work () {
await boss.work('indexItem', indexItem(args)) await boss.work('indexItem', indexItem(args))
await boss.work('indexAllItems', indexAllItems(args)) await boss.work('indexAllItems', indexAllItems(args))
await boss.work('auction', auction(args)) await boss.work('auction', auction(args))
await boss.work('earn', earn(args))
console.log('working jobs') console.log('working jobs')
} }