Skip to content

Commit

Permalink
Merge branch 'master' into opensearch-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
huumn authored Sep 24, 2023
2 parents 003c41e + 8017355 commit ee851d2
Show file tree
Hide file tree
Showing 67 changed files with 1,407 additions and 365 deletions.
35 changes: 27 additions & 8 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,27 @@ TWITTER_SECRET=<YOUR TWITTER SECRET>
LOGIN_EMAIL_SERVER=smtp://<YOUR EMAIL>:<YOUR PASSWORD>@<YOUR SMTP DOMAIN>:587
LOGIN_EMAIL_FROM=<YOUR FROM ALIAS>

# lnurl-auth
LNAUTH_URL=<YOUR PUBLIC TUNNEL TO LOCALHOST, e.g. NGROK>
#####################################################################
# OTHER / OPTIONAL #
# configuration for push notifications, slack and imgproxy are here #
#####################################################################

# VAPID for Web Push
VAPID_MAILTO=
NEXT_PUBLIC_VAPID_PUBKEY=
VAPID_PRIVKEY=

# slack
SLACK_BOT_TOKEN=
SLACK_CHANNEL_ID=

# imgproxy
NEXT_PUBLIC_IMGPROXY_URL=
IMGPROXY_KEY=
IMGPROXY_SALT=

#######################################################
# LND / OPTIONAL #
# WALLET / OPTIONAL #
# if you want to work with payments you'll need these #
#######################################################

Expand All @@ -33,6 +44,13 @@ LND_CERT=<YOUR LND HEX CERT>
LND_MACAROON=<YOUR LND HEX MACAROON>
LND_SOCKET=<YOUR LND GRPC HOST>:<YOUR LND GRPC PORT>

# lnurl
LNAUTH_URL=<PUBLIC URL TO /api/lnauth>
LNWITH_URL=<PUBLIC URL TO /api/lnwith>

# nostr (NIP-57 zap receipts)
NOSTR_PRIVATE_KEY=<YOUR NOSTR PRIVATE KEY IN HEX>

###############
# LEAVE AS IS #
###############
Expand All @@ -51,10 +69,7 @@ OPENSEARCH_URL=http://opensearch:9200
OPENSEARCH_USERNAME=
OPENSEARCH_PASSWORD=

# imgproxy
NEXT_PUBLIC_IMGPROXY_URL=
IMGPROXY_KEY=
IMGPROXY_SALT=
# imgproxy options
IMGPROXY_ENABLE_WEBP_DETECTION=1
IMGPROXY_MAX_ANIMATION_FRAMES=100
IMGPROXY_MAX_SRC_RESOLUTION=200
Expand All @@ -70,4 +85,8 @@ DATABASE_URL="postgresql://sn:password@db:5432/stackernews?schema=public"
# postgres container stuff
POSTGRES_PASSWORD=password
POSTGRES_USER=sn
POSTGRES_DB=stackernews
POSTGRES_DB=stackernews

# slack
SLACK_BOT_TOKEN=
SLACK_CHANNEL_ID=
3 changes: 3 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
10 changes: 7 additions & 3 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export async function commentFilterClause (me, models) {
return clause
}

