stacker.news/api/resolvers/item.js

1184 lines
40 KiB
JavaScript
Raw Normal View History

import { GraphQLError } from 'graphql'
2022-11-15 23:51:00 +00:00
import { ensureProtocol, removeTracking } from '../../lib/url'
2021-05-20 01:09:32 +00:00
import serialize from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
2021-08-22 15:25:17 +00:00
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino'
2022-09-21 19:57:36 +00:00
import {
2023-02-08 19:38:04 +00:00
BOOST_MIN, ITEM_SPAM_INTERVAL,
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD,
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL
2022-09-21 19:57:36 +00:00
} from '../../lib/constants'
import { msatsToSats, numWithUnits } from '../../lib/format'
2023-01-11 22:20:14 +00:00
import { parse } from 'tldts'
2023-01-12 18:05:47 +00:00
import uu from 'url-unshort'
2023-02-08 19:38:04 +00:00
import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
import { sendUserNotification } from '../webPush'
2023-07-13 20:18:04 +00:00
import { proxyImages } from './imgproxy'
import { defaultCommentSort } from '../../lib/item'
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
import { createHmac } from './wallet'
export async function commentFilterClause (me, models) {
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
if (me) {
const user = await models.user.findUnique({ where: { id: me.id } })
// wild west mode has everything
if (user.wildWestMode) {
return ''
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${me.id}`
}
// close the clause
clause += ')'
return clause
}
2021-06-22 17:47:49 +00:00
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
async function checkInvoice (models, hash, hmac, fee) {
if (!hmac) {
throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
}
const hmac2 = createHmac(hash)
if (hmac !== hmac2) {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
}
const invoice = await models.invoice.findUnique({
where: { hash },
include: {
user: true
}
})
if (!invoice) {
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
}
if (!invoice.msatsReceived) {
throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
}
if (msatsToSats(invoice.msatsReceived) < fee) {
throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
}
return invoice
}
2023-05-07 01:25:00 +00:00
async function comments (me, models, id, sort) {
2021-12-21 21:29:42 +00:00
let orderBy
switch (sort) {
case 'top':
orderBy = `ORDER BY ${await orderByNumerator(me, models)} DESC, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
2021-12-21 21:29:42 +00:00
break
case 'recent':
orderBy = 'ORDER BY "Item".created_at DESC, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC'
2021-12-21 21:29:42 +00:00
break
default:
orderBy = `ORDER BY ${await orderByNumerator(me, models)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
2021-12-21 21:29:42 +00:00
break
}
const filter = await commentFilterClause(me, models)
2023-05-07 01:25:00 +00:00
if (me) {
2023-07-26 16:01:31 +00:00
const [{ item_comments_with_me: comments }] = await models.$queryRawUnsafe(
2023-07-27 00:18:42 +00:00
'SELECT item_comments_with_me($1::INTEGER, $2::INTEGER, $3::INTEGER, $4, $5)', Number(id), Number(me.id), COMMENT_DEPTH_LIMIT, filter, orderBy)
2023-05-07 01:25:00 +00:00
return comments
}
2023-07-26 16:01:31 +00:00
const [{ item_comments: comments }] = await models.$queryRawUnsafe(
2023-07-27 00:18:42 +00:00
'SELECT item_comments($1::INTEGER, $2::INTEGER, $3, $4)', Number(id), COMMENT_DEPTH_LIMIT, filter, orderBy)
2023-05-06 21:51:17 +00:00
return comments
2021-04-29 15:56:28 +00:00
}
2022-09-21 19:57:36 +00:00
export async function getItem (parent, { id }, { me, models }) {
const [item] = await itemQueryWithMeta({
me,
models,
query: `
${SELECT}
FROM "Item"
WHERE id = $1`
}, Number(id))
2021-09-23 17:42:00 +00:00
return item
}
function whenClause (when, type) {
let interval = ` AND "${type === 'bookmarks' ? 'Bookmark' : 'Item'}".created_at >= $1 - INTERVAL `
switch (when) {
2022-10-25 21:35:32 +00:00
case 'forever':
interval = ''
2021-12-16 23:05:31 +00:00
break
case 'week':
interval += "'7 days'"
break
case 'month':
interval += "'1 month'"
break
case 'year':
interval += "'1 year'"
break
default:
2022-10-25 21:35:32 +00:00
interval += "'1 day'"
2021-12-16 23:05:31 +00:00
break
}
return interval
}
const orderByClause = async (by, me, models, type) => {
switch (by) {
2022-10-25 21:35:32 +00:00
case 'comments':
return 'ORDER BY "Item".ncomments DESC'
2022-10-25 21:35:32 +00:00
case 'sats':
return 'ORDER BY "Item".msats DESC'
case 'votes':
return await topOrderByWeightedSats(me, models)
default:
2023-07-29 23:27:32 +00:00
return `ORDER BY ${type === 'bookmarks' ? '"bookmarkCreatedAt"' : '"Item".created_at'} DESC`
2022-10-25 21:35:32 +00:00
}
}
2022-09-21 19:57:36 +00:00
export async function orderByNumerator (me, models) {
if (me) {
const user = await models.user.findUnique({ where: { id: me.id } })
if (user.wildWestMode) {
return '(GREATEST("Item"."weightedVotes", POWER("Item"."weightedVotes", 1.2)) + "Item"."weightedComments"/2)'
2022-09-21 19:57:36 +00:00
}
}
return `(CASE WHEN "Item"."weightedVotes" > "Item"."weightedDownVotes"
THEN 1
ELSE -1 END
2023-05-09 18:52:35 +00:00
* GREATEST(ABS("Item"."weightedVotes" - "Item"."weightedDownVotes"), POWER(ABS("Item"."weightedVotes" - "Item"."weightedDownVotes"), 1.2))
+ "Item"."weightedComments"/2)`
2022-09-21 19:57:36 +00:00
}
2023-05-23 14:21:04 +00:00
export async function joinSatRankView (me, models) {
if (me) {
const user = await models.user.findUnique({ where: { id: me.id } })
if (user.wildWestMode) {
return 'JOIN sat_rank_wwm_view ON "Item".id = sat_rank_wwm_view.id'
}
}
return 'JOIN sat_rank_tender_view ON "Item".id = sat_rank_tender_view.id'
}
export async function filterClause (me, models, type) {
// if you are explicitly asking for marginal content, don't filter them
if (['outlawed', 'borderland', 'freebies'].includes(type)) {
if (me && ['outlawed', 'borderland'].includes(type)) {
// unless the item is mine
return ` AND "Item"."userId" <> ${me.id} `
}
return ''
}
2022-09-27 21:19:15 +00:00
// by default don't include freebies unless they have upvotes
let clause = ' AND (NOT "Item".freebie OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0'
2022-09-21 19:57:36 +00:00
if (me) {
const user = await models.user.findUnique({ where: { id: me.id } })
2022-09-27 21:19:15 +00:00
// wild west mode has everything
2022-09-21 19:57:36 +00:00
if (user.wildWestMode) {
return ''
}
2022-09-27 21:19:15 +00:00
// greeter mode includes freebies if feebies haven't been flagged
if (user.greeterMode) {
2023-05-06 23:17:47 +00:00
clause = ' AND (NOT "Item".freebie OR ("Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0 AND "Item".freebie)'
2022-09-27 21:19:15 +00:00
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${me.id})`
} else {
// close default freebie clause
clause += ')'
2022-09-21 19:57:36 +00:00
}
// if the item is above the threshold or is mine
2022-09-27 21:19:15 +00:00
clause += ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
2022-09-21 19:57:36 +00:00
if (me) {
clause += ` OR "Item"."userId" = ${me.id}`
}
clause += ')'
return clause
}
function typeClause (type) {
2022-12-01 22:22:13 +00:00
switch (type) {
case 'links':
return ' AND "Item".url IS NOT NULL AND "Item"."parentId" IS NULL'
2022-12-01 22:22:13 +00:00
case 'discussions':
return ' AND "Item".url IS NULL AND "Item".bio = false AND "Item"."pollCost" IS NULL AND "Item"."parentId" IS NULL'
2022-12-01 22:22:13 +00:00
case 'polls':
return ' AND "Item"."pollCost" IS NOT NULL AND "Item"."parentId" IS NULL'
2022-12-01 22:22:13 +00:00
case 'bios':
return ' AND "Item".bio = true AND "Item"."parentId" IS NULL'
2023-01-26 16:11:55 +00:00
case 'bounties':
return ' AND "Item".bounty IS NOT NULL AND "Item"."parentId" IS NULL'
case 'comments':
return ' AND "Item"."parentId" IS NOT NULL'
case 'freebies':
return ' AND "Item".freebie'
case 'outlawed':
return ` AND "Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD}`
case 'borderland':
return ' AND "Item"."weightedVotes" - "Item"."weightedDownVotes" < 0 '
case 'all':
case 'bookmarks':
return ''
case 'jobs':
return ' AND "Item"."subName" = \'jobs\''
default:
return ' AND "Item"."parentId" IS NULL'
2022-12-01 22:22:13 +00:00
}
}
2023-05-07 01:25:00 +00:00
// this grabs all the stuff we need to display the item list and only
// hits the db once ... orderBy needs to be duplicated on the outer query because
// joining does not preserve the order of the inner query
async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ...args) {
2023-05-07 01:25:00 +00:00
if (!me) {
2023-07-26 16:01:31 +00:00
return await models.$queryRawUnsafe(`
2023-05-07 01:25:00 +00:00
SELECT "Item".*, to_json(users.*) as user
FROM (
${query}
) "Item"
JOIN users ON "Item"."userId" = users.id
${orderBy}`, ...args)
2023-05-07 01:25:00 +00:00
} else {
2023-07-26 16:01:31 +00:00
return await models.$queryRawUnsafe(`
2023-05-07 01:25:00 +00:00
SELECT "Item".*, to_json(users.*) as user, COALESCE("ItemAct"."meMsats", 0) as "meMsats",
2023-07-29 23:27:32 +00:00
COALESCE("ItemAct"."meDontLike", false) as "meDontLike", b."itemId" IS NOT NULL AS "meBookmark",
"ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription"
2023-05-07 01:25:00 +00:00
FROM (
${query}
) "Item"
JOIN users ON "Item"."userId" = users.id
2023-07-29 23:27:32 +00:00
LEFT JOIN "Bookmark" b ON b."itemId" = "Item".id AND b."userId" = ${me.id}
LEFT JOIN "ThreadSubscription" ON "ThreadSubscription"."itemId" = "Item".id AND "ThreadSubscription"."userId" = ${me.id}
2023-05-07 01:25:00 +00:00
LEFT JOIN LATERAL (
SELECT "itemId", sum("ItemAct".msats) FILTER (WHERE act = 'FEE' OR act = 'TIP') AS "meMsats",
bool_or(act = 'DONT_LIKE_THIS') AS "meDontLike"
FROM "ItemAct"
WHERE "ItemAct"."userId" = ${me.id}
AND "ItemAct"."itemId" = "Item".id
GROUP BY "ItemAct"."itemId"
) "ItemAct" ON true
${orderBy}`, ...args)
2023-05-07 01:25:00 +00:00
}
2023-05-06 23:17:47 +00:00
}
2023-05-23 14:21:04 +00:00
const subClause = (sub, num, table, solo) => {
return sub ? ` ${solo ? 'WHERE' : 'AND'} ${table ? `"${table}".` : ''}"subName" = $${num} ` : ''
2023-05-01 20:58:30 +00:00
}
const relationClause = (type) => {
switch (type) {
case 'comments':
return ' FROM "Item" JOIN "Item" root ON "Item"."rootId" = root.id '
case 'bookmarks':
return ' FROM "Item" JOIN "Bookmark" ON "Bookmark"."itemId" = "Item"."id" '
case 'outlawed':
case 'borderland':
case 'freebies':
case 'all':
return ' FROM "Item" LEFT JOIN "Item" root ON "Item"."rootId" = root.id '
default:
return ' FROM "Item" '
}
}
2023-07-29 23:27:32 +00:00
const selectClause = (type) => type === 'bookmarks'
? `${SELECT}, "Bookmark"."created_at" as "bookmarkCreatedAt"`
: SELECT
const subClauseTable = (type) => COMMENT_TYPE_QUERY.includes(type) ? 'root' : 'Item'
const activeOrMine = (me) => {
return me ? ` AND ("Item".status <> 'STOPPED' OR "Item"."userId" = ${me.id}) ` : ' AND "Item".status <> \'STOPPED\' '
}
2021-04-12 18:05:09 +00:00
export default {
Query: {
2022-08-10 15:06:31 +00:00
itemRepetition: async (parent, { parentId }, { me, models }) => {
if (!me) return 0
// how many of the parents starting at parentId belong to me
2023-07-27 00:18:42 +00:00
const [{ item_spam: count }] = await models.$queryRawUnsafe(`SELECT item_spam($1::INTEGER, $2::INTEGER, '${ITEM_SPAM_INTERVAL}')`,
2022-09-01 21:06:11 +00:00
Number(parentId), Number(me.id))
2022-08-10 15:06:31 +00:00
return count
},
items: async (parent, { sub, sort, type, cursor, name, when, by, limit = LIMIT }, { me, models }) => {
2021-06-22 17:47:49 +00:00
const decodedCursor = decodeCursor(cursor)
let items, user, pins, subFull, table
2021-10-26 20:49:37 +00:00
2023-05-05 18:06:53 +00:00
// HACK we want to optionally include the subName in the query
// but the query planner doesn't like unused parameters
const subArr = sub ? [sub] : []
2021-06-24 23:56:01 +00:00
switch (sort) {
case 'user':
2021-10-26 20:49:37 +00:00
if (!name) {
throw new GraphQLError('must supply name', { extensions: { code: 'BAD_INPUT' } })
2021-10-26 20:49:37 +00:00
}
user = await models.user.findUnique({ where: { name } })
if (!user) {
throw new GraphQLError('no user has that name', { extensions: { code: 'BAD_INPUT' } })
2021-10-26 20:49:37 +00:00
}
table = type === 'bookmarks' ? 'Bookmark' : 'Item'
items = await itemQueryWithMeta({
me,
models,
query: `
2023-07-29 23:27:32 +00:00
${selectClause(type)}
${relationClause(type)}
WHERE "${table}"."userId" = $2 AND "${table}".created_at <= $1
${subClause(sub, 5, subClauseTable(type))}
${activeOrMine(me)}
${await filterClause(me, models, type)}
${typeClause(type)}
${whenClause(when || 'forever', type)}
${await orderByClause(by, me, models, type)}
OFFSET $3
LIMIT $4`,
orderBy: await orderByClause(by, me, models, type)
}, decodedCursor.time, user.id, decodedCursor.offset, limit, ...subArr)
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
case 'recent':
items = await itemQueryWithMeta({
me,
models,
query: `
${SELECT}
${relationClause(type)}
WHERE "Item".created_at <= $1
${subClause(sub, 4, subClauseTable(type))}
${activeOrMine(me)}
${await filterClause(me, models, type)}
${typeClause(type)}
ORDER BY "Item".created_at DESC
OFFSET $2
LIMIT $3`,
orderBy: 'ORDER BY "Item"."createdAt" DESC'
}, decodedCursor.time, decodedCursor.offset, limit, ...subArr)
2022-02-17 17:23:43 +00:00
break
case 'top':
items = await itemQueryWithMeta({
me,
models,
query: `
2023-07-29 23:27:32 +00:00
${selectClause(type)}
${relationClause(type)}
WHERE "Item".created_at <= $1
AND "Item"."pinId" IS NULL AND "Item"."deletedAt" IS NULL
${subClause(sub, 4, subClauseTable(type))}
${typeClause(type)}
${whenClause(when, type)}
${await filterClause(me, models, type)}
${await orderByClause(by || 'votes', me, models, type)}
OFFSET $2
LIMIT $3`,
orderBy: await orderByClause(by || 'votes', me, models, type)
}, decodedCursor.time, decodedCursor.offset, limit, ...subArr)
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
default:
// sub so we know the default ranking
if (sub) {
subFull = await models.sub.findUnique({ where: { name: sub } })
}
switch (subFull?.rankingType) {
case 'AUCTION':
items = await itemQueryWithMeta({
me,
models,
query: `
2023-05-08 22:32:37 +00:00
${SELECT},
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
THEN 0 ELSE 1 END AS group_rank,
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
THEN rank() OVER (ORDER BY "maxBid" DESC, created_at ASC)
ELSE rank() OVER (ORDER BY created_at DESC) END AS rank
FROM "Item"
WHERE "parentId" IS NULL AND created_at <= $1
AND "pinId" IS NULL
${subClause(sub, 4)}
2023-05-08 22:32:37 +00:00
AND status IN ('ACTIVE', 'NOSATS')
ORDER BY group_rank, rank
OFFSET $2
LIMIT $3`,
2023-05-08 22:32:37 +00:00
orderBy: 'ORDER BY group_rank, rank'
}, decodedCursor.time, decodedCursor.offset, limit, ...subArr)
2022-02-17 17:23:43 +00:00
break
default:
2023-05-23 14:21:04 +00:00
items = await itemQueryWithMeta({
me,
models,
query: `
${SELECT}, rank
2023-05-06 23:17:47 +00:00
FROM "Item"
2023-05-23 14:21:04 +00:00
${await joinSatRankView(me, models)}
${subClause(sub, 3, 'Item', true)}
2023-05-24 07:14:46 +00:00
ORDER BY rank ASC
2023-05-23 14:21:04 +00:00
OFFSET $1
LIMIT $2`,
2023-05-24 07:14:46 +00:00
orderBy: 'ORDER BY rank ASC'
}, decodedCursor.offset, limit, ...subArr)
2022-02-17 17:23:43 +00:00
if (decodedCursor.offset === 0) {
// get pins for the page and return those separately
pins = await itemQueryWithMeta({
me,
models,
query: `
SELECT rank_filter.*
FROM (
${SELECT},
rank() OVER (
PARTITION BY "pinId"
ORDER BY created_at DESC
)
FROM "Item"
WHERE "pinId" IS NOT NULL
${subClause(sub, 1)}
) rank_filter WHERE RANK = 1`
}, ...subArr)
2022-02-17 17:23:43 +00:00
}
break
}
break
2021-06-24 23:56:01 +00:00
}
2021-06-22 17:47:49 +00:00
return {
cursor: items.length === limit ? nextCursorEncoded(decodedCursor) : null,
2022-01-07 16:32:31 +00:00
items,
pins
2021-06-22 17:47:49 +00:00
}
2021-04-24 21:05:07 +00:00
},
2021-09-23 17:42:00 +00:00
item: getItem,
2023-01-12 18:05:47 +00:00
pageTitleAndUnshorted: async (parent, { url }, { models }) => {
const res = {}
2021-08-22 15:25:17 +00:00
try {
const response = await fetch(ensureProtocol(url), { redirect: 'follow' })
const html = await response.text()
const doc = domino.createWindow(html).document
const metadata = getMetadata(doc, url, { title: metadataRuleSets.title })
2023-01-12 18:05:47 +00:00
res.title = metadata?.title
} catch { }
try {
const unshorted = await uu().expand(url)
if (unshorted) {
res.unshorted = unshorted
}
} catch { }
return res
2021-10-28 20:49:51 +00:00
},
2023-05-07 01:25:00 +00:00
dupes: async (parent, { url }, { me, models }) => {
2021-10-28 20:49:51 +00:00
const urlObj = new URL(ensureProtocol(url))
let uri = urlObj.hostname + '(:[0-9]+)?' + urlObj.pathname
uri = uri.endsWith('/') ? uri.slice(0, -1) : uri
2023-01-11 22:20:14 +00:00
const parseResult = parse(urlObj.hostname)
if (parseResult?.subdomain?.length) {
const { subdomain } = parseResult
uri = uri.replace(subdomain, '(%)?')
} else {
uri = `(%.)?${uri}`
}
let similar = `(http(s)?://)?${uri}/?`
const whitelist = ['news.ycombinator.com/item', 'bitcointalk.org/index.php']
2023-01-11 22:04:50 +00:00
const youtube = ['www.youtube.com', 'youtube.com', 'm.youtube.com', 'youtu.be']
if (whitelist.includes(uri)) {
similar += `\\${urlObj.search}`
} else if (youtube.includes(urlObj.hostname)) {
// extract id and create both links
const matches = url.match(/(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?<id>[_0-9a-z-]+)/i)
2023-01-24 14:37:33 +00:00
similar = `(http(s)?://)?((www.|m.)?youtube.com/(watch\\?v=|v/|live/)${matches?.groups?.id}|youtu.be/${matches?.groups?.id})((\\?|&|#)%)?`
} else {
2022-05-18 18:21:24 +00:00
similar += '((\\?|#)%)?'
}
return await itemQueryWithMeta({
me,
models,
query: `
${SELECT}
FROM "Item"
WHERE LOWER(url) SIMILAR TO LOWER($1)
ORDER BY created_at DESC
LIMIT 3`
}, similar)
2021-12-21 21:29:42 +00:00
},
2022-09-29 20:42:33 +00:00
auctionPosition: async (parent, { id, sub, bid }, { models, me }) => {
2022-10-05 18:55:30 +00:00
const createdAt = id ? (await getItem(parent, { id }, { models, me })).createdAt : new Date()
let where
2022-09-29 20:42:33 +00:00
if (bid > 0) {
2022-10-05 18:55:30 +00:00
// if there's a bid
// it's ACTIVE and has a larger bid than ours, or has an equal bid and is older
// count items: (bid > ours.bid OR (bid = ours.bid AND create_at < ours.created_at)) AND status = 'ACTIVE'
where = {
status: 'ACTIVE',
OR: [
{ maxBid: { gt: bid } },
{ maxBid: bid, createdAt: { lt: createdAt } }
]
}
2022-09-29 20:42:33 +00:00
} else {
2022-10-05 18:55:30 +00:00
// else
// it's an active with a bid gt ours, or its newer than ours and not STOPPED
// count items: ((bid > ours.bid AND status = 'ACTIVE') OR (created_at > ours.created_at AND status <> 'STOPPED'))
where = {
OR: [
{ maxBid: { gt: 0 }, status: 'ACTIVE' },
{ createdAt: { gt: createdAt }, status: { not: 'STOPPED' } }
]
}
2022-09-29 20:42:33 +00:00
}
2022-10-05 18:55:30 +00:00
where.subName = sub
2022-02-17 17:23:43 +00:00
if (id) {
2022-10-05 18:55:30 +00:00
where.id = { not: Number(id) }
2022-02-17 17:23:43 +00:00
}
2022-10-05 18:55:30 +00:00
return await models.item.count({ where }) + 1
2021-04-12 18:05:09 +00:00
}
},
Mutation: {
bookmarkItem: async (parent, { id }, { me, models }) => {
const data = { itemId: Number(id), userId: me.id }
const old = await models.bookmark.findUnique({ where: { userId_itemId: data } })
if (old) {
await models.bookmark.delete({ where: { userId_itemId: data } })
} else await models.bookmark.create({ data })
return { id }
},
subscribeItem: async (parent, { id }, { me, models }) => {
const data = { itemId: Number(id), userId: me.id }
const old = await models.threadSubscription.findUnique({ where: { userId_itemId: data } })
if (old) {
await models.threadSubscription.delete({ where: { userId_itemId: data } })
} else await models.threadSubscription.create({ data })
return { id }
},
2023-01-12 23:53:09 +00:00
deleteItem: async (parent, { id }, { me, models }) => {
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
2023-01-12 23:53:09 +00:00
}
const data = { deletedAt: new Date() }
if (old.text) {
data.text = '*deleted by author*'
}
if (old.title) {
data.title = 'deleted by author'
}
if (old.url) {
data.url = null
}
if (old.pollCost) {
data.pollCost = null
}
return await models.item.update({ where: { id: Number(id) }, data })
},
upsertLink: async (parent, args, { me, models }) => {
const { id, ...data } = args
data.url = ensureProtocol(data.url)
2022-11-15 23:51:00 +00:00
data.url = removeTracking(data.url)
2021-08-11 20:13:10 +00:00
2023-02-08 19:38:04 +00:00
await ssValidate(linkSchema, data, models)
if (id) {
2022-08-18 18:15:24 +00:00
return await updateItem(parent, { id, data }, { me, models })
} else {
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
2021-08-11 20:13:10 +00:00
}
},
upsertDiscussion: async (parent, args, { me, models }) => {
const { id, ...data } = args
2021-08-11 20:13:10 +00:00
2023-02-08 19:38:04 +00:00
await ssValidate(discussionSchema, data, models)
if (id) {
2022-08-18 18:15:24 +00:00
return await updateItem(parent, { id, data }, { me, models })
} else {
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
2021-08-11 20:13:10 +00:00
}
},
2023-01-26 16:11:55 +00:00
upsertBounty: async (parent, args, { me, models }) => {
const { id, ...data } = args
2023-02-08 19:38:04 +00:00
await ssValidate(bountySchema, data, models)
2023-01-26 16:11:55 +00:00
if (id) {
return await updateItem(parent, { id, data }, { me, models })
} else {
return await createItem(parent, data, { me, models })
}
},
2023-02-08 19:38:04 +00:00
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
const { sub, forward, boost, title, text, options, invoiceHash, invoiceHmac } = data
let author = me
let spamInterval = ITEM_SPAM_INTERVAL
const trx = []
if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, ANON_POST_FEE)
author = invoice.user
spamInterval = ANON_ITEM_SPAM_INTERVAL
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
}
if (!author) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
2022-07-30 13:25:46 +00:00
}
2023-02-08 19:38:04 +00:00
const optionCount = id
? await models.pollOption.count({
2023-07-25 14:14:45 +00:00
where: {
itemId: Number(id)
}
})
2023-02-08 19:38:04 +00:00
: 0
await ssValidate(pollSchema, data, models, optionCount)
2022-07-30 13:25:46 +00:00
2022-08-18 18:15:24 +00:00
let fwdUser
if (forward) {
fwdUser = await models.user.findUnique({ where: { name: forward } })
if (!fwdUser) {
throw new GraphQLError('forward user does not exist', { extensions: { code: 'BAD_INPUT' } })
2022-08-18 18:15:24 +00:00
}
}
2022-07-30 13:25:46 +00:00
2022-08-18 18:15:24 +00:00
if (id) {
2023-02-08 19:38:04 +00:00
const old = await models.item.findUnique({ where: { id: Number(id) } })
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
if (Number(old.userId) !== Number(author.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
2022-08-18 18:15:24 +00:00
}
const [item] = await serialize(models,
2023-07-27 00:18:42 +00:00
models.$queryRawUnsafe(`${SELECT} FROM update_poll($1, $2::INTEGER, $3, $4, $5::INTEGER, $6, $7::INTEGER) AS "Item"`,
2023-05-01 20:58:30 +00:00
sub || 'bitcoin', Number(id), title, text, Number(boost || 0), options, Number(fwdUser?.id)))
2022-08-18 18:15:24 +00:00
2023-02-08 19:38:04 +00:00
await createMentions(item, models)
item.comments = []
2022-07-30 13:25:46 +00:00
return item
} else {
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
const [query] = await serialize(models,
models.$queryRawUnsafe(
`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::INTEGER, '${spamInterval}') AS "Item"`,
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(author.id), options, Number(fwdUser?.id)), ...trx)
const item = trx.length > 0 ? query[0] : query
2022-08-18 18:15:24 +00:00
await createMentions(item, models)
2022-07-30 13:25:46 +00:00
item.comments = []
return item
}
},
2023-02-08 19:38:04 +00:00
upsertJob: async (parent, { id, ...data }, { me, models }) => {
2022-02-17 17:23:43 +00:00
if (!me) {
throw new GraphQLError('you must be logged in to create job', { extensions: { code: 'FORBIDDEN' } })
2022-02-17 17:23:43 +00:00
}
2023-02-08 19:38:04 +00:00
const { sub, title, company, location, remote, text, url, maxBid, status, logo } = data
2022-02-17 17:23:43 +00:00
const fullSub = await models.sub.findUnique({ where: { name: sub } })
if (!fullSub) {
throw new GraphQLError('not a valid sub', { extensions: { code: 'BAD_INPUT' } })
2022-02-17 17:23:43 +00:00
}
2023-02-08 19:38:04 +00:00
await ssValidate(jobSchema, data, models)
const loc = location.toLowerCase() === 'remote' ? undefined : location
2022-02-17 17:23:43 +00:00
2022-09-29 20:42:33 +00:00
let item
2022-02-17 17:23:43 +00:00
if (id) {
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
}
2022-09-29 20:42:33 +00:00
([item] = await serialize(models,
2023-07-26 16:01:31 +00:00
models.$queryRawUnsafe(
2023-07-27 00:18:42 +00:00
`${SELECT} FROM update_job($1::INTEGER, $2, $3, $4, $5::INTEGER, $6, $7, $8, $9::INTEGER, $10::"Status") AS "Item"`,
2023-02-08 19:38:04 +00:00
Number(id), title, url, text, Number(maxBid), company, loc, remote, Number(logo), status)))
2022-09-29 20:42:33 +00:00
} else {
([item] = await serialize(models,
2023-07-26 16:01:31 +00:00
models.$queryRawUnsafe(
2023-07-27 00:18:42 +00:00
`${SELECT} FROM create_job($1, $2, $3, $4::INTEGER, $5::INTEGER, $6, $7, $8, $9::INTEGER) AS "Item"`,
2023-02-08 19:38:04 +00:00
title, url, text, Number(me.id), Number(maxBid), company, loc, remote, Number(logo))))
2022-02-17 17:23:43 +00:00
}
2022-09-29 20:42:33 +00:00
await createMentions(item, models)
return item
2022-02-17 17:23:43 +00:00
},
2023-02-08 19:38:04 +00:00
createComment: async (parent, data, { me, models }) => {
await ssValidate(commentSchema, data)
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
const item = await createItem(parent, data,
{ me, models, invoiceHash: data.invoiceHash, invoiceHmac: data.invoiceHmac })
// fetch user to get up-to-date name
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
2023-07-26 16:01:31 +00:00
const parents = await models.$queryRawUnsafe(
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2',
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
Number(item.parentId), Number(user.id))
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
Promise.allSettled(
parents.map(({ userId }) => sendUserNotification(userId, {
title: `@${user.name} replied to you`,
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
body: data.text,
item,
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
tag: 'REPLY'
}))
)
return item
},
2023-02-08 19:38:04 +00:00
updateComment: async (parent, { id, ...data }, { me, models }) => {
await ssValidate(commentSchema, data)
return await updateItem(parent, { id, data }, { me, models })
2021-08-10 22:59:06 +00:00
},
2022-07-30 13:25:46 +00:00
pollVote: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
2022-07-30 13:25:46 +00:00
}
await serialize(models,
2023-07-27 00:18:42 +00:00
models.$queryRawUnsafe(`${SELECT} FROM poll_vote($1::INTEGER, $2::INTEGER) AS "Item"`,
2022-07-30 13:25:46 +00:00
Number(id), Number(me.id)))
return id
},
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
act: async (parent, { id, sats, invoiceHash, invoiceHmac }, { me, models }) => {
// need to make sure we are logged in
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
if (!me && !invoiceHash) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
}
2023-02-08 19:38:04 +00:00
await ssValidate(amountSchema, { amount: sats })
2021-04-27 21:30:58 +00:00
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
let user = me
let invoice
if (!me && invoiceHash) {
invoice = await checkInvoice(models, invoiceHash, invoiceHmac, sats)
user = invoice.user
2021-09-10 21:13:52 +00:00
}
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
// disallow self tips except anons
if (user.id !== ANON_USER_ID) {
const [item] = await models.$queryRawUnsafe(`
${SELECT}
FROM "Item"
WHERE id = $1 AND "userId" = $2`, Number(id), user.id)
if (item) {
throw new GraphQLError('cannot zap your self', { extensions: { code: 'BAD_INPUT' } })
}
}
const calls = [
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`
]
if (invoice) {
calls.push(models.invoice.delete({ where: { hash: invoice.hash } }))
}
const [{ item_act: vote }] = await serialize(models, ...calls)
2021-09-10 21:13:52 +00:00
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
const updatedItem = await models.item.findUnique({ where: { id: Number(id) } })
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
const title = `your ${updatedItem.title ? 'post' : 'reply'} ${updatedItem.fwdUser ? 'forwarded' : 'stacked'} ${
numWithUnits(msatsToSats(updatedItem.msats))}${updatedItem.fwdUser ? ` to @${updatedItem.fwdUser.name}` : ''}`
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
sendUserNotification(updatedItem.userId, {
title,
body: updatedItem.title ? updatedItem.title : updatedItem.text,
item: updatedItem,
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
tag: `TIP-${updatedItem.id}`
}).catch(console.error)
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
2021-09-10 21:13:52 +00:00
return {
vote,
sats
2021-09-10 21:13:52 +00:00
}
2022-09-21 19:57:36 +00:00
},
dontLikeThis: async (parent, { id }, { me, models }) => {
// need to make sure we are logged in
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
2022-09-21 19:57:36 +00:00
}
// disallow self down votes
2023-07-26 16:01:31 +00:00
const [item] = await models.$queryRawUnsafe(`
2022-09-21 19:57:36 +00:00
${SELECT}
FROM "Item"
WHERE id = $1 AND "userId" = $2`, Number(id), me.id)
if (item) {
throw new GraphQLError('cannot downvote your self', { extensions: { code: 'BAD_INPUT' } })
2022-09-21 19:57:36 +00:00
}
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
await serialize(models, models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER,
${me.id}::INTEGER, 'DONT_LIKE_THIS', ${DONT_LIKE_THIS_COST}::INTEGER)`)
2022-09-21 19:57:36 +00:00
return true
2021-04-12 18:05:09 +00:00
}
},
Item: {
2022-11-15 20:51:55 +00:00
sats: async (item, args, { models }) => {
return msatsToSats(item.msats)
},
commentSats: async (item, args, { models }) => {
return msatsToSats(item.commentMsats)
},
2022-09-29 20:42:33 +00:00
isJob: async (item, args, { models }) => {
return item.subName === 'jobs'
},
2022-02-17 17:23:43 +00:00
sub: async (item, args, { models }) => {
if (!item.subName && !item.root) {
2022-02-17 17:23:43 +00:00
return null
}
return await models.sub.findUnique({ where: { name: item.subName || item.root?.subName } })
2022-02-17 17:23:43 +00:00
},
2022-01-07 16:32:31 +00:00
position: async (item, args, { models }) => {
if (!item.pinId) {
return null
}
const pin = await models.pin.findUnique({ where: { id: item.pinId } })
if (!pin) {
return null
}
return pin.position
},
prior: async (item, args, { models }) => {
if (!item.pinId) {
return null
}
const prior = await models.item.findFirst({
where: {
pinId: item.pinId,
createdAt: {
lt: item.createdAt
}
},
orderBy: {
createdAt: 'desc'
}
})
if (!prior) {
return null
}
return prior.id
},
2022-07-30 13:25:46 +00:00
poll: async (item, args, { models, me }) => {
if (!item.pollCost) {
return null
}
const options = await models.$queryRaw`
2023-07-27 00:18:42 +00:00
SELECT "PollOption".id, option, count("PollVote"."userId")::INTEGER as count,
2022-07-30 13:25:46 +00:00
coalesce(bool_or("PollVote"."userId" = ${me?.id}), 'f') as "meVoted"
FROM "PollOption"
LEFT JOIN "PollVote" on "PollVote"."pollOptionId" = "PollOption".id
WHERE "PollOption"."itemId" = ${item.id}
GROUP BY "PollOption".id
ORDER BY "PollOption".id ASC
`
2023-07-27 00:18:42 +00:00
2022-07-30 13:25:46 +00:00
const poll = {}
poll.options = options
poll.meVoted = options.some(o => o.meVoted)
poll.count = options.reduce((t, o) => t + o.count, 0)
return poll
},
2023-05-06 23:17:47 +00:00
user: async (item, args, { models }) => {
if (item.user) {
return item.user
}
return await models.user.findUnique({ where: { id: item.userId } })
},
2022-04-19 18:32:39 +00:00
fwdUser: async (item, args, { models }) => {
if (!item.fwdUserId) {
return null
}
return await models.user.findUnique({ where: { id: item.fwdUserId } })
},
2023-07-26 00:45:35 +00:00
comments: async (item, { sort }, { me, models }) => {
if (typeof item.comments !== 'undefined') return item.comments
2023-07-26 00:45:35 +00:00
if (item.ncomments === 0) return []
2023-07-26 00:45:35 +00:00
return comments(me, models, item.id, sort || defaultCommentSort(item.pinId, item.bioId, item.createdAt))
},
2022-10-28 15:58:31 +00:00
wvotes: async (item) => {
return item.weightedVotes - item.weightedDownVotes
},
2021-12-05 17:37:55 +00:00
meSats: async (item, args, { me, models }) => {
2021-09-10 21:13:52 +00:00
if (!me) return 0
2023-07-27 00:18:42 +00:00
if (typeof item.meMsats !== 'undefined') {
return msatsToSats(item.meMsats)
}
2021-09-10 21:13:52 +00:00
2023-07-26 16:01:31 +00:00
const { _sum: { msats } } = await models.itemAct.aggregate({
_sum: {
2022-11-15 20:51:55 +00:00
msats: true
2021-09-10 21:13:52 +00:00
},
where: {
2022-01-27 19:18:48 +00:00
itemId: Number(item.id),
2021-09-10 21:13:52 +00:00
userId: me.id,
2021-12-05 17:37:55 +00:00
OR: [
{
act: 'TIP'
},
{
2022-11-23 18:12:09 +00:00
act: 'FEE'
2021-12-05 17:37:55 +00:00
}
]
2021-09-10 21:13:52 +00:00
}
})
2022-11-15 20:51:55 +00:00
return (msats && msatsToSats(msats)) || 0
2021-09-10 21:13:52 +00:00
},
2022-09-21 19:57:36 +00:00
meDontLike: async (item, args, { me, models }) => {
if (!me) return false
2023-07-29 23:27:32 +00:00
if (typeof item.meDontLike !== 'undefined') return item.meDontLike
2022-09-21 19:57:36 +00:00
const dontLike = await models.itemAct.findFirst({
where: {
itemId: Number(item.id),
userId: me.id,
act: 'DONT_LIKE_THIS'
}
})
return !!dontLike
},
meBookmark: async (item, args, { me, models }) => {
if (!me) return false
2023-07-29 23:27:32 +00:00
if (typeof item.meBookmark !== 'undefined') return item.meBookmark
const bookmark = await models.bookmark.findUnique({
where: {
userId_itemId: {
itemId: Number(item.id),
userId: me.id
}
}
})
return !!bookmark
},
meSubscription: async (item, args, { me, models }) => {
if (!me) return false
2023-07-29 23:27:32 +00:00
if (typeof item.meSubscription !== 'undefined') return item.meSubscription
const subscription = await models.threadSubscription.findUnique({
where: {
userId_itemId: {
itemId: Number(item.id),
userId: me.id
}
}
})
return !!subscription
},
2022-09-22 18:44:50 +00:00
outlawed: async (item, args, { me, models }) => {
if (me && Number(item.userId) === Number(me.id)) {
return false
}
return item.weightedVotes - item.weightedDownVotes <= -ITEM_FILTER_THRESHOLD
},
2021-12-05 17:37:55 +00:00
mine: async (item, args, { me, models }) => {
return me?.id === item.userId
},
2021-07-08 00:15:27 +00:00
root: async (item, args, { models }) => {
2023-01-26 19:37:51 +00:00
if (!item.rootId) {
2021-07-08 00:15:27 +00:00
return null
}
2023-02-04 00:08:08 +00:00
if (item.root) {
return item.root
}
2023-01-26 19:09:57 +00:00
return await models.item.findUnique({ where: { id: item.rootId } })
2021-07-08 00:15:27 +00:00
},
parent: async (item, args, { models }) => {
if (!item.parentId) {
return null
}
return await models.item.findUnique({ where: { id: item.parentId } })
2023-01-22 20:17:50 +00:00
},
parentOtsHash: async (item, args, { models }) => {
if (!item.parentId) {
return null
}
const parent = await models.item.findUnique({ where: { id: item.parentId } })
return parent.otsHash
}
}
}
2021-08-18 22:20:33 +00:00
const namePattern = /\B@[\w_]+/gi
2021-09-23 17:42:00 +00:00
export const createMentions = async (item, models) => {
2021-08-18 22:20:33 +00:00
// if we miss a mention, in the rare circumstance there's some kind of
// failure, it's not a big deal so we don't do it transactionally
// ideally, we probably would
if (!item.text) {
return
}
try {
2021-08-19 19:53:11 +00:00
const mentions = item.text.match(namePattern)?.map(m => m.slice(1))
if (mentions?.length > 0) {
2021-08-18 22:20:33 +00:00
const users = await models.user.findMany({
where: {
name: { in: mentions }
}
})
users.forEach(async user => {
const data = {
itemId: item.id,
userId: user.id
}
await models.mention.upsert({
where: {
itemId_userId: data
},
update: data,
create: data
})
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
sendUserNotification(user.id, {
title: 'you were mentioned',
body: item.text,
item,
Service worker rework, Web Target Share API & Web Push API (#324) * npm uninstall next-pwa next-pwa was last updated in August 2022. There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482 But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us. It even lead to a bug since pages were cached without our knowledge. So I will go with a different PWA approach. This different approach should do the following: - make it more transparent what the service worker is doing - gives us more control to configure the service worker and thus making it easier * Use workbox-webpack-plugin Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built. (PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature) These default configurations even lead to worse UX since they made invalid assumptions about stacker.news: We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to. Almost every page on SN should be fresh for the best UX. To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there). Therefore, this should be the simplest configuration with a valid precache and cache busting support. In the future, we can try to use prefetching to improve performance of navigation requests. * Add support for Web Share Target API See https://developer.chrome.com/articles/web-share-target/ * Use Web Push API for push notifications I followed this (very good!) guide: https://web.dev/notifications/ * Refactor code related to Web Push * Send push notification to users on events * Merge notifications * Send notification to author of every parent recursively * Remove unused userId param in savePushSubscription As it should be, the user id is retrieved from the authenticated user in the backend. * Resubscribe user if push subscription changed * Update old subscription if oldEndpoint was given * Allow users to unsubscribe * Use LTREE operator instead of recursive query * Always show checkbox for push notifications * Justify checkbox to end * Update title of first push notification * Fix warning from uncontrolled to controlled * Add comment about Notification.requestPermission * Fix timestamp * Catch error on push subscription toggle * Wrap function bodies in try/catch * Use Promise.allSettled * Filter subscriptions by user notification settings * Fix user notification filter * Use skipWaiting --------- Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
tag: 'MENTION'
}).catch(console.error)
2021-08-18 22:20:33 +00:00
})
}
} catch (e) {
console.log('mention failure', e)
}
}
2023-05-01 20:58:30 +00:00
export const updateItem = async (parent, { id, data: { sub, title, url, text, boost, forward, bounty, parentId } }, { me, models }) => {
// update iff this item belongs to me
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
}
2022-08-18 18:15:24 +00:00
// if it's not the FAQ, not their bio, and older than 10 minutes
const user = await models.user.findUnique({ where: { id: me.id } })
2022-10-23 15:43:39 +00:00
if (![349, 76894, 78763, 81862].includes(old.id) && user.bioId !== id && Date.now() > new Date(old.createdAt).getTime() + 10 * 60000) {
throw new GraphQLError('item can no longer be editted', { extensions: { code: 'BAD_INPUT' } })
}
2022-08-18 18:15:24 +00:00
if (boost && boost < BOOST_MIN) {
throw new GraphQLError(`boost must be at least ${BOOST_MIN}`, { extensions: { code: 'BAD_INPUT' } })
2022-08-10 15:06:31 +00:00
}
2022-08-27 02:57:41 +00:00
if (!old.parentId && title.length > MAX_TITLE_LENGTH) {
throw new GraphQLError('title too long', { extensions: { code: 'BAD_INPUT' } })
2022-08-26 23:31:51 +00:00
}
2022-08-18 18:15:24 +00:00
let fwdUser
if (forward) {
fwdUser = await models.user.findUnique({ where: { name: forward } })
if (!fwdUser) {
throw new GraphQLError('forward user does not exist', { extensions: { code: 'BAD_INPUT' } })
2022-08-18 18:15:24 +00:00
}
}
2023-07-13 20:18:04 +00:00
url = await proxyImages(url)
text = await proxyImages(text)
2022-08-18 18:15:24 +00:00
const [item] = await serialize(models,
2023-07-26 16:01:31 +00:00
models.$queryRawUnsafe(
2023-07-27 00:18:42 +00:00
`${SELECT} FROM update_item($1, $2::INTEGER, $3, $4, $5, $6::INTEGER, $7::INTEGER, $8::INTEGER) AS "Item"`,
2023-05-01 20:58:30 +00:00
old.parentId ? null : sub || 'bitcoin', Number(id), title, url, text,
Number(boost || 0), bounty ? Number(bounty) : null, Number(fwdUser?.id)))
2021-08-18 22:20:33 +00:00
await createMentions(item, models)
return item
}
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash, invoiceHmac }) => {
let author = me
let spamInterval = ITEM_SPAM_INTERVAL
const trx = []
if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
author = invoice.user
spamInterval = ANON_ITEM_SPAM_INTERVAL
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
}
if (!author) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
}
2022-03-09 19:44:50 +00:00
if (boost && boost < BOOST_MIN) {
throw new GraphQLError(`boost must be at least ${BOOST_MIN}`, { extensions: { code: 'BAD_INPUT' } })
2021-09-11 21:52:19 +00:00
}
2022-08-27 01:27:57 +00:00
if (!parentId && title.length > MAX_TITLE_LENGTH) {
throw new GraphQLError('title too long', { extensions: { code: 'BAD_INPUT' } })
2022-08-26 23:31:51 +00:00
}
2022-04-19 18:32:39 +00:00
let fwdUser
if (forward) {
fwdUser = await models.user.findUnique({ where: { name: forward } })
if (!fwdUser) {
throw new GraphQLError('forward user does not exist', { extensions: { code: 'BAD_INPUT' } })
2022-04-19 18:32:39 +00:00
}
}
2023-07-13 20:18:04 +00:00
url = await proxyImages(url)
text = await proxyImages(text)
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
const [query] = await serialize(
2023-01-26 16:11:55 +00:00
models,
2023-07-26 16:01:31 +00:00
models.$queryRawUnsafe(
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, '${spamInterval}') AS "Item"`,
2023-05-01 20:58:30 +00:00
parentId ? null : sub || 'bitcoin',
2023-01-26 16:11:55 +00:00
title,
url,
text,
Number(boost || 0),
bounty ? Number(bounty) : null,
Number(parentId),
Allow zapping, posting and commenting without funds or an account (#336) * Add anon zaps * Add anon comments and posts (link, discussion, poll) * Use payment hash instead of invoice id as proof of payment Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs. * Allow pay per invoice for stackers The modal which pops up if the stacker does not have enough sats now has two options: "fund wallet" and "pay invoice" * Fix onSuccess called twice For some reason, when calling `showModal`, `useMemo` in modal.js and the code for the modal component (here: <Invoice>) is called twice. This leads to the `onSuccess` callback being called twice and one failing since the first one deletes the invoice. * Keep invoice modal open if focus is lost * Skip anon user during trust calculation * Add error handling * Skip 'invoice not found' errors * Remove duplicate insufficient funds handling * Fix insufficient funds error detection * Fix invoice amount for comments * Allow pay per invoice for bounty and job posts * Also strike on payment after short press * Fix unexpected token 'export' * Fix eslint * Remove unused id param * Fix comment copy-paste error * Rename to useInvoiceable * Fix unexpected token 'export' * Fix onConfirmation called at every render * Add invoice HMAC This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN. Only the user which created the invoice knows the HMAC and thus can use the invoice hash. * make anon posting less hidden, add anon info button explainer * Fix anon users can't zap other anon users * Always show repeat and contacts on action error * Keep track of modal stack * give anon an icon * add generic date pivot helper * make anon user's invoices expire in 5 minutes * fix forgotten find and replace * use datePivot more places * add sat amounts to invoices * reduce anon invoice expiration to 3 minutes * don't abbreviate * Fix [object Object] as error message Any errors thrown here are already objects of shape { message: string } * Fix empty invoice creation attempts I stumbled across this while checking if anons can edit their items. I monkey patched the code to make it possible (so they can see the 'edit' button) and tried to edit an item but I got this error: Variable "$amount" of required type "Int!" was not provided. I fixed this even though this function should never be called without an amount anyway. It will return a sane error in that case now. * anon func mods, e.g. inv limits * anon tips should be denormalized * remove redundant meTotalSats * correct overlay zap text for anon * exclude anon from trust graph before algo runs * remove balance limit on anon * give anon a bio and remove cowboy hat/top stackers; * make anon hat appear on profile * concat hash and hmac and call it a token * Fix localStorage cleared because error were swallowed * fix qr layout shift * restyle fund error modal * Catch invoice errors in fund error modal * invoice check backoff * anon info typo * make invoice expiration times have saner defaults * add comma to anon info * use builtin copy input label --------- Co-authored-by: ekzyis <ek@stacker.news> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2023-08-11 23:50:57 +00:00
Number(author.id),
Number(fwdUser?.id)),
...trx)
const item = trx.length > 0 ? query[0] : query
2021-08-18 22:20:33 +00:00
await createMentions(item, models)
2021-05-20 01:09:32 +00:00
item.comments = []
return item
}
// we have to do our own query because ltree is unsupported
2021-09-23 17:42:00 +00:00
export const SELECT =
`SELECT "Item".id, "Item".created_at, "Item".created_at as "createdAt", "Item".updated_at,
"Item".updated_at as "updatedAt", "Item".title, "Item".text, "Item".url, "Item"."bounty",
"Item"."userId", "Item"."fwdUserId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
"Item"."rootId", "Item".upvotes, "Item".company, "Item".location, "Item".remote, "Item"."deletedAt",
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item".boost, "Item".msats,
"Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes",
"Item"."weightedDownVotes", "Item".freebie, "Item"."otsHash", "Item"."bountyPaidTo",
2023-05-09 18:52:35 +00:00
ltree2text("Item"."path") AS "path", "Item"."weightedComments"`
2021-04-27 21:30:58 +00:00
2022-09-21 19:57:36 +00:00
async function topOrderByWeightedSats (me, models) {
return `ORDER BY ${await orderByNumerator(me, models)} DESC NULLS LAST, "Item".id DESC`
}