async function checkInvoice (models, hash, hmac, fee) {
export async function checkInvoice (models, hash, hmac, fee) {
if (!hash) {
throw new GraphQLError('hash required', { extensions: { code: 'BAD_INPUT' } })
}
if (!hmac) {
throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
}
Expand Down Expand Up @@ -1203,15 +1206,16 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo

const notifyUserSubscribers = async () => {
try {
const isPost = !!item.title
const userSubs = await models.userSubscription.findMany({
where: {
followeeId: Number(item.userId)
followeeId: Number(item.userId),
[isPost ? 'postsSubscribedAt' : 'commentsSubscribedAt']: { not: null }
},
include: {
followee: true
}
})
const isPost = !!item.title
await Promise.allSettled(userSubs.map(({ followerId, followee }) => sendUserNotification(followerId, {
title: `@${followee.name} ${isPost ? 'created a post' : 'replied to a post'}`,
body: isPost ? item.title : item.text,
Expand Down
9 changes: 6 additions & 3 deletions api/resolvers/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,11 @@ export default {
JOIN "UserSubscription" ON "Item"."userId" = "UserSubscription"."followeeId"
WHERE "UserSubscription"."followerId" = $1
AND "Item".created_at <= $2
-- Only show items that have been created since subscribing to the user
AND "Item".created_at >= "UserSubscription".created_at
AND (
-- Only include posts or comments created after the corresponding subscription was enabled, not _all_ from history
("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt")
)
${await filterClause(me, models)}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
Expand Down Expand Up @@ -202,7 +205,7 @@ export default {
FROM "Invoice"
WHERE "Invoice"."userId" = $1
AND "confirmedAt" IS NOT NULL
AND NOT "isHeld"
AND "isHeld" IS NULL
AND created_at <= $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
Expand Down
42 changes: 32 additions & 10 deletions api/resolvers/rewards.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { GraphQLError } from 'graphql'
import { settleHodlInvoice } from 'ln-service'
import { amountSchema, ssValidate } from '../../lib/validate'
import serialize from './serial'
import { ANON_USER_ID } from '../../lib/constants'
import { getItem } from './item'
import { getItem, checkInvoice } from './item'

export default {
Query: {
Expand All @@ -25,8 +26,8 @@ export default {
WITH days_cte (day) AS (
SELECT date_trunc('day', t)
FROM generate_series(
COALESCE(${when?.[0]}::timestamp - interval '1 day', now() AT TIME ZONE 'America/Chicago'),
COALESCE(${when?.[when.length - 1]}::timestamp - interval '1 day', now() AT TIME ZONE 'America/Chicago'),
COALESCE(${when?.[0]}::text::timestamp - interval '1 day', now() AT TIME ZONE 'America/Chicago'),
COALESCE(${when?.[when.length - 1]}::text::timestamp - interval '1 day', now() AT TIME ZONE 'America/Chicago'),
interval '1 day') AS t
)
SELECT coalesce(FLOOR(sum(sats)), 0) as total,
Expand Down Expand Up @@ -82,8 +83,8 @@ export default {
WITH days_cte (day) AS (
SELECT date_trunc('day', t)
FROM generate_series(
${when[0]}::timestamp,
${when[when.length - 1]}::timestamp,
${when[0]}::text::timestamp,
${when[when.length - 1]}::text::timestamp,
interval '1 day') AS t
)
SELECT coalesce(sum(sats), 0) as total, json_agg("Earn".*) as rewards
Expand All @@ -102,17 +103,38 @@ export default {
}
},
Mutation: {
donateToRewards: async (parent, { sats }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
donateToRewards: async (parent, { sats, hash, hmac }, { me, models, lnd }) => {
if (!me && !hash) {
throw new GraphQLError('you must be logged in or pay', { extensions: { code: 'FORBIDDEN' } })
}
let user
if (me) {
user = me
}

let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac, sats)
user = invoice.user
}

await ssValidate(amountSchema, { amount: sats })

await serialize(models,
const trx = [
models.$queryRawUnsafe(
'SELECT donate($1::INTEGER, $2::INTEGER)',
sats, Number(me.id)))
sats, Number(user.id))
]

if (invoice) {
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${user.id}`)
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
}
await serialize(models, ...trx)

if (invoice?.isHeld) {
await settleHodlInvoice({ secret: invoice.preimage, lnd })
}

return sats
}
Expand Down
8 changes: 4 additions & 4 deletions api/resolvers/serial.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { GraphQLError } = require('graphql')
const retry = require('async-retry')
const Prisma = require('@prisma/client')
import { GraphQLError } from 'graphql'
import retry from 'async-retry'
import Prisma from '@prisma/client'

async function serialize (models, ...calls) {
return await retry(async bail => {
Expand Down Expand Up @@ -56,4 +56,4 @@ async function serialize (models, ...calls) {
})
}

module.exports = serialize
export default serialize
77 changes: 65 additions & 12 deletions api/resolvers/user.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { readFile } from 'fs/promises'
import { join, resolve } from 'path'
import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { msatsToSats } from '../../lib/format'
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
import { getItem, updateItem, filterClause, createItem } from './item'
import { datePivot } from '../../lib/time'

const contributors = new Set()

const loadContributors = async (set) => {
try {
const fileContent = await readFile(resolve(join(process.cwd(), 'contributors.txt')), 'utf-8')
fileContent.split('\n')
.map(line => line.trim())
.filter(line => !!line)
.forEach(name => set.add(name))
} catch (err) {
console.error('Error loading contributors', err)
}
}

export function within (table, within) {
let interval = ' AND "' + table + '".created_at >= $1 - INTERVAL '
switch (within) {
Expand Down Expand Up @@ -323,6 +339,10 @@ export default {
WHERE
"UserSubscription"."followerId" = $1
AND "Item".created_at > $2::timestamp(3) without time zone
AND (
("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt")
)
${await filterClause(me, models)}
LIMIT 1`, me.id, lastChecked)
if (newUserSubs.length > 0) {
Expand Down Expand Up @@ -402,9 +422,7 @@ export default {
confirmedAt: {
gt: lastChecked
},
isHeld: {
not: true
}
isHeld: null
}
})
if (invoice) {
Expand Down Expand Up @@ -588,13 +606,23 @@ export default {

return true
},
subscribeUser: async (parent, { id }, { me, models }) => {
const data = { followerId: Number(me.id), followeeId: Number(id) }
const old = await models.userSubscription.findUnique({ where: { followerId_followeeId: data } })
if (old) {
await models.userSubscription.delete({ where: { followerId_followeeId: data } })
subscribeUserPosts: async (parent, { id }, { me, models }) => {
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
if (existing) {
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { postsSubscribedAt: existing.postsSubscribedAt ? null : new Date() } })
} else {
await models.userSubscription.create({ data: { ...lookupData, postsSubscribedAt: new Date() } })
}
return { id }
},
subscribeUserComments: async (parent, { id }, { me, models }) => {
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
if (existing) {
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { commentsSubscribedAt: existing.commentsSubscribedAt ? null : new Date() } })
} else {
await models.userSubscription.create({ data })
await models.userSubscription.create({ data: { ...lookupData, commentsSubscribedAt: new Date() } })
}
return { id }
},
Expand Down Expand Up @@ -774,9 +802,9 @@ export default {

return relays?.map(r => r.nostrRelayAddr)
},
meSubscription: async (user, args, { me, models }) => {
meSubscriptionPosts: async (user, args, { me, models }) => {
if (!me) return false
if (typeof user.meSubscription !== 'undefined') return user.meSubscription
if (typeof user.meSubscriptionPosts !== 'undefined') return user.meSubscriptionPosts

const subscription = await models.userSubscription.findUnique({
where: {
Expand All @@ -787,7 +815,32 @@ export default {
}
})

return !!subscription
return !!subscription?.postsSubscribedAt
},
meSubscriptionComments: async (user, args, { me, models }) => {
if (!me) return false
if (typeof user.meSubscriptionComments !== 'undefined') return user.meSubscriptionComments

const subscription = await models.userSubscription.findUnique({
where: {
followerId_followeeId: {
followerId: Number(me.id),
followeeId: Number(user.id)
}
}
})

return !!subscription?.commentsSubscribedAt
},
isContributor: async (user, args, { me }) => {
// lazy init contributors only once
if (contributors.size === 0) {
await loadContributors(contributors)
}
if (me?.id === user.id) {
return contributors.has(user.name)
}
return !user.hideIsContributor && contributors.has(user.name)
}
}
}
Loading

0 comments on commit ee851d2

Please sign in to comment.