Compare commits

..

1541 Commits

Author SHA1 Message Date
Keyan
eaa15b3b43
Update awards.csv with all 3 search issue closes 2025-03-24 14:41:23 -05:00
Keyan
9b4f1643a7
Update awards.csv 2025-03-24 14:35:22 -05:00
Keyan
411c8317b0
Update awards.csv using failed dangling github actions 2025-03-24 14:25:54 -05:00
github-actions[bot]
c39bfcecb6
Extending awards.csv (#2013)
Co-authored-by: huumn <34140557+huumn@users.noreply.github.com>
2025-03-24 13:31:46 -05:00
Edward Kung
f20ebad772
allow sort for search queries with only filters (#2012) 2025-03-23 11:53:45 -05:00
ekzyis
3ff03960eb
Remove unused addAccount, removeAccount (#2009) 2025-03-23 11:53:04 -05:00
ekzyis
9b08988402
Refactor login cookie with cookieOptions function (#2003) 2025-03-22 19:36:04 -05:00
ekzyis
b54268a88f
normalized wallet logs (#1826)
* Add invoiceId, withdrawalId to wallet logs

* Truncate wallet logs

* Fix extra db dips per log line

* Fix leak of invoice for sender
2025-03-22 17:31:10 -05:00
ekzyis
e7eece744f
Use __Secure- cookie prefix (#1998) 2025-03-22 16:59:57 -05:00
M Ʌ R C
54d3b11fbc
Update faq.md (#2008)
Adding links to guides
2025-03-22 10:51:10 -05:00
k00b
06b877e3d3 bump nextjs 2025-03-22 10:13:32 -05:00
ekzyis
5ff1334722
Remove unnecessary exports (#2006) 2025-03-21 19:56:01 -05:00
ekzyis
5e2185c18f
Use cookieOptions for pointer cookie (#2005) 2025-03-21 19:53:49 -05:00
k00b
bce4053b72 update boost explainer 2025-03-21 19:49:53 -05:00
k00b
2d619438b1 make hot_score_view editable through parameters of another view 2025-03-21 19:47:12 -05:00
k00b
5164258df8 remove independence threshold 2025-03-20 19:18:56 -05:00
ekzyis
f96b3bf19a
Fix useQuery lifecycle anti-pattern (#2001) 2025-03-20 17:46:19 -05:00
ekzyis
a83783f008
Fix missing max-age cookie option (#2000) 2025-03-20 16:38:13 -05:00
soxa
dbbd9477fd
don't send a verification email during sign in if no match (#1999)
* don't send verification email on signin if there's no matching user, update email.js message

* conditional magic code message; signup/signin button

* unnecessary useCallback

* switch to cookie parsing lib

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-03-20 15:32:31 -05:00
Keyan
a669ec832b
Update README.md 2025-03-20 13:28:26 -05:00
k00b
7dccd383c3 fix hot score on limited comment queries fix #1996 2025-03-20 12:21:20 -05:00
ekzyis
271563efbd
Fix space before question mark in delete prompt (#1995) 2025-03-20 10:48:17 -05:00
ekzyis
ada230597d
Fix anon dropdown button width (#1997) 2025-03-20 10:46:07 -05:00
Edward Kung
08501583df
reset nym editting state on page change (#1993) 2025-03-19 18:55:22 -05:00
ekzyis
74d99e9b74
Reset multi_auth cookies on error (#1957)
* multi_auth cookies check + reset

* multi_auth cookies refresh

* Expire cookies after 30 days

This is the actual default for next-auth.session-token.

* Collapse issues by default

* Only refresh session cookie manually as anon

* fix mangled merge

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-03-19 18:54:43 -05:00
soxa
71caa6d0fe
Prevent new account creation on login (#1976)
* Prevent account creation if we're not signin up

* remove cookie once logged in, 24 hours expiry, comment

* adjust error messages

* check signin instead of signup

* appendHeader to avoid overwrites, fix typo, use NodeNextRequest to handle cookies

* expire cookie if signup
2025-03-19 16:55:38 -05:00
k00b
4f17615291 fix expireBoost boolean condition 2025-03-19 11:48:36 -05:00
k00b
0e4b467b3c make expire boost exclude unpaid invoices 2025-03-19 11:44:35 -05:00
Edward Kung
9905e6eafe
Top cowboys territory selector fix (#1972)
* fix territory selector when in top/cowboys

* redirect /~sub/top/cowboys to /top/cowboys

* check if pathname ends with /top/cowboys

Co-authored-by: ekzyis <ek@stacker.news>

* fix territory selector in top/stackers and top/territories

* better routing logic

---------

Co-authored-by: ekzyis <ek@stacker.news>
2025-03-19 08:19:19 -05:00
github-actions[bot]
fc6cbba40c
Extending awards.csv (#1988)
Co-authored-by: huumn <34140557+huumn@users.noreply.github.com>
2025-03-18 18:18:58 -05:00
ekzyis
60f628e77e
Fix no pw manager autofill for device sync password (#1953)
* Fix no pw manager autofill for device sync password

* Remove unused rows property
2025-03-18 18:17:23 -05:00
soxa
63704a5f0f
Hide pull-to-refresh when not pulling (#1986) 2025-03-18 18:16:36 -05:00
ekzyis
964cdc1d61
Fix warning about missing key for children (#1987) 2025-03-18 17:48:13 -05:00
Scroogey-SN
e31f8e9c69
Support .onion for phoenixd (#1975)
* fix #1964 support .onion urls like lnbits does

* fix lint: const hostname

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-03-18 15:39:53 -05:00
k00b
b7dfef41c0 make search query work with os2.17 and upgrade containers 2025-03-18 14:01:00 -05:00
k00b
344c23ed5c use max zap creation 2025-03-17 20:03:05 -05:00
Edward Kung
b71398a06c
Search improvements: Add relevance search and make recent searches less strict (#1962)
* reconfigured search pipeline

* remove console debug messages

* log1p for comments

* improve relevance of non-relevance sorted queries

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-03-17 19:25:20 -05:00
github-actions[bot]
1a52ff7784
Extending awards.csv (#1978)
Co-authored-by: huumn <34140557+huumn@users.noreply.github.com>
2025-03-17 14:31:00 -05:00
Scroogey-SN
a3762f70b0
fix #1959 strip trailing slashes from url (#1973) 2025-03-17 13:16:31 -05:00
ekzyis
c492618d31
Fix weeks not support in reminder command (#1977) 2025-03-17 13:15:09 -05:00
k00b
4be7f12119 add another hot_score index without nulls clause 2025-03-15 15:29:01 -05:00
Keyan
b672d015e2
territory specific trust (#1965)
* territory specific trust

* functional parity with master

* revert back to materialized view for ranking

* update query for populating subWeightedVotes

* fix anon hot comments

* fix zap denormalization, change weightedComments to be for zaps, order updates of ancestors to prevent deadlocks

* reduce weight of comment zaps for hot score

* do zap ancestor updates together

* initialize trust in new/unpopular territories

* simplify denormalization of zap/downzaps

* recompute all scores
2025-03-15 08:11:33 -05:00
Keyan
53f6c34ee7
Merge pull request #1974 from stackernews/rename-to-next-account
Fix missing rename to nextAccount
2025-03-15 08:06:21 -05:00
ekzyis
54e7793668 Fix missing rename to nextAccount 2025-03-14 22:01:55 -05:00
Keyan
0d93c92e30
Merge pull request #1971 from stackernews/fix-anon-failed-invoices
Don't poll failed invoices if anon
2025-03-12 20:25:12 -05:00
ekzyis
6f50b485b4 Don't poll failed invoices if anon 2025-03-12 20:10:15 -05:00
Keyan
6715943862
Merge pull request #1970 from stackernews/reply-cost-check
Add check for Sub.replyCost > 0
2025-03-12 19:46:47 -05:00
Keyan
dbce0a7517
Merge pull request #1969 from stackernews/fix-saloon-replies
Fix saloon replies
2025-03-12 19:46:21 -05:00
ekzyis
4e62053b45 Fix saloon replies
The query will return { replyCost: null } if no sub was found because of the left join.
2025-03-12 17:53:39 -05:00
ekzyis
a9943981fd Add check for Sub.replyCost > 0 2025-03-12 17:53:15 -05:00
ekzyis
2038701ecf
Merge pull request #1968 from stackernews/faq-update
Update FAQ with wallet questions
2025-03-12 17:44:59 -05:00
ekzyis
b46418e71e Shorten question 2025-03-12 15:14:57 -05:00
ekzyis
907a7aabbd Update FAQ with wallet questions 2025-03-12 15:13:27 -05:00
Keyan
8f8b2e4496
Merge pull request #1966 from stackernews/rename-to-next-account
Rename to /api/next-account
2025-03-11 07:56:46 -05:00
ekzyis
69e62c1e6e Rename to /api/next-account 2025-03-10 20:24:13 -05:00
Keyan
9aad2fd903
Merge pull request #1963 from stackernews/fix-item-cost
Fix item cost in details
2025-03-10 20:03:48 -05:00
k00b
ed9fa5f823 fix fragment when comment visited directly + one db dip 2025-03-10 20:02:55 -05:00
ekzyis
65b1db23a7 Fix item cost in details 2025-03-10 18:13:33 -05:00
Keyan
d8c604172a
Update awards.csv - pay up 2025-03-10 14:48:35 -05:00
ekzyis
3a93b04e07
Merge pull request #1960 from stackernews/fix-welcome
Fix fetching of user items in welcome script
2025-03-09 23:29:14 -05:00
ekzyis
0801191b9e Fix fetching of user items in welcome script 2025-03-09 23:06:50 -05:00
ekzyis
7b725746b0
Remove unnecessary wrapping with NodeNext(Request|Response) (#1956) 2025-03-09 10:53:14 -05:00
ekzyis
5b7ff24955
Allow any admin to toggle SNL banner (#1955) 2025-03-07 18:01:34 -06:00
github-actions[bot]
8ad352ee0f
Extending awards.csv (#1954)
Co-authored-by: huumn <34140557+huumn@users.noreply.github.com>
2025-03-07 18:00:55 -06:00
Edward Kung
2c79472c69
Update semantic search documentation (#1952)
* update semantic search setup documentation

* instructions to modify .env.local instead of .env.development

* fix typo
2025-03-07 15:46:34 -06:00
Keyan
e269a8eb5b
Update awards.csv 2025-03-06 18:26:31 -06:00
k00b
19e3b65edd store extracted links 2025-03-06 18:14:10 -06:00
k00b
b7130b68fd sort notes 2025-03-06 18:14:10 -06:00
Keyan
9292d4f991
Update awards.csv - add known lnaddrs 2025-03-06 14:49:46 -06:00
Keyan
7c98a32452
Update awards.csv 2025-03-06 13:20:51 -06:00
github-actions[bot]
3659b4555e
Extending awards.csv (#1949)
Co-authored-by: huumn <34140557+huumn@users.noreply.github.com>
2025-03-06 13:09:00 -06:00
Scroogey-SN
6363550123
change trigger from pull_request to pull_request_target (#1948) 2025-03-06 13:06:51 -06:00
k00b
53e4683d98 overflow hidden on media container, don't revert-layer on video 2025-03-06 13:04:32 -06:00
k00b
4dd9088f25 fix #1945 2025-03-06 09:27:23 -06:00
soxa
b03e02e4cf
Expose env-dependent constant to service worker; Add Brave Browser-specific push registration error and guidance; Remove unused fetch debug plugin (#1947) 2025-03-06 09:24:49 -06:00
ekzyis
75d0a8e3d9
Fix undefined db for wallet logs (#1944) 2025-03-05 07:29:21 -06:00
k00b
d3e5af40ec fix trust after mathjs upgrade 2025-03-05 07:26:59 -06:00
ekzyis
bf54044a96
Also check for user before setting multi auth cookies (#1941)
* Move multi auth init

* Store same token as we return in jwt callback
2025-03-04 08:58:48 -06:00
k00b
8eb5a51fd6 move sanctioned country codes to env var 2025-03-03 18:14:36 -06:00
k00b
5a8804de79 disallow buying CCs through lnurl-pay and lightning address 2025-03-03 18:03:34 -06:00
Keyan
090203b579
Update extend-awards.yml adding issues: read 2025-03-03 15:41:04 -06:00
soxa
8baf725a37
fix safari videos regression, overflow clipping images (#1938) 2025-03-03 14:53:02 -06:00
Keyan
20b2f9d4c6
Update extend-awards.yml with contents write 2025-03-03 14:39:31 -06:00
Scroogey-SN
65cb25e28c
Extend awards action (#1939)
* remove debug job, restrict create-pull-request to only awards.txt, add documentation

* make create-pull-request use a custom branch, and filter that out, so PRs generated by action don't invoke action again

* add permissions: line

* pull-requests write instead of all

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-03-03 14:35:22 -06:00
ekzyis
dfe0c4ad23
Fix footnotes and overflow (#1940)
* Fix missing uncollapse on footnote click

* Add comments to variables
2025-03-03 14:31:10 -06:00
Keyan
0d57dce068
Update awards.csv 2025-03-03 13:33:24 -06:00
Scroogey-SN
b1cdc76eec
fix #1167: allow pointer events in linkBoxParent pre for scrollbar (#1930)
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-03-03 12:47:54 -06:00
Scroogey-SN
27104302d5
Extend awards action (#1937)
* remove debug job, restrict create-pull-request to only awards.txt, add documentation

* make create-pull-request use a custom branch, and filter that out, so PRs generated by action don't invoke action again
2025-03-03 12:47:31 -06:00
Keyan
8a764f0f75
Revert "Extend awards action (#1933)" (#1936)
This reverts commit dfc297436b451cb14cdb57f42ea47990dd586464.
2025-03-01 16:57:56 -06:00
soxa
f72af08882
fix: WebLN QR fallback for anon users (#1858)
* fix: WebLN QR fallback for anon users

* wip: clear zap color on payment fail

* reverse clearItemMeAnonSats

* webln-specific retry bypass

* cleanup

* send WebLN payment when user is Anon AND on QR

* skip wallet checking on anon

* Use WalletError for all errors in webln.sendPayment

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-03-01 16:56:18 -06:00
soxa
5e7fd693f1
Redirect to top cowboys page if there's a time descriptor (#1913)
* redirect to cowboys.js if there's a time descriptor

* add comment for future reference

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-03-01 16:52:37 -06:00
Scroogey-SN
dfc297436b
Extend awards action (#1933)
* add an action that extends awards.csv on PR merges

* change trigger to pull_request, add unfiltered step for logging
2025-03-01 16:51:39 -06:00
Edward Kung
34c7218eba
Add SimpleStacker to contributors.txt (#1935)
* Add SimpleStacker to contributors list

* remove empty line
2025-03-01 10:38:39 -06:00
Keyan
5a7593f2a7
Revert "add an action that extends awards.csv on PR merges (#1931)" (#1932)
This reverts commit 5de9d92af25be1c8c757b344843f70fb82ffbd6a.
2025-02-28 19:19:07 -06:00
Edward Kung
73170ba8a2
Territory analytics (#1926)
* add territory to analytics selectors

* implement territory analytics, revert user satistics header

* fix linting errors

* disallow some territory names

* fix linting error

* minor adjustments to header

* escape input

* 404 on non-existant sub

* exclude unused queries depending on sub select

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-02-28 19:15:18 -06:00
Scroogey-SN
5de9d92af2
add an action that extends awards.csv on PR merges (#1931) 2025-02-28 19:13:14 -06:00
k00b
4e113c267b moar link extraction vibe coding: config file, batching, log level 2025-02-28 11:35:41 -06:00
Keyan
d1bbfd5339
Update awards.csv 2025-02-27 15:48:10 -06:00
Scroogey-SN
b6618dd66a
Fix issue #1924, require nostr prefixes start with slash (#1928) 2025-02-27 15:24:09 -06:00
Scroogey-SN
33db3b2c79
Add self to contributors.txt (#1929) 2025-02-27 15:16:40 -06:00
Keyan
f271926665
Update awards.csv 2025-02-27 13:13:27 -06:00
Scroogey-SN
f97c1b04e6
Fix issue #1905, item popover with commentId (#1911)
* Fix issue #1905, item popover with commentId

* Fix issue #1924, require nostr prefixes start with slash

* revert hacks and unrelated changes, use commentId in rehype

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-02-27 13:10:42 -06:00
k00b
43edef55eb vibe coded nostr link extractor script 2025-02-27 11:58:02 -06:00
soxa
31532ff830
Dynamic loading optimizations (#1925)
* dynamic: React QR Scanner, React Datepicker; placeholder for syntax highlighting

* Loading placeholders, prevent layout shifting
2025-02-24 15:06:40 -06:00
k00b
31b58baf51 decrease nofollow limit 2025-02-23 11:08:05 -06:00
soxa
7b988b87d9
hotfix: email address should be case insensitive (#1923) 2025-02-23 11:03:32 -06:00
soxa
bc3c008a6d
Dynamically import MathJax (#1910)
* Dynamically import MathJax

* Only load if there's math content; cleanup

* avoid loading RSH on Math, we have MathJax for that; cleanup

* support multiline mathjax

---------

Co-authored-by: k00b <k00b@stacker.news>
2025-02-21 15:41:27 -06:00
Keyan
c571ba0cb7
README.md typo 2025-02-15 20:16:34 -06:00
Keyan
46f87e98b6
Orbstack encouragement 2025-02-15 20:15:51 -06:00
soxa
868847cb43
Dynamically import React Syntax Highlighter; correct theme (#1909) 2025-02-15 16:33:44 -06:00
k00b
d8de6255fe Merge divergent deploy branch 2025-02-15 13:56:41 -06:00
Keyan
4651b36944
Update awards.csv 2025-02-14 22:09:54 -06:00
ekzyis
fca5193beb
Fix autowithdrawal error handling (#1908)
* Fix autowithdrawal error message

* Fix no error thrown if autowithdrawal failed
2025-02-14 20:55:17 -06:00
Keyan
f0d7eaf446
Update awards.csv 2025-02-14 20:09:28 -06:00
ekzyis
5e85147578
Fix receiver fallback on caller error (#1907)
* Rename to createUserInvoice

* Fix no receiver fallback on wrap, direct or autowithdrawal error

* Fix missing error logs for direct payments
2025-02-14 20:01:14 -06:00
ekzyis
0032e064b2
Automated retries (#1776)
* Poll failed invoices with visibility timeout

* Don't return intermediate failed invoices

* Don't retry too old invoices

* Retry invoices on client

* Only attempt payment 3 times

* Fix fallbacks during last retry

* Rename retry column to paymentAttempt

* Fix no index used

* Resolve TODOs

* Use expiring locks

* Better comments for constants

* Acquire lock during retry

* Use expiring lock in retry mutation

* Use now() instead of CURRENT_TIMESTAMP

* Cosmetic changes

* Immediately show failed post payments in notifications

* Update hasNewNotes

* Never retry on user cancel

For a consistent UX and less mental overhead, I decided to remove the exception for ITEM_CREATE where it would still retry in the background even though we want to show the payment failure immediately in notifications.

* Fix notifications without pending retries missing if no send wallets

If a stacker has no send wallets, they would miss notifications about failed payments because they would never get retried.

This commit fixes this by making the notifications query aware if the stacker has send wallets. This way, it can tell if a notification will be retried or not.

* Stop hiding userCancel in notifications

As mentioned in a previous commit, I want to show anything that will not be attempted anymore in notifications.

Before, I wanted to hide manually cancelled invoices but to not change experience unnecessarily and to decrease mental overhead, I changed my mind.

* Also consider invoice.cancelledAt in notifications

* Always retry failed payments, even without send wallets

* Fix notification indicator on retry timeout

* Set invoice.updated_at to date slightly in the future

* Use default job priority

* Stop retrying after one hour

* Remove special case for ITEM_CREATE

* Replace retryTimeout job with notification indicator query

* Fix sortTime

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-02-14 19:25:11 -06:00
k00b
f4040756b3 fix graphql errors in notifications 2025-02-14 15:10:15 -06:00
ekzyis
87b5bb80fd
Fix missing cleanup of dark mode listeners (#1906) 2025-02-14 11:37:24 -06:00
Edward Kung
15bd1c3fc5
Fix the check for misleading links (#1901)
* Fix the check for misleading links

* replace tabs with spaces

* remove trailing spaces

* move isMisleadingLinks to lib/url.js and create unit tests

* Add comments to test cases

* URLs can contain hyphens

---------

Co-authored-by: ekzyis <ek@stacker.news>
2025-02-14 09:43:08 -06:00
ekzyis
77781e07ed
Don't parse content in code blocks (#1899) 2025-02-13 11:46:53 -06:00
ekzyis
3cdf5c9451
Fix comment not outlined again (#1902) 2025-02-13 11:46:35 -06:00
k00b
a4cce7afed only record landing of referree if they don't have referrer 2025-02-12 10:10:25 -06:00
soxa
1afadbdf3b
enhance: referral notifications with source (#1862)
* wip: referral notification shows source of referral

* simpler approach for source info gathering

* fix territory representation; fix fragment field

* cleanup; fix UI

* better margin approach

* hotfix: null check

* add support for comments

* use Union to represent ReferralSource; clarify with switch statements

* cleanup: compact switch statement on Referral resolver

* wip use refereeLanding

* add comments; cleanup

* hotfix: backwards compatibility for Earnings calculation

* small copy and semantics changes

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-02-11 20:02:04 -06:00
k00b
bdd87e7d39 add retry link back 2025-02-11 16:29:37 -06:00
soxa
e6081ebef3
fix: THREAD notification type for noteAllDescendants (#1894)
* THREAD notifications to distinguish direct replies from follow-ups

* hotfix: typo

* hotfix: avoid subquery when we already have a JOIN
2025-02-11 13:45:04 -06:00
k00b
5af61f415f monospace font for edit countdowns 2025-02-10 19:19:22 -06:00
ekzyis
1ce88a216a
Merge pull request #1893 from stackernews/fix-welcome-script
Fix NaN in welcome script
2025-02-10 16:04:20 +01:00
ekzyis
a913c2d452 Fix NaN in welcome script 2025-02-10 01:39:13 +01:00
ekzyis
54afe67558
Merge pull request #1892 from stackernews/fix-welcome-script
Fix bios missed in welcome script
2025-02-10 01:33:21 +01:00
ekzyis
34aadba352 Fix bios missed in welcome script 2025-02-10 01:32:30 +01:00
Keyan
a8b3ee37bf
Update awards.csv 2025-02-08 18:42:41 -06:00
Keyan
64bbd2e1b8
Update awards.csv 2025-02-08 15:14:07 -06:00
soxa
c01f4865dc
apply flex rules after images are loaded (#1886) 2025-02-08 13:14:42 -06:00
k00b
1e673cab77 monospace font for edit countdowns 2025-02-07 18:49:22 -06:00
ekzyis
2732013da3
FAQ feedback (#1888) 2025-02-07 17:37:15 -06:00
ekzyis
f90ed8d294
Add territory stats answer to FAQ (#1887) 2025-02-07 14:51:03 -06:00
soxa
5c2aa979ea
feat: comment fee control (#1768)
* feat: comment fee control

* update typeDefs for unarchiving territories

* review: move functions to top level; consider saloon items

* ux: cleaner post/reply cost section

* hotfix: handle salon replies

* bios don't have subs + simplify root query

* move reply cost to accordian

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-02-07 13:38:57 -06:00
ekzyis
ac321be3cd
Big FAQ update (#1800) 2025-02-07 12:53:11 -06:00
k00b
95e98501ec reduce max incoming invoice expiration and expiration buffer 2025-02-05 12:47:35 -06:00
k00b
ee8fe6e72a add boost badge fix #1860 2025-02-03 19:56:29 -06:00
k00b
9885bcf209 make sure comments specify time zone for invoicePaidAt fix #1859 2025-02-03 19:40:33 -06:00
jason-me
2dfde257d2
Update price button accessibility in header (#1857)
* Update price button accessibility

* Updated accName and accDescription for speech dictation and screen reader users.

* Update price.js

Replace double quote with single

* Update price.js

Remove trailing spaces

* make .visually-hidden global, use className rather than class

* make accessible button component

---------

Co-authored-by: Jason Hester <jhester@TPGLPT-LTC23.attlocal.net>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-02-03 19:22:02 -06:00
soxa
be7c702602
Login with magic code (#1818)
* fix: cannot login with email on PWA

* adjust other email templates

* restore manual url on new user email

* no padding on button section

* cleanup

* generate 6-digit bechh32 token

* token needs to be fed as lower case; validator case insensitive

* delete token if user has failed 3 times

* proposal: context-independent error page

* include expiration time on email page message

* add expiration time to emails

* independent checkPWA function

* restore token deletion if successful auth

* final cleanup: remove unused function

* compact useVerificationToken

* email.js: magic code for non-PWA users

* adjust email templates

* MultiInput component; magic code via MultiInput

* hotfix: revert length testing; larger width for inputs

* manual bech32 token generation; no upperCase

* reverting to string concatenation

* layout tweaks, fix error placement

* pastable inputs

* small nit fixes

* less ambiguous error path

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-02-03 18:41:01 -06:00
k00b
89187db1ea fix failing jobs 2025-02-01 15:36:48 -06:00
Keyan
074f0c0634
If we are a hop hint, use alternate form of estimateRouteFee (#1854)
* untested draft

* handle empty routes
2025-01-31 20:19:21 -06:00
k00b
efdcbef733 fixes related to comment paging 2025-01-30 19:19:49 -06:00
k00b
33beb1dc52 fix history race on flat comment click 2025-01-30 17:08:16 -06:00
k00b
bb916b8669 fix comments in notifications 2025-01-30 10:54:51 -06:00
k00b
312f4defb0 fix recursive limited comments call 2025-01-30 10:10:29 -06:00
Keyan
01b021a337
comment pagination with limit/offset (#1824)
* basic query with limit/offset

* roughly working increment

* working limiting/pageable queries

* denormalize direct comments + full comments below threshold

* notifications in megathread + working nest view more buttons

* fix empty comment footer

* make comments nested resolver again

* use time in cursor to avoid duplicates

* squash migrations

* do not need item.comments undefined checks
2025-01-29 19:00:05 -06:00
soxa
bd84b8bf88
fix: Images on iOS are cropped weird (#1840)
* force sync decoding on images

* use decode() to load the image

* add comment
2025-01-28 15:30:54 -06:00
k00b
965e482ea3 stimulus to 50k 2025-01-28 10:57:45 -06:00
Keyan
f8fa0f65e7
Update awards.csv 2025-01-27 18:49:58 -06:00
Keyan
8059945f82
Update awards.csv 2025-01-27 18:18:24 -06:00
ekzyis
ee2d076d1b
Welcome series script update (#1848)
* Parse args in welcome script

* Refactor welcome script

* Show sats/ccs

* Add sat standard
2025-01-27 15:14:03 -06:00
ekzyis
156b895fb6
Add createrune info for v24.11 (#1847) 2025-01-26 12:28:51 -06:00
ekzyis
53b8f6f956
FAQ formatting changes (#1842)
* Fix inconsistent markdown formatting in FAQ

- remove usage of &lrm;
- don't skip headers (##### instead of ###)

* Use hash links in FAQ
2025-01-25 13:48:51 -06:00
ekzyis
c023e8d7d5
Fix missing push notifications for thread subscriptions (#1843)
* Fix missing push notifications for thread subscriptions

* Filter by comments in calling context

* Fix mutes not considered

* Fix duplicate push notification (reply+thread subscription) sent
2025-01-25 13:47:58 -06:00
k00b
b28407ee99 remove changes from footer 2025-01-23 16:54:50 -06:00
soxa
78533bda1b
fix: downzappable pinned posts (#1841)
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-01-22 19:15:03 -06:00
soxa
47faef872d
Subscribe unarchiver to unarchived territory (#1839)
* enhance: subscribe unarchiver to unarchived territory

* use upsert and fix #1517

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-01-22 19:11:19 -06:00
ekzyis
ca7726fda5
Fix recent sort order for retried items (#1829)
* Fix recent sort order for retried items

* Also fix for comments

* don't hide createdAt, order item query inner subquery

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2025-01-22 17:42:18 -06:00
soxa
ae1942ada7
fix: duplicate push notification on subscribed user and territory (#1820)
* fix: duplicate notification on subscribed user and territory

* fix comments not showing up, adjust query

* use  and tagged template helpers

---------

Co-authored-by: k00b <k00b@stacker.news>
2025-01-22 16:12:41 -06:00
soxa
a92215ccf6
fix: globally pinned items rank in global (#1814)
* fix: globally pinned items rank in global; use query to filter global pinned items

* cleanup

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-01-22 15:23:21 -06:00
Keyan
382714e422
Merge pull request #1837 from stackernews/fix-possible-silent-push
Fix possible silent push
2025-01-21 09:37:10 -06:00
ekzyis
1942c79193 Fix possible silent push 2025-01-21 10:23:48 +01:00
Keyan
355abc7221
Merge pull request #1831 from stackernews/remove-msats-warning
Remove msats warning
2025-01-20 19:01:09 -06:00
ekzyis
181cb87c18 Remove warning if wallet does not support msats 2025-01-21 00:28:11 +01:00
Keyan
0c0fdfb63b
Update awards.csv 2025-01-20 15:51:04 -06:00
Keyan
020b914d0d
Update awards.csv 2025-01-20 15:32:18 -06:00
Keyan
0a83a88e06
Merge pull request #1828 from stackernews/fix-cc-info-typo
Fix typo in CC info
2025-01-20 12:04:56 -06:00
ekzyis
ac7bd5df7e Fix typo in CC info 2025-01-20 15:02:03 +01:00
Keyan
1057fcc04d
Merge pull request #1827 from stackernews/welcome
Script for welcome series
2025-01-19 19:30:49 -06:00
ekzyis
c6de7a1081 Script for welcome series 2025-01-19 23:56:19 +01:00
Keyan
566a5f9675
Merge pull request #1825 from Soxasora/fix_sw_uncaught_promise
fix: uncaught promise on getOS causes onMessage event to end early
2025-01-18 11:43:17 -06:00
Soxasora
1cbf5ab871 fix: uncaught promise on getOS causes event to end early 2025-01-18 18:10:32 +01:00
Keyan
a895a91277
Merge pull request #1823 from stackernews/update-forward-limits
Update forward limits
2025-01-18 09:29:16 -06:00
ekzyis
630af6bc40 Update forward limits 2025-01-18 09:53:17 +01:00
k00b
d9932e0a27 add max fee bullet to cc modal 2025-01-14 14:43:43 -06:00
Keyan
3ce9d7339c
Merge pull request #1817 from stackernews/daily-rewards-refill-job
bounty updates and auto-rewards refill
2025-01-14 10:57:20 -06:00
k00b
d688e9fce8 automate daily rewards 2025-01-13 19:59:05 -06:00
k00b
5c1129c98f update bounty bots 2025-01-13 17:47:42 -06:00
k00b
8905868a62 fix typo in CC modal 2025-01-13 15:06:03 -06:00
k00b
c40ef5a1c1 fix daily rewards link in reward sats modal 2025-01-12 15:47:21 -06:00
Keyan
4520c91179
cowboy credit modal explainers wherever they are referenced (#1815) 2025-01-12 15:15:53 -06:00
Keyan
90ccbc58b1
Update awards.csv 2025-01-11 19:14:27 -06:00
Keyan
8d548ce152
Update awards.csv 2025-01-11 19:13:29 -06:00
Keyan
2137dacf14
Update awards.csv 2025-01-11 19:11:54 -06:00
k00b
7daf688ea3 remove p2p zap notification indicator fixing 'phantom' notifications 2025-01-08 18:10:14 -06:00
ekzyis
5261c83f4d
Add FAQ to version control (#1799)
* Add FAQ to version control

* Add script to deploy markdown content

* Move faq.md into docs/user
2025-01-08 13:47:34 -06:00
soxa
466620ff05
fix: zapping too fast causes duplicate notifications (#1812)
* fix: zapping too fast causes duplicate notifications

* add comment

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-01-08 12:51:07 -06:00
soxa
44d3748d5f
fix: iOS PWA push notifications (#1794)
* fix: iOS pwa push notifications

* fix lint

* align onNotificationClick promises to event.waitUntil

* align CLEAR_NOTIFICATIONS promises to event.waitUntil

* include notifications url for merged payloads

* hotfix: track amount via notification.length

* better comments

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2025-01-08 12:40:25 -06:00
Riccardo Balbo
511b0eea40
fix tooltip blinking at the edges (#1813) 2025-01-07 08:18:35 -06:00
ekzyis
b37a12bf71
Use sats / ccs format in item details (#1805)
* Use sats / ccs format in item details

* Use row-gap and line-height
2025-01-06 17:22:56 -06:00
ekzyis
c74107269d
Fix job edits (#1811) 2025-01-06 17:20:14 -06:00
k00b
29c31e3d4a double allowed outgoing cltv delta 2025-01-05 16:17:22 -06:00
Riccardo Balbo
0750a7b197
add hasSendWallet to bounty pay (#1806) 2025-01-05 11:26:18 -06:00
Riccardo Balbo
95950cdff8
pass hasSendWallet to ItemAct actor (#1804) 2025-01-05 11:25:49 -06:00
k00b
529b5d1fef fix #1795 2025-01-04 19:31:25 -06:00
soxa
a04647d304
fix: iOS check, proper AMOUNT count on iOS (#1796) 2025-01-04 19:18:15 -06:00
ekzyis
34175babb2
Fix scroll after modal close (#1798) 2025-01-04 15:32:45 -06:00
k00b
3fc1291600 fix missing sender wallet flag 2025-01-03 13:51:28 -06:00
Keyan
146b60278c
cowboy credits (aka nov-5 (aka jan-3)) (#1678)
* wip adding cowboy credits

* invite gift paid action

* remove balance limit

* remove p2p zap withdrawal notifications

* credits typedefs

* squash migrations

* remove wallet limit stuff

* CCs in item detail

* comments with meCredits

* begin including CCs in item stats/notifications

* buy credits ui/mutation

* fix old /settings/wallets paths

* bios don't get sats

* fix settings

* make invites work with credits

* restore migration from master

* inform backend of send wallets on zap

* satistics header

* default receive options to true and squash migrations

* fix paidAction query

* add nav for credits

* fix forever stacked count

* ek suggested fixes

* fix lint

* fix freebies wrt CCs

* add back disable freebies

* trigger cowboy hat job on CC depletion

* fix meMsats+meMcredits

* Update api/paidAction/README.md

Co-authored-by: ekzyis <ek@stacker.news>

* remove expireBoost migration that doesn't work

---------

Co-authored-by: ekzyis <ek@stacker.news>
2025-01-03 10:33:07 -06:00
ekzyis
47debbcb06
Fix insecure default id for invites (#1789)
* Fix insecure default id for invites

* Use 16 bytes
2025-01-03 09:42:28 -06:00
ekzyis
077727dced
Fix expirein used instead of keepuntil (#1788)
* Fix expirein used instead of keepuntil

* Fix existing boost jobs
2025-01-03 08:52:34 -06:00
ekzyis
d53bc09773
Distinguish invoices cancelled by user (#1785) 2025-01-02 11:53:05 -06:00
ekzyis
6a02ea8c5c
Allow cancel of own invoices without hmac (#1787) 2025-01-02 10:35:54 -06:00
soxa
0ca9596310
enhance: collapse childless deleted comments (#1786) 2025-01-01 13:07:32 -06:00
ekzyis
ba2ac2c94e
Fix ignored cancel error (#1784) 2025-01-01 11:18:54 -06:00
ekzyis
4623743c8f
fix cookie pointer override during account switching (#1783) 2024-12-31 13:05:20 -06:00
ekzyis
a41d077c21
Fix muted parents not uncollapsed (#1775)
* Fix parents not uncollapsed of router.query.commentId

* Fix code comment

* Use descriptive fragment name

* Add cache to effect dependencies

not sure if needed since also works without but probably safer to have than not
2024-12-31 10:24:59 -06:00
ekzyis
ba2cdc2275
Fix NostrAuth link text (#1782) 2024-12-31 08:40:30 -06:00
Riccardo Balbo
1972af1fd9
filter out POST /api/graphql spam from sndev logs (#1780)
* filter out POST /api/graphql spam from sndev logs

* don't use -P in grep
2024-12-30 18:49:54 -06:00
ekzyis
bec27a5a0e
Fix user since if first item wasn't paid (#1773) 2024-12-30 16:33:17 -06:00
k00b
4262189c91 fix week interval in newsletter 2024-12-28 15:50:17 -06:00
k00b
211e3c136b fix lint 2024-12-28 11:16:59 -06:00
CypherCosmo
43129f7045
fit missing signup button spacing
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-12-28 10:47:57 -06:00
ekzyis
c89220dbde
Add exclusion constraint to prevent duplicate items within 10m (#1747)
* Add exclusion constraint to prevent duplicate items within 10m

* Fix missing extension

* More user-friendly error message

* Use MD5 for slightly better performance

* Always use MD5 for columns of type TEXT

* shift constraint into the future

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-12-28 10:33:53 -06:00
ekzyis
9783df8e3b
Require invite limit (#1748)
* Fix invite limit required

* Fix redeeming of unlimited invites

* Require invite limit
2024-12-28 09:28:05 -06:00
k00b
74f771adf4 fix #1754 2024-12-27 19:06:30 -06:00
Keyan
eb22fdab41
make possibly conflicting optimistic cache updates write to each other (#1772)
* make possibly conflicting optimistic cache updates write to each other

* always update optimistic caches with cache.modifying items
2024-12-27 14:52:32 -06:00
Keyan
18445b1dc1
fix 1695 by not updating ancestors on zap (#1766)
* fix 1695 by not updating ancestors on zap

* update ancestors but only onPaid
2024-12-27 10:19:49 -06:00
Riccardo Balbo
6efa782c11
fix typo (#1764) 2024-12-26 13:10:15 -06:00
ekzyis
4ad54d6f5a
Fix typo in deposit notification (#1765) 2024-12-26 13:10:02 -06:00
k00b
1b1045fcd9 increase pull refresh distance 2024-12-23 19:50:51 -06:00
Keyan
9490520925
renerf ek and I (#1760) 2024-12-23 19:17:32 -06:00
Keyan
2fc529419e
reduce territory price with prorated refund for actives (#1761) 2024-12-23 18:50:35 -06:00
ekzyis
b4352e74b4
Fix echoed message about mining blocks (#1759) 2024-12-23 13:15:37 -06:00
ekzyis
0ca37038dd
Fix upvote button shown for deleted items (#1753) 2024-12-20 18:16:24 -06:00
Keyan
f5fb88342b
update cache on vault key update (#1752)
* update cache on vault key update

* Update components/vault/use-vault-configurator.js

Co-authored-by: ekzyis <ek@stacker.news>

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-12-20 18:15:06 -06:00
Riccardo Balbo
6ad7a3cf60
add ofelia cron for wallets and force cln to reconnect (#1746) 2024-12-19 17:02:27 -06:00
ekzyis
fdbe14d195
Fix send-only wallet save after device sync enabled (#1732)
* Rename walletData to recvConfig

* Use nested upsert during wallet update
2024-12-19 11:31:11 -06:00
Riccardo Balbo
e4ca2d6e07
Refine #1739 and fix regression causing nostr crossposts and login to not work (#1740)
* Refine #1739 and fix regression causing crossposts and login to not work

* use temp nostr instance for signing
2024-12-19 08:27:45 -06:00
ekzyis
4db2edb1d9
Close relay connections after each NWC call (#1739) 2024-12-19 00:11:03 -06:00
ekzyis
d3a705d3ad
Fix admin edits (#1737) 2024-12-18 18:28:18 -06:00
k00b
b8061a630c don't double factor trust 2024-12-18 18:27:38 -06:00
k00b
16b7160d36 increase create invoice timeout 2024-12-18 13:47:03 -06:00
k00b
faeefdc498 increase send payment timeout 2024-12-18 12:09:07 -06:00
Keyan
6d4dfddae8
improve rewards (#1731)
* don't bias to early zapping so much

* untested rewards/leaderboard changes

* fix cln dep for payments

* make zap proportion scale using quad root

* fix for missing proportion on hidden users

* improve rewards cutoff criteria

* Update api/resolvers/user.js

Co-authored-by: ekzyis <ek@stacker.news>

* Update api/typeDefs/user.js

Co-authored-by: ekzyis <ek@stacker.news>

* improve switch readability

* small increase in min zap

* refresh materialized views on migration

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-12-18 10:12:11 -06:00
ekzyis
6098d39574
Fix missing logs on save (#1729)
* Fix missing logs on save

* fix receive logs wrt device sync

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-12-16 17:37:31 -06:00
ekzyis
62a922247d
Add timeouts to all wallet API calls (#1722)
* Add timeout to all wallet API calls

* Pass timeout signal to wallet API

* Fix timeout error message not shown on timeout

* Fix cross-fetch throws generic error message on abort

* Fix wrong method in error message

* Always use FetchTimeoutError

* Catch NDK timeout error to replace with custom timeout error

* Also use 15s for NWC connect timeout

* Add timeout delay
2024-12-16 14:05:31 -06:00
ekzyis
819d382494
Fix lightning address logs deletion (#1728) 2024-12-15 11:14:33 -06:00
ekzyis
14de23b21d
Refactor CLN function signatures (#1726) 2024-12-14 10:32:51 -06:00
ekzyis
3cdfe620d0
Refactor Blink function signatures (#1725)
This makes them consistent with function signatures of other wallets
2024-12-14 08:56:45 -06:00
ekzyis
77d22cfd77
Remove unused wallet context args (#1724) 2024-12-14 08:55:08 -06:00
Riccardo Balbo
bdd24130f9
Nip46 auth with NDK (#1636)
* ndk

* fix: remove duplicated zap note event template

* don't init Nip07 signer by default

* Update wallets/nwc/server.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* nwc protocol parsing workaround

* WebSocket polyfill for worker

* increase nwc timeout

* remove NDKNip46Signer type

* fix type annotation

* move  eslint-disable camelcase to the top

* pass event args to the constructor

* fix error handling

* Update wallets/nwc/index.js

Co-authored-by: ekzyis <ek@stacker.news>

* nip46 auth

* style tweak, remove unmaintained signers from the list

* don't use modal

* workaround url parsing

* use kind 27235

* add kind 27235 metadata

* show suggestion after a timeout

* Update lib/nostr.js

Co-authored-by: ekzyis <ek@stacker.news>

* Update components/nostr-auth.js

Co-authored-by: ekzyis <ek@stacker.news>

* fix unrelated lnauth crash when closing ext prompt

* make ui consistent ...

* give buttons spacing

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-12-13 20:25:34 -06:00
ekzyis
285203889d Remove unused import 2024-12-13 21:20:51 +01:00
ekzyis
a50a2c8bd1
Fix receiver fallbacks depend on fast polls (#1723) 2024-12-13 14:19:00 -06:00
Riccardo Balbo
d73f6323ff
NDK (#1590)
* ndk

* fix: remove duplicated zap note event template

* don't init Nip07 signer by default

* Update wallets/nwc/server.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* nwc protocol parsing workaround

* WebSocket polyfill for worker

* increase nwc timeout

* remove NDKNip46Signer type

* fix type annotation

* move  eslint-disable camelcase to the top

* pass event args to the constructor

* fix error handling

* Update wallets/nwc/index.js

Co-authored-by: ekzyis <ek@stacker.news>

* Fix type annotation

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-12-13 13:28:36 -06:00
Riccardo Balbo
52734940a3
Bolt12 dev environment (#1702)
* lndk-eclair bolt12 test environment

* use static certs for lndk dev

* move eclair/lndk/cln to wallets profile, force lndk onto x86 platform

* fix port conflict

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-12-13 12:30:30 -06:00
Keyan
bf541aa643
Merge pull request #1721 from stackernews/fix-missing-usage-of-timeout-error
Fix missing usage of new TimeoutError
2024-12-13 10:29:50 -06:00
ekzyis
06a3a71eb9 Use new TimeoutError 2024-12-13 16:29:00 +01:00
Keyan
adfbdeddba
Merge pull request #1720 from stackernews/fix-toast-padding
Fix missing toast padding
2024-12-13 08:39:45 -06:00
ekzyis
fc4303658d
Use AbortSignal.timeout + custom timeout error message (#1718)
* refactor: replace custom logic with AbortSignal.timeout

* Use custom timeout error message

* Include method and url in fetch timeout error

* Fix error not rethrown
2024-12-13 08:38:42 -06:00
Keyan
e8434d07c5
Merge pull request #1719 from stackernews/fix-missing-save-error-log
Fix missing logging of sender wallet validation error
2024-12-13 08:36:37 -06:00
ekzyis
959cd1f3f4 Fix missing toast padding 2024-12-13 15:03:04 +01:00
ekzyis
64ba7a56cb Fix missing wallet error logging 2024-12-13 11:30:54 +01:00
Riccardo Balbo
ec213907fc
Use debian cdn instead of de mirror (#1715)
* Use debian cdn instead of de mirror

* use https in deb mirror
2024-12-12 10:30:39 -06:00
ekzyis
66b7352bf0
Fix forever edits (#1716)
* Fix forever edits

* Refactor edit check on server
2024-12-12 09:35:30 -06:00
Keyan
6918bcb452
Merge pull request #1714 from stackernews/1704-fix-exposed-routing-fees
Don't expose p2p zap receivers to routing fees
2024-12-11 15:47:57 -06:00
k00b
98fae6c9ae prioritize payment reliability 2024-12-11 15:38:38 -06:00
Keyan
0e765d4179
Merge pull request #1709 from stackernews/fix-client-pending-forwards-paid
Fix pending forwards considered paid by client
2024-12-11 14:43:33 -06:00
Keyan
3fe5f4b435
Merge pull request #1713 from stackernews/shared-failed-forwards-view 2024-12-11 14:42:31 -06:00
ekzyis
c9439c33c6 Don't expose p2p zap receivers to routing fees 2024-12-11 20:55:43 +01:00
ekzyis
8f092cdc66 Use consistent view about failed forwards 2024-12-11 20:09:49 +01:00
ekzyis
4e6fb40c0b Use conditional waitFor to fix premature payment success 2024-12-11 19:27:29 +01:00
ekzyis
8cb89574ae Fix pending forwards considered paid by client 2024-12-11 14:39:01 +01:00
ekzyis
756e75ed7c
Fix latest timestamp not updated (#1705) 2024-12-10 15:36:04 -06:00
ekzyis
a46f81f1e1
Receiver fallbacks (#1688)
* Use same naming scheme between ln containers and env vars

* Add router_lnd container

* Only open channels to router_lnd

* Use 1sat base fee and 0ppm fee rate

* Add script to test routing

* Also fund router_lnd wallet

* Receiver fallbacks

* Rename to predecessorId

* Remove useless wallet table join

* Missing renaming to predecessor

* Fix payment stuck on sender error

We want to await the invoice poll promise so we can check for receiver errors, but in case of sender errors, the promise will never settle.

* Don't log failed forwards as sender errors

* fix check for receiver error

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-12-10 14:15:29 -06:00
Keyan
e6c74c965b
Update awards.csv 2024-12-10 11:15:10 -06:00
ekzyis
3ead4db8dc
Use new LND node as routing node (#1700)
* Use same naming scheme between ln containers and env vars

* Add router_lnd container

* Only open channels to router_lnd

* Use 1sat base fee and 0ppm fee rate

* Add script to test routing

* Also fund router_lnd wallet

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-12-10 10:52:17 -06:00
ekzyis
1a41760915
CLN: Use port 9735 and log to stdout (#1701)
* use port 9735 for cln node

this makes it consistent with the rest of the environment (default port for regtest is 19846)

* cln log to stdout

---------

Co-authored-by: Riccardo Balbo <riccardo0blb@gmail.com>
2024-12-10 09:14:18 -06:00
ekzyis
3e29e04b01
Use same naming scheme between ln containers and env vars (#1698) 2024-12-10 09:13:14 -06:00
k00b
bf20cf8f56 fix different carousels named the exact same thing 2024-12-09 19:06:46 -06:00
k00b
52098a3e50 fix broken static header from carousel 2024-12-09 19:03:30 -06:00
ekzyis
61fb1c445f
Fix header carousel desync (#1696) 2024-12-09 16:09:26 -06:00
Keyan
d05a27a6c3
Update awards.csv 2024-12-09 09:10:49 -06:00
ekzyis
a5fa40aa1b
Fix comment in useSendWallets (#1691) 2024-12-09 09:04:41 -06:00
Riccardo Balbo
080459cd21
Fix lud-18 validation imports (#1690)
* fix lud18PayerDataSchema import

* fix validateSchema import
2024-12-08 12:37:56 -06:00
Keyan
4ec36670c3
Update awards.csv
@felipebueno I can't find a route to your lightning address from SN. Please advise. :)
2024-12-07 15:59:38 -06:00
Keyan
a8f79d59ff
Update awards.csv 2024-12-07 15:48:20 -06:00
ekzyis
ec904e6b4c
Move useInvoice and useQrPayment into own files (#1686) 2024-12-05 08:52:32 -06:00
soxa
3806dc23af
enhance: show an error when uploads are expired (#1685)
* enhance: show an error when uploads are expired

* lint: standardjs
2024-12-05 08:15:08 -06:00
ekzyis
e55af28763
Fix wallet logs delete for single wallet (#1684) 2024-12-05 08:10:29 -06:00
Keyan
713227b255
invite paid action (#1681) 2024-12-04 12:10:30 -06:00
k00b
909853521d item referral threshold 2024-12-02 14:45:15 -06:00
ekzyis
01d5177006
Fix edit timer stuck at 00:00 (#1673)
* Fix edit timer stuck at 00:00

* refactor with useCanEdit hook
2024-12-02 08:18:35 -06:00
k00b
8595a2b8b0 stop probable source of 504 toasts 2024-12-01 17:01:13 -06:00
Riccardo Balbo
7f11792111
Custom invite code and note (#1649)
* Custom invite code and note

* disable autocomplete and hide invite code under advanced

* show invite description only to the owner

* note->description and move unser advanced

* Update lib/validate.js

Co-authored-by: ekzyis <ek@stacker.news>

* Update lib/webPush.js

Co-authored-by: ekzyis <ek@stacker.news>

* Update api/typeDefs/invite.js

Co-authored-by: ekzyis <ek@stacker.news>

* Update pages/invites/index.js

Co-authored-by: ekzyis <ek@stacker.news>

* Update pages/invites/index.js

Co-authored-by: ekzyis <ek@stacker.news>

* fix

* apply review suggestions

* change limits

* Update lib/validate.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* don't show invite id in push notification

* remove invoice metadata from push notifications

* fix form reset, jsx/dom attrs, accidental uncontrolled prop warnings

* support underscores as we claim

* increase default gift to fit inflation

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-12-01 16:31:47 -06:00
soxa
76e384b188
fix can't upload from iOS camera/mov files (#1667)
* fix can't upload from iOS camera/mov files

* pivot: iOS automatically transcodes HEVC mov files to H264, custom error if codec not supported
2024-11-30 19:12:13 -06:00
soxa
0b97d2ae94
fix: top boosts shows others' unpaid boosts (#1647) 2024-11-30 19:06:10 -06:00
Lorenzo
d88971e8e5
Fix: progress bar shown on back navigation through pathname check (#1633)
* fix: progress bar shown on back navigation through pathname check

* fix progress done race

* use router.pathname instead cause it's already there

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-30 19:05:26 -06:00
ekzyis
0837460c53
Fix missing authentication check for invite revocation (#1666)
* Fix missing authentication check for invite revocation

* Toast invite revocation error
2024-11-30 12:08:30 -06:00
Felipe Bueno
55d1f2c952
Introduce SubPopover (#1620)
* Introduce SubPopover

* add truncation to sub description popover

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-29 19:58:18 -06:00
Lorenzo
bd5db1b62e
Fix Territories selector updates without hard-reload (#1619)
* fix: territories select fetches new data on reload

* chore: removed unnecessary extra function

* chore: territories refetched on nsfwMode change

* chore: check for undefined me object on refetch hook

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-11-29 18:59:39 -06:00
Lorenzo
7cb2aed9db
feat: recent unpaid bounties selection (#1589)
* feat: recent unpaid bounties selection

* chore: added checkbox on recent header

* chore: active bounties selection made through a checkbox

* chore: renamed function for better clarity

* chore: fixed active bounties only checkbox alignment

* chore: active-only option passed as query param

* chores: variablesFunc refactoring

* chore: removed type mapping function from recent header

* chore: router replace instead of push

* chore: router retrieved by hook instead of argument

* chore: checkbox starts checked based on url's query param

* more idiomatic react + push instead of replace

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-28 18:47:01 -06:00
ekzyis
8ce2e46519
Merge pull request #1642 from stackernews/sender-fallbacks
Sender fallbacks
2024-11-28 23:00:03 +01:00
k00b
9caeca00df fix cache update option name on qr 2024-11-28 14:23:41 -06:00
ekzyis
799a39b75f
Fix deposit push notifications (#1662) 2024-11-28 11:48:08 -06:00
k00b
404cf188b3 function for merging data after retry 2024-11-28 11:43:40 -06:00
k00b
105f7b07e5 make fallback retry cache updates a special case 2024-11-28 11:22:46 -06:00
k00b
cb028d217c fix zap fallback retries in notifications 2024-11-28 10:48:22 -06:00
k00b
6b59e1fa75 usesendwallets 2024-11-27 19:39:20 -06:00
k00b
f89286ffdf make logger use full wallet 2024-11-27 19:10:00 -06:00
ekzyis
6c3301a9c4 Fix missing item invoice update on failure 2024-11-28 01:53:45 +01:00
k00b
67f6c170aa readability improvements 2024-11-27 18:39:23 -06:00
ekzyis
f9169c645a Fix [undefined] in logs 2024-11-28 01:36:29 +01:00
ekzyis
a0d33a23f3 Fix wallet save 2024-11-28 01:16:30 +01:00
k00b
b608fb6848 refactor out array of hooks 2024-11-27 17:31:08 -06:00
Keyan
61395a3525
Merge branch 'master' into sender-fallbacks 2024-11-27 17:17:11 -06:00
ekzyis
5e76ee1844
Remove unused useWallet from QR code component (#1660) 2024-11-27 17:16:44 -06:00
ekzyis
7a8db53ecf Only retry same receiver if forward did not fail 2024-11-27 23:00:27 +01:00
ekzyis
b301b31a46 Create wrapped invoices on p2p zap retries 2024-11-27 23:00:27 +01:00
ekzyis
9cfc18d655 Return latest state of paid or failed invoice 2024-11-27 23:00:27 +01:00
ekzyis
68513559e4 Remove unnecessary error handling in LNC
We already wrap sendPayment with our own error handling so these errors will become instances of WalletPaymentErrors anyway
2024-11-27 23:00:27 +01:00
ekzyis
a4144d4fcc Fix missing item invoice update for optimistic actions 2024-11-27 23:00:27 +01:00
ekzyis
0051c82415 Fix invoice retry even if no payment was attempted 2024-11-27 23:00:27 +01:00
ekzyis
14a92ee5ce Fix filter for wallets that can send
Testing for canSend is not enough since it can also return true if the wallet is not enabled.

This is the case because we want to allow disabling wallets but canSend must still return true in this case if send is configured.

This should probably be changed such that canSend only returns true when the wallet is enabled without preventing disabling of wallets.
2024-11-27 23:00:27 +01:00
ekzyis
b1cdd953a0 Return last attempted invoice in canceled state 2024-11-27 23:00:27 +01:00
ekzyis
7e25e29507 Show aggregated wallet errors in QR code 2024-11-27 23:00:27 +01:00
ekzyis
7f5bb33073 Fix payment method returned by retries 2024-11-27 23:00:27 +01:00
ekzyis
be4ce5daf9 Allow retries of pessimistic actions 2024-11-27 23:00:27 +01:00
ekzyis
1f2b717da9 Fix last wallet not returning new invoice 2024-11-27 23:00:27 +01:00
ekzyis
00f9e05dd7 Ignore wallet configuration errors in QR code 2024-11-27 23:00:27 +01:00
ekzyis
974e897753 Add comment when err.newInvoice is not set 2024-11-27 23:00:27 +01:00
ekzyis
517d9a9bb9 Abort payment on unexpected errors 2024-11-27 23:00:27 +01:00
ekzyis
413f76c33a Refactor wallet error handling with inheritance 2024-11-27 23:00:27 +01:00
ekzyis
ed82d9cfc0 Fix payments if recv-only wallet enabled 2024-11-27 23:00:27 +01:00
ekzyis
d99caa43fc Remove unnecessary sort 2024-11-27 23:00:27 +01:00
ekzyis
7036804c67 Fix old invoice passed to QR code 2024-11-27 23:00:27 +01:00
ekzyis
7742257470 Fix TypeError 2024-11-27 23:00:27 +01:00
ekzyis
bc0c6d1038 Fix SenderError name 2024-11-27 23:00:27 +01:00
ekzyis
5218a03b3a sender fallbacks 2024-11-27 23:00:27 +01:00
ekzyis
2b47bf527b Fix comment 2024-11-27 23:00:27 +01:00
ekzyis
35159bf7f3 Remove unused useWallet from QR code component 2024-11-27 22:54:00 +01:00
soxa
707d7bdf8b
fix: cannot add images above text (#1659)
* fix: cannot add images above text

* make sure there are always 2 newlines on either side of media

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-11-27 15:31:46 -06:00
Riccardo Balbo
e05989d371
Improve LNC realiability by being nicer (#1658)
* be nicer to lnc

* decrease timeout to 4 seconds
2024-11-27 12:46:40 -06:00
Riccardo Balbo
6630899e79
use flexbox for wallet card header and make logos more consistent (#1654)
* use flexbox for wallet card header

* make wallet logo consistent

* remove extra div

* Update styles/wallet.module.css

Co-authored-by: ekzyis <ek@stacker.news>

* Update styles/wallet.module.css

Co-authored-by: ekzyis <ek@stacker.news>

* resize wallet banner

* remove unused justify-content

* remove cardMeta

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-27 12:14:00 -06:00
ekzyis
a032da57b9
Wallet filters (#1627)
* Add wallet filters

* Fix grid layout shift

* Store filter state in query params

* Use auto-fill instead of auto-fit

This doesn't seem to change anything but this is closer to our intention how the grid should work with fixed column width.

* Use same order for filters as icons in card

* Use state update function

* Use user-select: none for wallet filters

* Remove unnecessary '|| false'

* Add media query to keep centered grid layout on small screens

* Decrease wallet filter margin-top to 1rem

* fix wallet support usage

* improve grid

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-27 11:39:30 -06:00
Keyan
0bff478d39
direct receives and send paid action (#1650)
* direct receives and send paid action

* remove withdrawl->invoiceForward has many relationship

* fix formatMsats implicit type expectations

* ui + dropping direct payment bolt11s

* squash migrations

* fix bolt11 dropping and improve paid action wallet logging

* remove redundant sender id

* fix redirect when funding account over threshold

* better logging
2024-11-27 07:39:05 -06:00
ekzyis
8b5e13236b
Fix inconsistent actionArgs on retry (#1651) 2024-11-26 07:39:05 -06:00
ekzyis
b0207a2906
Create useWalletStatus and useWalletSupport (#1646) 2024-11-23 20:19:22 -06:00
ekzyis
f5569d7444
Only fetch logs when we need them (#1638)
* Don't fetch logs as anon

* Only fetch logs if we need them on the current page

* Wait for poll to finish with setTimeout

This makes sure that we wait for the pending poll to finish before we poll again. This prevents running multiple polls at the same time on slow connections.

I noticed we don't need to queue a new poll ourselves since a poll updates effect dependencies so we will cleanup and run the effect again anyway.

* Fix polling via useEffect abuse
2024-11-23 18:37:30 -06:00
ekzyis
cb9947e4f2
refactor: Check darkmode in useWallets (#1640)
* Check darkmode in useWallets

* Check darkmode in useWalletImage
2024-11-23 17:59:59 -06:00
ekzyis
3023d8a5d3
Remove unused function usePayment (#1641) 2024-11-23 12:21:39 -06:00
Riccardo Balbo
a7ea380476
fix LNC attachment and docs (#1639) 2024-11-22 18:03:02 -06:00
Riccardo Balbo
83341d5b7d
add in error message how high are estimated fees when too high (#1634) 2024-11-22 08:26:18 -06:00
ekzyis
923ddb74ca
Use invoice.cancelledAt to determine if invoice expired or was canceled by user (#1631)
* Set and resolve invoice.cancelledAt

* Don't close modal if invoice expired
2024-11-22 08:25:20 -06:00
k00b
96e5c6c51c fix #1628 2024-11-21 08:52:24 -06:00
Riccardo Balbo
eb3efbef57
HOTFIX: restore missing link between lnurlp and nip57 (#1630)
* restore missing link between lnurlp and nip57

* pass noteStr as receive action arg

* make sure the desc field is not updated if noteStr is unset
2024-11-21 08:46:28 -06:00
ekzyis
277f8a20fb
Fix lnaddr not returning payment request (#1626) 2024-11-20 19:57:16 -06:00
ekzyis
7448cef932
Fix wallet status ignoring failed requests to create invoices (#1624) 2024-11-20 16:26:50 -06:00
Riccardo Balbo
d238eafdde
fix typo signular -> singular (#1623) 2024-11-20 16:26:18 -06:00
k00b
09bacdc016 catch wrap invoice error as noninvoiceable 2024-11-20 14:47:22 -06:00
Keyan
67a0565c04
Update awards.csv 2024-11-20 13:58:55 -06:00
Keyan
cfbb274d8f
Update awards.csv 2024-11-20 13:58:25 -06:00
Keyan
b012f7670e
Update awards.csv 2024-11-20 13:57:17 -06:00
Riccardo Balbo
21207184dd
refresh cln certs (#1622) 2024-11-20 12:48:03 -06:00
ekzyis
f45144cb1c
Always check res.ok and content-type header (#1621)
* Always check res.ok and content-type header

* Fix blink body consumed before we use it

* Always consume response body to avoid memory leaks
2024-11-20 12:46:39 -06:00
Simone Cervino
c88afc5aae
fix can't upload mp4 on safari (#1617) 2024-11-20 07:06:05 -06:00
ekzyis
6bae1f1a89
Fix account switching anon login (#1618)
* Always switch to user we just logged in as

If we're logged in and switch to anon and then use login to get into our previous account instead of using 'switch accounts', we only updated the JWT but we didn't switch to the user.

* Fix getToken unaware of multi-auth middleware

If we use login with new credentials while switched to anon (multi_auth.user-id === 'anonymous'), we updated the pubkey because getToken wasn't aware of the switch and thus believed we're logged in as a user.

This is fixed by applying the middleware before calling getToken.
2024-11-20 07:05:42 -06:00
ekzyis
82fead60f1
Fix mentions for pessimistic actions (#1615)
* Fix mentions for pessimistic actions

* (item)mentions should use tx not models

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-19 19:28:21 -06:00
k00b
5dac2f2ed0 fix #1566 2024-11-19 19:12:18 -06:00
Keyan
9179688abc
Merge pull request #1614 from stackernews/wallet-fixes
Wallet fixes
2024-11-19 15:49:12 -06:00
k00b
66ec5d5da8 dark/light mode images on wallet pages 2024-11-19 15:46:11 -06:00
k00b
f3cc0f9e1d make recv optional 2024-11-19 15:38:27 -06:00
k00b
aa4c448999 fix account switch disconnect 2024-11-19 14:58:48 -06:00
Keyan
8441e04d3e
Update awards.csv 2024-11-19 12:55:55 -06:00
ekzyis
bcd229af93
Fix 'Cannot mix BigInt and other types' on zaps (#1609)
* Fix 'Cannot mix BigInt and other types' on zaps

* Also add sybilFeePercent to context in worker

* add paymentMethod to context

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-11-19 09:31:26 -06:00
ekzyis
c2dc0be1c1
Always use custom PTR on Android (#1607) 2024-11-18 19:13:52 -06:00
k00b
5d71e57839 fix ptr interfering with layout container 2024-11-18 17:33:45 -06:00
ekzyis
570c842934
Wallet send+recv status derived from logs (#1559)
* Derive wallet status from logs

* Add send/recv icons

* Set status individually for send and recv

* Move status logic into own function

* Add LNbits, Blink, CLN, LND, phoenixd logo

* Fix wallet.status.any not using Status enum

* Fix WebLN being weird

* Use phoenixd logo with text

* Also use wallet logo on config page

* Also poll logs for wallet status

* Use logger.info for logs not relevant for wallet status

* Remove no longer used wallet badges

* Crop LND logo like other logos

* Fix all wallets show 'configure'

* Fix wallet status not respecting enabled

* Fix wallet.def.requiresConfig undefined

* Fix banner shown for WebLN

* Fix attach shown when configured

* Filter by context.status to determine wallet status

* Fix +- shown without context

* Fix missing theme support for wallet logos
2024-11-18 16:46:24 -06:00
Keyan
62cf5b9c34
Merge pull request #1603 from stackernews/fix-dark-mode-update
Fix missing useDarkMode update
2024-11-18 16:09:16 -06:00
ekzyis
120dd4122f Fix missing useDarkMode update 2024-11-18 23:01:12 +01:00
Keyan
c3b7ad3fdd
Merge pull request #1599 from Soxasora/feat_pwa_pull_to_refresh
feat: PWA pull to refresh
2024-11-18 13:11:56 -06:00
k00b
d2b5d23af5 add body margin on ptr 2024-11-18 13:05:51 -06:00
Soxasora
a992426058 enhance: cleanup, ux/ui changes, safer approach to android's ptr 2024-11-18 15:49:54 +01:00
Keyan
cce7195652
Merge pull request #1600 from stackernews/update-pr-template
Also ask if tested on light/dark mode in pull request template
2024-11-17 16:16:10 -06:00
ekzyis
deccb0fea9 Also ask if tested on light/dark mode 2024-11-17 22:58:28 +01:00
Soxasora
5a359feeed test: togglable Android's native PTR 2024-11-17 21:58:40 +01:00
Simone Cervino
c08088bdbe
fix uploaded videos don't load on safari (#1593)
* fix uploaded videos don't load on safari

* fix safari loading video as image, min-content restored

* refinements for ssr and skip load check for images

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-17 13:38:11 -06:00
Soxasora
eea121e30c feat: pull-to-refresh for PWA without native refresh 2024-11-17 20:23:26 +01:00
Lorenzo
83e72e21cc
fix: reply storage is updated with the new content on file upload (#1585)
* fix: reply storage is updated with the new content on file upload

* Revert "fix: reply storage is updated with the new content on file upload"

This reverts commit 350931fd0c7a47ffe59716722755ab294c481b71.

* chore: reworked image draft save by using events

* chore: helpers.setValue called just after setNativeValue

* chore: updated setNativeValue function to be more use-case specific
2024-11-16 17:23:07 -06:00
Lorenzo
8a2bd84f69
fix: upvote widget not rendered when comment is collapsed (#1583)
* fix: upvote widget not rendered when comment is collapsed

* fix: restored missing conditional on handleShortPress

* chore: icon horizontal space maintained even if the comment is collapsed

* chore: 'rendered' argument renamed to 'visible'

* chore: collapsed condition merged with the 'disabled' variable

* reduce unecessary code

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-16 17:04:49 -06:00
k00b
73df5e0308 improve newsletter import 2024-11-16 16:52:08 -06:00
ekzyis
5631d6acf6
Fix #undefined in downzap invoice description (#1597) 2024-11-16 14:58:33 -06:00
ekzyis
79ada2ab58
Fix unpaid items are counted (#1595)
* Fix unpaid items are counted

* Also fix for ncomments

* Never count unpaid items
2024-11-15 20:02:15 -06:00
Riccardo Balbo
9c55f1ebe2
Implement deposit as receive paidAction (#1570)
* lnurlp paid action

* lnurlp has 10% sybil fee

* fix merge issue

* Update pages/settings/index.js

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>

* fix notifications

* fix destructure

* pass lud18Data to lnurlp action

* minor cleanup

* truncate invoice description to permitted length

* remove redundant targetUserId

* lnurlp paidAction -> receive paidAction

* remove redundant user query

* improve determining if peer is invoiceable

* fix inconsistent relative imports

* prevent paying self-proxied invoices and better held invoice cancellation

* make gun/horse streak zap specific

* unique withdrawal hash should apply to confirmed payments too

* prevent receive from exceeding wallet limits

* notifications

* fix notifications & enhance invoice/withdrawl page

* notification indicator, proxy receive based on threshold, refinements

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-15 18:38:14 -06:00
ekzyis
8c43caed80
Fix TypeError if local storage is cleared (#1594) 2024-11-15 12:02:02 -06:00
ekzyis
b7fc0e0e74
Fix fee added to received amount (#1582) 2024-11-12 19:50:15 -06:00
ekzyis
08c29b9f10
Fix missing sorting of wallet logs (#1581) 2024-11-12 19:48:21 -06:00
ekzyis
3044c2e98f
Fix outlawed can show up on front page (#1565)
If an item has a lot of comments, it gets ranked higher.

When an item is now downzapped, it can become outlawed but still show up on the front page because of the comments.

This fixes it by filtering outlawed items out instead of relying on the ranking to make them not show up.
2024-11-12 19:47:14 -06:00
Keyan
a44d0daf09
paid action payment methods as an array (#1584)
* introduce fee credits & allow paid actions to specify payment method priority

* fix merge issue

* express supported paid action payment methods as an array

* log force payment method skipping methods

* fix stuff

* immutable context

* immutable paidAction context and other fixes

---------

Co-authored-by: Riccardo Balbo <riccardo0blb@gmail.com>
2024-11-12 19:00:51 -06:00
k00b
d1c770dbbc noop walletLogger if wallet isn't provided 2024-11-12 09:22:51 -06:00
k00b
cb8cce77f0 don't let args overwrite withdrawal/deposit checking params 2024-11-12 08:50:54 -06:00
Riccardo Balbo
7e5a8310df
fix lnd hostname (#1580) 2024-11-11 17:52:15 -06:00
Riccardo Balbo
18700b4201
configurable sybil fee (#1577)
* configurable sybil fee

* document getSybilFeePercent

* fixes

* remove null check

* refine at the margins

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-11-11 16:59:52 -06:00
ekzyis
fdd34b2eb3
Fix edit countdown on deleted items (#1571)
* Clarify conditions to show edit countdown

* Fix edit countdown shown for deleted items

* refactor: Minimize canEdit state

I noticed that only anonEdit requires state because it needs to use useEffect to fetch from local storage.

The other conditions can simply be checked during render.

* refactor: Use datePivot for edit countdown
2024-11-11 09:23:08 -06:00
ekzyis
406ae81693
Fixes around account switching / authentication (#1575)
* Fix missing page reload after account switch on logout

* Fix missing key

* Explain why we set multi_auth cookies on login/signup

* Fix 500 if multi_auth cookie missing
2024-11-11 09:16:32 -06:00
Keyan
4675a2c29d
sndev cleanup (#1563)
* begin sndev cleanup

* cleanup sndev

* fix posix shell compliance

* add tests to sndev
2024-11-09 15:52:04 -06:00
Keyan
c31cf97288
fix precedence of op for not full hot sort (#1564) 2024-11-09 14:25:52 -06:00
ekzyis
d06f4ae70d
Fix offcanvas not closed before showing logout prompt (#1561) 2024-11-09 14:00:36 -06:00
ekzyis
afb71012af
Only send push notification if referrer was updated (#1562) 2024-11-09 14:00:07 -06:00
ekzyis
72e2d19433
supercharged wallet logs (#1516)
* Inject wallet logger interface

* Include method in NWC logs

* Fix wrong page total

* Poll for new logs every second

* Fix overlapping pagination

* Remove unused total

* Better logs for incoming payments

* Use _setLogs instead of wrapper

* Remove inconsistent receive log

* Remove console.log from wallet logger on server

* Fix missing 'wallet detached' log

* Fix confirm_withdrawl code

* Remove duplicate autowithdrawal log

* Add context to log

* Add more context

* Better table styling

* Move CSS for wallet logs into one file

* remove unused logNav class
* rename classes

* Align key with second column

* Fix TypeError if context empty

* Check content-type header before calling res.json()

* Fix duplicate 'failed to create invoice'

* Parse details from LND error

* Fix invalid DOM property 'colspan'

* P2P zap logs with context

* Remove unnecessary withdrawal error log

* the code assignment was broken anyway
* we already log withdrawal errors using .catch on payViaPaymentRequest

* Don't show outgoing fee to receiver to avoid confusion

* Fix typo in comment

* Log if invoice was canceled by payer

* Automatically populate context from bolt11

* Fix missing context

* Fix wrap errors not logged

* Only log cancel if client canceled

* Remove unused imports

* Log withdrawal/forward success/error in payment flow

* Fix boss not passed to checkInvoice

* Fix TypeError

* Fix database timeouts caused by logger

The logger shares the same connection pool with any currently running transaction.

This means that we enter a classic deadlock when we await logger calls: the logger call is waiting for a connection but the currently running transaction is waiting for the logger call to finish before it can release a connection.

* Fix cache returning undefined

* Fix typo in comment

* Add padding-right to key in log context

* Always use 'incoming payment failed:'

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-11-08 13:26:40 -06:00
k00b
f1b2197d31 find lightning module relative to project directory 2024-11-08 09:37:45 -06:00
ekzyis
d6916fa3f4
Merge pull request #1557 from stackernews/fix-invoice-waiting
fix invoice waiting
2024-11-08 05:25:05 +01:00
k00b
dcab8e1365 fix invoice waiting 2024-11-07 19:55:34 -06:00
Keyan
e9a5925c50
Merge pull request #1556 from stackernews/fix-1544
fix invoice status display
2024-11-07 19:25:06 -06:00
k00b
544a54399c fix invoice status display 2024-11-07 19:17:50 -06:00
Keyan
0891e51c9e
fix passphrase scanning (#1553) 2024-11-07 16:24:41 -06:00
Keyan
a67ef43f6e
listen for local storage wallet changes (#1552) 2024-11-07 13:15:39 -06:00
Keyan
d117549348
fix lightning module resolution (#1551)
* fix tsx usage

* Revert "Revert "Merge pull request #1521 from riccardobl/tordev""

This reverts commit 4cc3780eca1f1390bdd331f0e418b9a7efd4347e.

* lightning module resolution

* fix our one test
2024-11-07 09:03:54 -06:00
k00b
4cc3780eca Revert "Merge pull request #1521 from riccardobl/tordev"
This reverts commit 3112fc30d8b476c3682d6dc717bf0c83b50286a6, reversing
changes made to 803daed2dfe7171d317e9bf4bf02ed6a0962822c.
2024-11-05 20:08:33 -06:00
Keyan
4ad93cd4ab
Merge pull request #1546 from stackernews/fix-1543
fix vault key changes not seen by all instances of useVault
2024-11-05 19:45:16 -06:00
k00b
f0a5bc4f27 fix vault key changes not seen by all instances of useVault 2024-11-05 17:25:18 -06:00
Keyan
281757dd81
Merge pull request #1545 from riccardobl/passhide
Use -webkit-text-security to hide passphrase
2024-11-05 12:59:11 -06:00
Riccardo Balbo
49caec5b63 do not override passed style 2024-11-05 18:06:45 +01:00
Riccardo Balbo
4d8f85508a Use -webkit-text-security to hide passphrase 2024-11-05 17:16:27 +01:00
Keyan
532abcb486
Merge pull request #1540 from riccardobl/inputdefault
add explicit support for default values in input fields
2024-11-05 09:37:23 -06:00
Riccardo Balbo
cd4fcdff83 Merge remote-tracking branch 'upstream/master' into inputdefault 2024-11-05 16:35:16 +01:00
Riccardo Balbo
7eb668e0e2 explicit default values 2024-11-05 16:27:48 +01:00
Keyan
7efd0899c8
Merge pull request #1539 from stackernews/fix-1538
fix #1538
2024-11-05 07:02:38 -06:00
Keyan
3112fc30d8
Merge pull request #1521 from riccardobl/tordev
TOR patch
2024-11-04 19:41:54 -06:00
Keyan
40ff3a83f4
Merge branch 'master' into tordev 2024-11-04 19:20:29 -06:00
Keyan
803daed2df
Merge pull request #1535 from stackernews/fix-global-wallet-logs
Fix global wallet logs never fetch logs from server
2024-11-04 18:52:02 -06:00
Keyan
6feaf383b9
Merge pull request #1536 from stackernews/fix-wallet-priority-reorder
Fix wallet priority reorder inconsistency
2024-11-04 18:50:07 -06:00
k00b
db55cc7a00 fix 1538 2024-11-04 18:39:52 -06:00
ekzyis
bcaa135270 Fix wallet priority reorder 2024-11-05 00:40:06 +01:00
ekzyis
be212d1de9 Fix global wallet logs never fetch logs from server 2024-11-05 00:39:57 +01:00
Keyan
8ab44e19fd
Merge pull request #1537 from riccardobl/rqwt
requiredWithout doesn't expect an array
2024-11-04 15:43:07 -06:00
Riccardo Balbo
0416ebbdd8 requiredWithout doesn't expect an array 2024-11-04 22:18:43 +01:00
Riccardo Balbo
571a230b3c fix require is not defined in ES module scope 2024-11-04 20:09:34 +01:00
Keyan
d6caee5b42
Merge pull request #1510 from stackernews/fix-more-shown-after-delete
Fix more button shown after logs deleted
2024-11-04 07:28:58 -06:00
ekzyis
0901f15249 Fix more button shown after logs deleted by wrapping setLogs 2024-11-04 14:05:41 +01:00
ekzyis
58c37bbd63 Fix more button shown after logs deleted 2024-11-04 13:22:40 +01:00
Riccardo Balbo
048c27fa7e
Merge branch 'master' into tordev 2024-11-04 10:38:00 +01:00
Riccardo Balbo
c708c5bf6f fix lnd paths 2024-11-04 10:13:22 +01:00
Riccardo Balbo
7a8646c516 stacker_cln get_onion -> stacker_clncli get_onion 2024-11-04 10:12:52 +01:00
Keyan
d9e9a6722a
Merge pull request #1528 from riccardobl/trace
decode minified stacktrace
2024-11-03 17:28:25 -06:00
Keyan
08b160d663
Merge branch 'master' into tordev 2024-11-03 14:52:21 -06:00
Keyan
e375cc7c76
Merge pull request #1507 from stackernews/wallet-fantasy-refactor
Fantasy wallet refactor
2024-11-03 09:10:01 -06:00
k00b
a4440c991f add removeLocalWallets fixes from rblb 2024-11-03 01:15:10 -05:00
k00b
fb65ea3ace fix removing server config on unsynced client vault 2024-11-03 01:09:45 -05:00
k00b
25facad5d9 merge master 2024-11-02 19:15:44 -05:00
Riccardo Balbo
9e39e0bd01
Merge branch 'master' into tordev 2024-11-03 00:36:42 +01:00
Riccardo Balbo
1d6f1a962b
Merge branch 'master' into trace 2024-11-03 00:36:30 +01:00
Keyan
193ceefc58
Merge pull request #1527 from riccardobl/urfh
Prevent UPDATE_COMMENT from invalidating the cache of child comments (Fix #1509 )
2024-11-02 16:52:04 -05:00
Keyan
a253e42829
Merge pull request #1512 from riccardobl/clearbug
Fix some issues in clear button for input fields
2024-11-02 16:45:33 -05:00
Keyan
fee9a96186
Merge pull request #1479 from riccardobl/blinkreceiver
Blink Receive
2024-11-02 16:45:03 -05:00
Riccardo Balbo
3b523fe949
Merge branch 'master' into clearbug 2024-11-02 11:45:13 +01:00
Riccardo Balbo
6cb4870595
Merge branch 'master' into blinkreceiver 2024-11-02 11:44:54 +01:00
Riccardo Balbo
5924de6172
Merge branch 'master' into tordev 2024-11-02 11:43:59 +01:00
Riccardo Balbo
d75f9d5c74
Merge branch 'master' into urfh 2024-11-02 11:43:31 +01:00
Riccardo Balbo
2cd16a68c2
Merge branch 'master' into trace 2024-11-02 11:43:16 +01:00
Keyan
fd5f649d5c
Merge pull request #1520 from stackernews/edit-ux
Don't hide cancel if edit timer runs out
2024-11-01 20:28:43 -05:00
Keyan
11a5bb5367
Merge branch 'master' into edit-ux 2024-11-01 20:18:49 -05:00
Keyan
c68589597d
Merge pull request #1513 from stackernews/fix-hide-invoice-desc
Fix description of wrapped invoices don't respect privacy setting
2024-11-01 20:09:31 -05:00
Keyan
0039efc8c7
Merge pull request #1506 from stackernews/colored-badges
Color send & recv badges
2024-11-01 20:02:26 -05:00
Keyan
2b136715a5
Merge branch 'master' into colored-badges 2024-11-01 19:12:34 -05:00
Keyan
10008f71e2
Merge pull request #1529 from stackernews/fix-territory-revenue
fix territory revenue attribution for one founder and multiple territories
2024-11-01 13:42:06 -05:00
k00b
a73ac8896e fix territory revenue attribution for one founder and multiple terrtories 2024-11-01 13:38:04 -05:00
k00b
dccfd21be4 make sure autowithdraw members are numbers 2024-10-31 18:25:15 -05:00
k00b
14ab51a730 fixes related to p2p zaps 2024-10-31 18:12:55 -05:00
k00b
7b4a33b354 fix wallet creation without vaultEntries 2024-10-31 17:54:47 -05:00
k00b
1f71a9e187 fix logout purge of localstorage 2024-10-31 15:36:51 -05:00
k00b
b8216740d4 final touches 2024-10-31 14:06:58 -05:00
Riccardo Balbo
c3c3fe1ccb Always return original stack trace 2024-10-31 15:54:54 +01:00
Riccardo Balbo
be9b919b60 decode minified stacktrace 2024-10-31 15:43:20 +01:00
k00b
3cfbaf4638 validate generated fields 2024-10-30 22:26:45 -05:00
k00b
b1fc341017 sync/desync from localstorage on vault connect/disconnect 2024-10-30 18:37:45 -05:00
k00b
0c8180d89c fix active vault check and optional vaultEntries 2024-10-30 13:49:57 -05:00
k00b
dce5762f63 get vault working 2024-10-30 13:49:57 -05:00
k00b
eae4c2b882 cancel button spacing 2024-10-30 13:49:57 -05:00
k00b
4f7bdadd80 better wallet security banner 2024-10-30 13:49:57 -05:00
k00b
84f5db4488 update wallet readme 2024-10-30 13:49:57 -05:00
k00b
aa04adacea remove unused phoenix schema 2024-10-30 13:49:57 -05:00
k00b
e96982c353 refactor wallet validation 2024-10-30 13:49:57 -05:00
k00b
57603a936f reorder priority 2024-10-30 13:49:57 -05:00
k00b
ccdf346954 server side config saves 2024-10-30 13:49:47 -05:00
k00b
4826ae5a7b wip upsertWallet 2024-10-30 13:45:09 -05:00
k00b
2bdbb433df webln saves at least *double kazoo* 2024-10-30 13:45:09 -05:00
k00b
48640cbed6 pages load *kazoo* 2024-10-30 13:45:05 -05:00
k00b
da020cf899 complete fantasy scaffolding 2024-10-30 13:44:18 -05:00
k00b
b61c957cc7 fix missing field from merge conflict resolution 2024-10-30 13:42:55 -05:00
k00b
4e61c19bb3 fix showing autowithdraw settings on send only wallets 2024-10-30 13:42:55 -05:00
Riccardo Balbo
1e68182cda sender wallets: only test if enabled 2024-10-30 13:42:55 -05:00
Riccardo Balbo
2cfe851046 merge migrations 2024-10-30 13:42:55 -05:00
Riccardo Balbo
319db6dea6 fix schema 2024-10-30 13:42:55 -05:00
Riccardo Balbo
6a23aac6f9 Update api/resolvers/vault.js 2024-10-30 13:42:55 -05:00
Riccardo Balbo
ed66cfb3f8 remove unused fragment, fix BestWallet to return flags 2024-10-30 13:42:55 -05:00
Riccardo Balbo
fdc3df9c15 force refresh client wallets when device sync is enabled 2024-10-30 13:42:55 -05:00
Riccardo Balbo
00c047f09b do not drop config on error (might be caused by temporary connection issues) 2024-10-30 13:42:55 -05:00
Riccardo Balbo
3acad86157 improve local storage hook implementation 2024-10-30 13:42:55 -05:00
Riccardo Balbo
86994c4c46 use openConfig instead of useConfig 2024-10-30 13:42:55 -05:00
Riccardo Balbo
eeef7039b9 prevent stale me entry from causing vault configurator to delete the local vault key 2024-10-30 13:42:55 -05:00
Riccardo Balbo
4bc669c1c5 prevent double close 2024-10-30 13:42:55 -05:00
Riccardo Balbo
623b69df3a skip wallet fetch for anon users 2024-10-30 13:42:55 -05:00
Riccardo Balbo
41b86c8251 unsetLocalKey 2024-10-30 13:42:55 -05:00
Riccardo Balbo
de0eb8a52c ensure that wallets are configured to send and/or receive 2024-10-30 13:42:55 -05:00
Riccardo Balbo
0263aa8372 do not test invoice when disabling wallets 2024-10-30 13:42:50 -05:00
Riccardo Balbo
240040f2a3 ensure wallets are kept in-sync between clients 2024-10-30 13:41:41 -05:00
Riccardo Balbo
1beac3a405 ensure wallet id is in sync before saving the config 2024-10-30 13:41:41 -05:00
Riccardo Balbo
a6665bca6a fix priority sorting for send wallets,caching and sorting 2024-10-30 13:41:41 -05:00
Riccardo Balbo
bb91b629f7 make sorting optional 2024-10-30 13:41:41 -05:00
Riccardo Balbo
40f24236fd show enabled only if configured to receive or send (handle client settings wipe) 2024-10-30 13:41:41 -05:00
Riccardo Balbo
2ef7651421 optimize api calls, remove useless effects 2024-10-30 13:41:41 -05:00
Riccardo Balbo
aded5ac422 fixes 2024-10-30 13:41:41 -05:00
Riccardo Balbo
87c5634b55 add debug log 2024-10-30 13:41:41 -05:00
Riccardo Balbo
f438b278bc fix priority sorting 2024-10-30 13:41:41 -05:00
Riccardo Balbo
d30502a011 fix wallet filtering 2024-10-30 13:41:41 -05:00
Riccardo Balbo
06afe2cda2 remove debug log 2024-10-30 13:41:41 -05:00
Riccardo Balbo
4fce6fa234 Fix for enabled but not available wallets 2024-10-30 13:41:41 -05:00
Riccardo Balbo
6bd07284a5 optimize api calls 2024-10-30 13:41:41 -05:00
Riccardo Balbo
4aa9608212 fixed and add wallet migration 2024-10-30 13:41:41 -05:00
Riccardo Balbo
6f1113636f fix: await in transaction 2024-10-30 13:41:41 -05:00
Riccardo Balbo
a95e4cd6e9 collect meta from server config 2024-10-30 13:41:41 -05:00
Riccardo Balbo
4604a7bac9 use SSR constant 2024-10-30 13:41:41 -05:00
Riccardo Balbo
49cf1f2e23 fix window checks for SSR 2024-10-30 13:41:41 -05:00
Riccardo Balbo
b70dbeb6d6 user vault and server side client wallets 2024-10-30 13:41:09 -05:00
Riccardo Balbo
3d708ae7eb Prevent UPDATE_COMMENT from invalidating the cache of child comments 2024-10-30 18:42:30 +01:00
Riccardo Balbo
d9a51f9c1c
Merge branch 'master' into tordev 2024-10-29 11:21:25 +01:00
Keyan
7a942881ed
Merge pull request #1526 from stackernews/1525-fix-mathjax-unicode-typerror
Replace unicode currency symbols in inline math
2024-10-28 14:41:54 -05:00
ekzyis
b50bb2fcc1 Replace unicode currency symbols in inline math 2024-10-28 20:00:00 +01:00
Riccardo Balbo
96e1f86bca use patched authenticatedLndGrpc instead of privoxy to handle non onion grpc traffic 2024-10-26 20:06:22 +02:00
Riccardo Balbo
4fb873b105 enable tor for cln and lnbits 2024-10-26 11:36:06 +02:00
Riccardo Balbo
33c4314212 run both tor http proxy and privoxy 2024-10-26 11:35:48 +02:00
Riccardo Balbo
57042d9ed0 use cross-fetch because native fetch doesn't support agents 2024-10-26 11:34:38 +02:00
Riccardo Balbo
5466270c41 grpc_proxy should be lowercase (?) 2024-10-26 11:00:39 +02:00
Riccardo Balbo
5a09c48e04 tor for the dev environment 2024-10-26 01:53:08 +02:00
ekzyis
146f578d3d Rename onEdit to toggleEdit 2024-10-25 22:10:15 +02:00
ekzyis
739509de4f Don't hide cancel if still in edit mode 2024-10-25 22:05:29 +02:00
Keyan
18565200e6
Merge pull request #1519 from stackernews/1518-fix-invites
Fix missing fragmentName
2024-10-25 07:33:29 -05:00
ekzyis
6fa747b234 Fix missing fragmentName 2024-10-25 14:10:41 +02:00
Keyan
6c16b5eff3
Update feature_request.yml 2024-10-24 16:39:54 -05:00
Keyan
be6bcd7736
Update feature_request.yml 2024-10-24 16:39:12 -05:00
Keyan
687b9c343c
Update bug_report.yml 2024-10-24 16:38:30 -05:00
Keyan
25238f261a
Merge pull request #1514 from stackernews/gh-templates
Use issue forms
2024-10-24 16:37:24 -05:00
ekzyis
3fa4037197 More concise pull request template 2024-10-24 22:31:10 +02:00
ekzyis
9987921692 Use Github form templates for issues 2024-10-24 22:31:10 +02:00
ekzyis
383272f1c2 Fix description of wrapped invoices don't respect privacy setting 2024-10-24 21:48:44 +02:00
Riccardo Balbo
a267f3a476 use useIsClient 2024-10-24 19:58:04 +02:00
Riccardo Balbo
68dafa3684 disable clear button if field is readonly 2024-10-24 16:27:13 +02:00
Riccardo Balbo
9c6e98b47b use effect and state to disable clear button: prevent hydration errors if the field is initialized differently in the client 2024-10-24 16:27:06 +02:00
ekzyis
455f6ab665 Merge branch 'master' into blinkreceiver 2024-10-22 21:21:56 +02:00
ekzyis
ba06f3043f Color send & recv badges 2024-10-22 21:15:43 +02:00
Riccardo Balbo
00ac89287b Fix readonly (https://github.com/stackernews/stacker.news/pull/1479#pullrequestreview-2383552377) 2024-10-22 10:57:14 +02:00
Riccardo Balbo
bc4b4196f4 make sending checks less strict 2024-10-22 10:54:05 +02:00
Riccardo Balbo
751869fb63 fix text 2024-10-22 10:52:58 +02:00
Riccardo Balbo
63c2a7f270 mark fields as non editable 2024-10-22 10:51:41 +02:00
k00b
820ea90267 bump secp256k1 2024-10-21 14:52:55 -05:00
Keyan
31d3b2fd76
Update awards.csv 2024-10-21 09:26:03 -05:00
Keyan
46b2a57ece
Merge pull request #1488 from stackernews/max-base-fee
Max total fee
2024-10-20 18:44:47 -05:00
Keyan
d146e50660
Merge branch 'master' into max-base-fee 2024-10-20 18:25:16 -05:00
Keyan
fd9b087c99
Update awards.csv 2024-10-20 18:06:28 -05:00
Keyan
e307573e8b
Merge pull request #1444 from stackernews/local-dev-lnbits-recv
Map localhost:<port> to lnbits:5000 on server
2024-10-20 17:56:28 -05:00
Keyan
6049baf742
Merge branch 'master' into local-dev-lnbits-recv 2024-10-20 17:38:57 -05:00
ekzyis
88fa3bdca6 Fix autoWithdrawThreshold saved in send config 2024-10-20 15:19:53 +02:00
ekzyis
c97ce2627b Rename to autoWithdrawMaxFeeTotal 2024-10-20 15:14:31 +02:00
ekzyis
d06f89d707 Change text 2024-10-20 15:14:31 +02:00
ekzyis
596d67fc68 Add max base fee setting 2024-10-20 15:14:31 +02:00
Keyan
f5ebd573d6
Merge pull request #1499 from stackernews/fix-wallet-save
Fix wallet client validation
2024-10-19 20:22:22 -05:00
Keyan
5d5bc22e3d
Merge pull request #1502 from stackernews/fix-unarchive
Fix territory unarchive schema validation
2024-10-19 20:21:40 -05:00
ekzyis
50e153df7c Fix territory unarchive schema validation 2024-10-20 01:25:25 +02:00
ekzyis
1f9ab08228 Add comments 2024-10-19 22:36:26 +02:00
ekzyis
69c80e3d5c Fix wallet client validation 2024-10-19 22:33:37 +02:00
Keyan
7aa0d8f430
Merge pull request #1498 from stackernews/fix-1485
premature invoice deletion: fix interval in sql template
2024-10-19 09:52:16 -05:00
k00b
01580d9ee8 delete primage when invoice is deleted 2024-10-19 09:51:24 -05:00
k00b
bcd8adae45 fix interval in sql template 2024-10-18 20:20:45 -05:00
Keyan
59a24192c1
Merge pull request #1487 from stackernews/fix-missing-autowithdraw-validation
Fix missing autowithdraw settings validation
2024-10-18 13:06:57 -05:00
Riccardo Balbo
d45cf99dd4 Add autowithdrawSchemaMembers 2024-10-18 19:17:22 +02:00
ekzyis
6cc49e937b Fix missing autowithdraw settings validation 2024-10-18 18:58:59 +02:00
Riccardo Balbo
fea990390a remove strict toggle 2024-10-18 17:20:24 +02:00
Riccardo Balbo
1ef2af3f5d fix error text 2024-10-18 17:18:19 +02:00
Keyan
ad1244c260
Merge pull request #1484 from stackernews/fix-wallet-logs-pagination
Fix first page of wallet logs loaded twice
2024-10-18 09:07:54 -05:00
ekzyis
49ddace73f Fix first page of wallet logs loaded twice 2024-10-18 15:10:27 +02:00
Keyan
153f47de7d
Merge pull request #1480 from stackernews/fix-wallet-logs-infinite-loop
Fix infinite loop of loading wallet logs + center more button
2024-10-18 07:52:03 -05:00
Keyan
6b1f671329
Merge pull request #1483 from stackernews/fix-lnaddr-withdrawals
Fix lnaddr withdrawals
2024-10-18 07:50:45 -05:00
ekzyis
ab1104e115 Fix lnaddr withdrawals 2024-10-18 14:19:40 +02:00
k00b
aba212f6ec don't abort on blinded path feature bits 2024-10-17 14:23:03 -05:00
ekzyis
70858b97f7 Center more button 2024-10-17 21:22:45 +02:00
ekzyis
d91c5c90de Fix infinite loop of loading wallet logs 2024-10-17 21:18:11 +02:00
ekzyis
75051f1c56 LNbits updates for local dev
* persistent lnbits db with lnbits superuser
* map localhost:5001 -> lnbits:5000 in local dev for LNbits receives
* updated ATTACH.md
2024-10-17 20:54:07 +02:00
k00b
6aac9eeed4 update breaking change in estimateRouteFee 2024-10-17 13:25:49 -05:00
k00b
1d3ab23ec4 cached fetcher delete key 2024-10-16 14:43:11 -05:00
k00b
1645c2aabf avoid dev refresh of lnd 2024-10-16 14:43:00 -05:00
Riccardo Balbo
ce3ee703df improve validation errors 2024-10-16 19:56:27 +02:00
Riccardo Balbo
14b6d7f818 blink receive 2024-10-16 19:46:33 +02:00
Keyan
ec8e775ae6
Merge pull request #1477 from stackernews/upgrade-deps
Upgrade deps
2024-10-15 12:47:56 -05:00
k00b
031d589686 disable next telemetry 2024-10-15 12:13:40 -05:00
Keyan
7d139faca1
Merge branch 'master' into upgrade-deps 2024-10-15 11:34:55 -05:00
k00b
4b18498651 hide cowboy hat -> essentials 2024-10-14 17:52:40 -05:00
k00b
2562999e85 fix mathjax single dollar conflicts 2024-10-13 10:08:54 -05:00
k00b
fac70a0b94 npm audit 2024-10-12 18:28:16 -05:00
k00b
81897461e3 fix qrcode display after upgrade 2024-10-12 18:06:07 -05:00
k00b
6a8b823f9f upgrade qr code scanner and fix #1476 2024-10-12 18:06:07 -05:00
k00b
f9ed1ee6f5 upgrade non-(apparently)-breaking major versions 2024-10-12 18:06:07 -05:00
k00b
ff3ad7676d upgrade minor dep versions 2024-10-12 18:06:07 -05:00
k00b
9a6a167dd4 upgrade patch versions 2024-10-12 18:06:07 -05:00
k00b
021a13d21e fix anon badge 2024-10-12 18:05:45 -05:00
Keyan
245419185f
wallet streaks (#1468)
* wallet streaks backend

* notifications and badges

* reuseable streak fragment

* squash migrations

* push notifications

* update cowboy notification setting label text
2024-10-11 19:14:18 -05:00
k00b
915fc87596 tradeoff memory for throughput in prod 2024-10-10 18:11:39 -05:00
k00b
ce2d7e5791 try to fix apollo leak 2024-10-10 16:03:21 -05:00
k00b
d6d4f01b45 remove prod debug of cached fetcher 2024-10-10 10:44:11 -05:00
k00b
c634c61dd2 cached fetcher debug env var 2024-10-10 09:35:39 -05:00
ekzyis
7eaaa7ce44
Fix sub?.removeAllListeners is not a function (#1469) 2024-10-09 20:13:53 -05:00
ekzyis
41da95b125
Fix wallet double tap on mobile (#1467)
* Fix wallet double tap on mobile

* also add icon that card can be dragged

* Fix ugly drag image
2024-10-09 19:27:49 -05:00
Keyan
e48cd61721
remove unused fields from me fragment (#1466) 2024-10-09 11:56:29 -05:00
k00b
29b3f6008e reduce prod heap size 2024-10-08 22:25:08 -05:00
k00b
adcb80782b caching is hard 2024-10-08 19:26:29 -05:00
k00b
449568e3a2 don't let pending cache to build up 2024-10-08 17:58:15 -05:00
k00b
8c0cafa3ec bump nodejs version 2024-10-08 15:23:27 -05:00
k00b
67498fbc87 bump nextjs patch version 2024-10-08 15:00:37 -05:00
k00b
f8d88d18f8 give nextjs more memory in prod 2024-10-08 14:54:21 -05:00
k00b
651053fd71 remove deprecated canonizeResults 2024-10-08 14:25:23 -05:00
k00b
ebe513b5ca bump apollo versions 2024-10-08 14:22:12 -05:00
Keyan
fec7c92fd9
run noncritical side effects outside critical path of paid action (#1464)
* run noncritical side effects outside critical path of paid action

* fix item fetching of zap side effect

* fix vapid pubkey env var name in readme
2024-10-08 11:48:19 -05:00
k00b
4532e00085 Revert "Include extension in S3 key (#1426)"
This reverts commit b82641d1bdd020142e453477d21230b1a3cdcdc1.
2024-10-07 13:22:01 -05:00
Keyan
6b1f3ba8ef
Update awards.csv 2024-10-07 12:09:10 -05:00
k00b
a916533826 fix apple-mobile-web-app-capable deprecation 2024-10-07 10:52:29 -05:00
k00b
070b350211 catch when indexeddb is not available fix #1462 2024-10-07 10:51:25 -05:00
k00b
154c0e0a4a fix cache key bloat 2024-10-06 18:56:48 -05:00
k00b
177e0f6bb0 fix #1453 2024-10-05 13:57:55 -05:00
k00b
153455983e Revert "Encrypted device sync (#1373)"
This reverts commit a9a566a79f1ce59255f85127a58642beb6b70241.
2024-10-04 15:00:13 -05:00
Keyan
5543a0755a
paginating wallet logs (#1459)
* paginating wallet logs

* refine
2024-10-04 06:59:32 -05:00
k00b
00bcd8c992 fix #1451 2024-10-02 21:23:16 -05:00
Keyan
a01590e321
Update awards.csv 2024-10-02 19:53:26 -05:00
k00b
dff452f00f mathjax close #1436 2024-10-02 19:52:05 -05:00
Keyan
f4382ad73e
better boost hints (#1441)
* better boost hints

* refine
2024-10-02 19:24:01 -05:00
Keyan
5f1d3dbde4
Update awards.csv 2024-10-02 18:53:18 -05:00
k00b
65a7ef10d0 make bio work as paid action 2024-10-02 18:39:56 -05:00
k00b
5fab3abb82 fix bio quirks 2024-10-02 18:12:49 -05:00
toyota-corolla0
c400a6c1c6
feat: cache bio draft (#1455) 2024-10-02 18:06:22 -05:00
Keyan
4ce395889d
Be kind to lnd (#1448)
* cache or remove unecessary calls to lnd

* avoid redundant grpc calls in state machine

* store preimage whenever available

* enhancements post self-code review

* small refinements

* fixes

* fix lnurl-verify

* prevent wallet logger throwing on idb close

* fix promise in race while waiting for payment
2024-10-02 15:03:30 -05:00
toyota-corolla0
56809d6389
chore: update postgres docker container version (#1449)
* chore: update postgres docker container

* sndev postgres to v16

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-10-02 08:42:56 -05:00
Riccardo Balbo
a9a566a79f
Encrypted device sync (#1373)
* user vault

* code cleanup and fixes

* improve ui

* prevent name collisions between users on the same device

* some improvements

* implement storage migration

* comments and cleanup

* make connect button primary instead of warning

* move show passphrase in new line (improvement for small screen devices)

* make show passphrase field readOnly

* fixes

* fix vault key unsync

* implicit migration

* move device sync under  general tab

* fix locally disabled wallets and default wallet selection

* improve text

* remove useless SSR check

* add auth checks

* Rename variables

* Fix missing await

* Refactor local<>vault storage interface

I've changed quite some things here. Attempt of a summary:

* storageKey is now only controlled by useVaultStorageState

I've noticed that dealing with how storage keys are generated (to apply user scope) was handled in two places: the existing wallet code and in the new vault code.

This was confusing and error-prone. I've fixed that by completely relying on the new vault code to generate correct storage keys.

* refactored migration

Migration now simply encrypts any existing local wallets and sends them to the server. On success, the local unencrypted version is deleted.

The previous code seemed to unnecessarily generate new local entries prefixed by 'vault:'.

However, since we either use unencrypted local state OR use the encrypted vault on the server for the data, I didn't see any need for these.

Migration seems to work just as well as before.

* removed unnecessary state

In the <DeviceSync> component, enabled & connected were using a unnecessary combo of useState+useEffect.

They were only using variables that are always available during render so simple assignments were enough.

* other minor changes include:

  * early returns
  * remove unnecessary SSR checks in useEffect or useCallback
  * formatting, comments
  * remove unnecessary me? to expose possible bugs

* Fix missing dependency for useZap

This didn't cause any bugs because useWallet returns everything we need on first render.

This caused a bug with E2EE device sync branch though since there the wallet is loaded async.

This meant that during payment, the wallet config was undefined.

* Assume JSON during encryption and decryption

* Fix stale value from cache served on next fetches

* Add wallet.perDevice field

This adds 'perDevice' as a new wallet field to force local storage. For example, WebLN should not be synced across devices.

* Remove debug buttons

* Rename userVault -> vault

* Update console.log's

* revert some of the migration and key handling changes. restore debug buttons for testing

* Fix existing wallets not loaded

* Pass in localOnly and generate localStorageKey once

* Small refactor of migration

* Fix wallet drag and drop

* Add passphrase copy button

* Fix priorityOnly -> skipTests

* Disable autocompletion for reset confirmation prompt

* Show wrong passphrase as input error

* Move code into components/device-sync.js

* Import/export passphrase via QR code

* Fix modal back button invisible in light mode

* Fix modal closed even on connect error

* Use me-2 for cancel/close button

* Some rephrasing

* Fix wallet detach

* Remove debug buttons

* Fix QR code scan in dark mode

* Don't allow custom passphrases

* More rephrasing

* Only use schema if not enabled

* Fix typo in comment

* Replace 'generate passphrase' button with reload icon

* Add comment about IV reuse in GCM

* Use 600k iterations as recommended by OWASP

* Set extractable to false where not needed

* use-vault fallbacks to local storage only for anonymous users

* fix localStorage reset on logout

* add copy button

* move reset out of modals

* hide server side errors

* hardened passphrase storage

* do not show passphrase even if hardened storage is disabled (ie. indexeddb not supported)

* show qr code button on passphrase creation

* use toast for serverside error

* Move key (de)serialization burden to get/setLocalKey functions

* password textarea and remove qr

* don't print plaintext vault values into console

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-10-01 14:55:01 -05:00
ekzyis
5d1be66eef
Add nwc_recv container (#1442) 2024-10-01 09:14:29 -05:00
ekzyis
7d86ba3865
Fix local anon not called 'anon' (#1443) 2024-09-30 22:45:11 -05:00
k00b
861e944fe1 fix item full embed for the 3rd and final(?) time 2024-09-30 12:37:33 -05:00
k00b
865cdccb2a fix item full embed param 2024-09-30 12:24:06 -05:00
k00b
2e1a7c1035 don't allow other markup in headings 2024-09-29 11:25:01 -05:00
k00b
bbdc37ffd4 fix missing src on autolinks 2024-09-29 11:13:33 -05:00
k00b
6f198fd1ca dark mode for nostr embed 2024-09-28 20:02:33 -05:00
k00b
4f158a98a8 fix link item embed regression 2024-09-28 19:17:15 -05:00
k00b
80a7c24e54 recognize nostr entities 2024-09-28 18:56:37 -05:00
k00b
f7010ee3f7 fix #663 2024-09-28 18:20:33 -05:00
Keyan
cc4bbf99e4
fixes #1395 (#1430)
* fixes #1395

* rehype plugin for embeds

* fix lint

* replace many plugins with one rehype and improve image collage

* remove unused css

* handle more custom markdown behavior in rehype

* refactor markdown rendering more + better footnotes

* move more markdown logic to reyhpe plugin + better headers

* fix #1397

* refactor embeds out of media-or-link
2024-09-28 16:33:07 -05:00
Keyan
0f9b6f02f6
Update awards.csv 2024-09-26 18:41:24 -05:00
ekzyis
9f79d588a8
Image carousel (#1425)
* Add image carousel in fullscreen

* Flip through all images of a post

* Disable image selection in fullscreen

* Keep max-width: 100vw for images

* Fix missing dependency

* fix merge resolve bug

* better css

* refactor, keypress/swipe events, remove scoll

* changes after self-review

* give previews their own carousel

* hooks for arrow keys and swiping

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-09-26 17:37:13 -05:00
k00b
5371e1abf8 fix bounty paid action notification 2024-09-26 09:53:25 -05:00
k00b
0d4c8cdc23 no optimistic item repetition for anon 2024-09-26 09:44:14 -05:00
k00b
b3c8d32d5a remove debug log comment 2024-09-25 18:04:03 -05:00
k00b
3f49c93ecb update item repetition optimistically 2024-09-25 18:02:40 -05:00
k00b
68f7e4111b prevent shift on pending -> paid 2024-09-25 17:38:37 -05:00
Keyan
9f06fd65ee
UX latency enhancements for paid actions (#1434)
* prevent multiple retries & pulse retry button

* fix lint

* don't wait for settlement on pessimistic zaps

* optimistic act modal
2024-09-25 13:32:52 -05:00
Keyan
450c969dfc
allow edit of pending items (#1431) 2024-09-24 15:42:32 -05:00
ekzyis
1641b58e55
CSS animation for toasts and offcanvas (#1432)
* CSS animation for toasts

* Smaller toasts

* CSS animation for offcanvas

* faster animations and toast from the bottom

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-09-24 13:43:15 -05:00
k00b
d7667f7820 add boost index 2024-09-24 13:10:10 -05:00
Keyan
76218dccac
batch zap requests (#1424) 2024-09-24 09:38:48 -05:00
ekzyis
b82641d1bd
Include extension in S3 key (#1426) 2024-09-24 08:10:43 -05:00
ekzyis
18fbd17025
CTRL+U for uploads (#1423) 2024-09-23 20:08:37 -05:00
k00b
2b5a1cbfe9 more reply placeholders 2024-09-22 12:05:03 -05:00
k00b
b6dcee4f26 add booosts to newsletter script 2024-09-22 12:05:03 -05:00
ekzyis
d30dace266
Fix missing dependency for useZap (#1420)
This didn't cause any bugs because useWallet returns everything we need on first render.

This caused a bug with E2EE device sync branch though since there the wallet is loaded async.

This meant that during payment, the wallet config was undefined.
2024-09-22 11:03:38 -05:00
k00b
894d02a196 allow top sorting by boost 2024-09-21 14:58:25 -05:00
k00b
83458fdc9e add amas to newsletter script 2024-09-21 14:44:17 -05:00
k00b
101574b605 fix territory revenue notification 2024-09-20 11:07:15 -05:00
k00b
59d5fd60f2 wider top boost on rewards page 2024-09-20 10:46:07 -05:00
k00b
f90a9905ba unzapped bolt shouldn't glow 2024-09-20 10:41:46 -05:00
k00b
8447a4a8b2 boost icon refinement 2024-09-20 10:15:44 -05:00
k00b
4981d572bb show boost for when forwardee 2024-09-20 09:58:53 -05:00
ekzyis
4e7b4ee571
Fix upvote hover style not showing for first zap (#1418) 2024-09-20 09:44:15 -05:00
k00b
8c9d4aa59b fix auction based ranking 2024-09-19 17:07:36 -05:00
k00b
ad9a65ce78 fix expire boost unit 2024-09-19 16:10:04 -05:00
ekzyis
b4e143460b
Fix nwc error message (#1417)
* Fix this.error undefined on relay error

* Also use arrow function for ws.onmessage
2024-09-19 15:53:42 -05:00
k00b
731df5fc67 better err message ek suggestion 2024-09-19 15:32:18 -05:00
k00b
2f191e04f9 ek boost hint suggestions 2024-09-19 15:27:09 -05:00
k00b
09be42844e boosted items aren't freebies 2024-09-19 15:18:07 -05:00
Keyan
3310925155
increase founders fee to 70% and zap sybil fee to 30% (#1388)
* increase founders fee to 70% and zap sybil fee to 30%

* more sybil fee changes
2024-09-19 15:15:56 -05:00
k00b
020b4c5eea allow comment updates when they have boost 2024-09-19 14:06:34 -05:00
k00b
e323ed27c6 fix boost hint for comments 2024-09-19 13:42:59 -05:00
k00b
d17929f2c5 ss validate boost acts 2024-09-19 13:38:13 -05:00
Keyan
5f0494de30
rethinking boost (#1408)
* reuse boost for jobs

* wip

* allow job stopping

* restore upvote.js

* expire boost

* boost beyond edit window

* fix boost bolt styling

* rank comments with boost

* no random sort for jobs

* top boost for month at top of territory

* boost hints

* more boost help

* squash migrations

* for same boost, prioritize older

* show ad only if active

* fix itemCreate/Update boost expiration jobs

* fix fee button precedence
2024-09-19 13:13:14 -05:00
Keyan
beba2f4794
Update awards.csv 2024-09-18 15:45:52 -05:00
ekzyis
dcbe83f155
Add wss:// to user relays by default (#1412) 2024-09-18 15:42:48 -05:00
ekzyis
5088673b84
Fix margin on row overflow (#1411) 2024-09-18 11:18:08 -05:00
k00b
ae579cbec3 remove mod outlaws from hot 2024-09-16 13:30:40 -05:00
Keyan
a5af2538c6
Update awards.csv 2024-09-16 12:11:15 -05:00
k00b
d46ae03598 attempt to fix img shift 2024-09-16 12:00:15 -05:00
k00b
e63609a7c1 don't show deleted items in main sorts 2024-09-16 11:57:16 -05:00
k00b
404cb0aaa7 fixes for toplevel media embeds 2024-09-16 11:54:22 -05:00
k00b
d9213da268 fix overflowed image 2024-09-14 19:25:25 -05:00
k00b
12d2e99576 fix inline image shrinking 2024-09-14 18:22:11 -05:00
k00b
88eea9ca94 newsletter script: search for bounties on @sn account 2024-09-14 13:44:57 -05:00
k00b
df65104c60 fix text/media component styling 2024-09-13 20:40:40 -05:00
k00b
2018cc1f0b fix image height in certain contexts 2024-09-13 19:14:49 -05:00
ekzyis
7c68eafa56
Fix hat contrast in profile in dark mode (#1406) 2024-09-13 19:05:16 -05:00
ekzyis
85a8fc5dcd
Fix different slugs generated in ToC vs text (#1405) 2024-09-13 15:54:30 -05:00
ekzyis
be7ea41d03
Always set Secure for multi auth cookies in prod (#1404) 2024-09-13 13:00:16 -05:00
ekzyis
a32d1f2177
Use X-Forwarded-Proto to detect scheme (#1403) 2024-09-13 12:27:52 -05:00
ekzyis
c8975038bd
Never update author of item on edit (#1401)
* Never update author of item on edit

* Only show option to edit via hmac if anonymous

* Only send hash+hmac if anonymous
2024-09-13 11:19:54 -05:00
ekzyis
30d5eb9801
Catch s3 upload errors (#1400)
* Catch s3 upload errors

* Include file name in error message

* More renaming from image to file
2024-09-13 10:41:07 -05:00
k00b
e09e1398fd increase upload max and update error messages 2024-09-13 10:13:34 -05:00
ekzyis
8a4e67e9f0
Anon edits (#1393)
* Rename vars around edit permission

* Allow anon edits with hash+hmac

* Fix missing time zone for invoice.confirmedAt of comments

* Fix missing invoice update on item update

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-09-13 10:11:19 -05:00
ekzyis
4340a82a62
Allow video uploads (#1399)
* Allow video uploads

* fix video preview

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-09-13 09:26:08 -05:00
k00b
b3d9eb0eba fix media grid with newlines in same p tag 2024-09-12 16:44:05 -05:00
ekzyis
24aacd8839
Fix recv wallet deleted on logout (#1398)
* Fix recv wallet deleted on logout

* Fix wallet logs on server deleted on logout

* Remove unused option in deleteLogs to override wallet
2024-09-12 13:06:13 -05:00
ekzyis
a6713f9793
Account Switching (#644)
* WIP: Account switching

* Fix empty USER query

ANON_USER_ID was undefined and thus the query for @anon had no variables.

* Apply multiAuthMiddleware in /api/graphql

* Fix 'you must be logged in' query error on switch to anon

* Add smart 'switch account' button

"smart" means that it only shows if there are accounts to which one can switch

* Fix multiAuth not set in backend

* Comment fixes, minor changes

* Use fw-bold instead of 'selected'

* Close dropdown and offcanvas

Inside a dropdown, we can rely on autoClose but need to wrap the buttons with <Dropdown.Item> for that to work.

For the offcanvas, we need to pass down handleClose.

* Use button to add account

* Some pages require hard reload on account switch

* Reinit settings form on account switch

* Also don't refetch WalletHistory

* Formatting

* Use width: fit-content for standalone SignUpButton

* Remove unused className

* Use fw-bold and text-underline on selected

* Fix inconsistent padding of login buttons

* Fix duplicate redirect from /settings on anon switch

* Never throw during refetch

* Throw errors which extend GraphQLError

* Only use meAnonSats if logged out

* Use reactive variable for meAnonSats

The previous commit broke the UI update after anon zaps because we actually updated item.meSats in the cache and not item.meAnonSats.

Updating item.meAnonSats was not possible because it's a local field. For that, one needs to use reactive variables.

We do this now and thus also don't need the useEffect hack in item-info.js anymore.

* Switch to new user

* Fix missing cleanup during logout

If we logged in but never switched to any other account, the 'multi_auth.user-id' cookie was not set.

This meant that during logout, the other 'multi_auth.*' cookies were not deleted.

This broke the account switch modal.

This is fixed by setting the 'multi_auth.user-id' cookie on login.

Additionally, we now cleanup if cookie pointer OR session is set (instead of only if both are set).

* Fix comments in middleware

* Remove unnecessary effect dependencies

setState is stable and thus only noise in effect dependencies

* Show but disable unavailable auth methods

* make signup button consistent with others

* Always reload page on switch

* refine account switch styling

* logout barrier

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-09-12 13:05:11 -05:00
ekzyis
36e9f3f16f
useWallet hook cleanup (#1396)
* Assign everything to wallet object

* Add canReceive for sake of completeness
2024-09-12 11:09:44 -05:00
Keyan
fe0678b4cb
css to put adjacent images/video into vertical alignment (#1387)
* css to put adjacent images/video into vertical alignment

* fix grids chrome

* even grid gap with safari support

* fixes for video and horizontal scroll
2024-09-11 20:10:52 -05:00
ekzyis
0e0fe1af69
Fix duplicate push notification as reply and subscription (#1392) 2024-09-11 16:28:55 -05:00
ekzyis
0bf9fb0780
Fix layout shift between setting tabs (#1390) 2024-09-11 11:14:56 -05:00
k00b
da2fabc95c fix embed styling 2024-09-11 11:10:21 -05:00
k00b
855afcb1de embed spotify podcasts 2024-09-10 15:11:46 -05:00
ekzyis
821ac60de5
Throw errors which extend GraphQLError (#1386) 2024-09-10 11:35:25 -05:00
ekzyis
ec5241ad29
Enable WebLN wallet on 'webln:enabled' (#1385)
* Enable WebLN wallet on 'webln:enabled'

* Optimistically use WebLN for login with lightning

* Don't scope WebLN config to user

* Rename var to wallet
2024-09-10 11:13:39 -05:00
Keyan
f0e49c160a
automate meme monday, fact friday, what work wednesday (#1384) 2024-09-10 10:43:41 -05:00
ekzyis
8c56904094
Fix missing user invoice timeout (#1379)
* Wait max 10 seconds for user wallet to create invoice

* Add timeout in error message
2024-09-08 17:16:52 -05:00
ekzyis
2ad2cabb03
Remove overused fw-bold from infos (#1377) 2024-09-08 16:53:13 -05:00
ekzyis
1822b3fe42
Fix embeds (#1375)
* Fix wavlake embed

* Fix invalid DOM property

* Fix iframe message not received

* Fix spotify embed controller and popups

* Allow popups to escape sandbox
2024-09-08 10:32:35 -05:00
k00b
62556d2154 fix multiple nostr embed race and link clicks 2024-09-07 19:45:37 -05:00
ekzyis
597d1087f6
Fix jest module resolution (#1372) 2024-09-07 12:45:17 -05:00
Keyan
15b038cd78
refactor embeds to be reused (#1368)
* refactor embeds to be reused

* adjust the meaning of settings for embeds

* add wavlake embed (close #1359)

* add spotify embed (closes #1360)

* fix 'format' appearing in srcSet

* add nostr embed

* refine nostr embed

* Update components/media-or-link.js

Co-authored-by: ekzyis <ek@stacker.news>

* Update pages/settings/index.js

Co-authored-by: ekzyis <ek@stacker.news>

* ek suggestions

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-09-07 12:07:10 -05:00
ekzyis
f05b29717a
Fix grammar in autodelete invoices info (#1371) 2024-09-07 10:01:27 -05:00
ekzyis
54f8a61483
Random CSS fixes (#1370)
* Fix missing margin-left for invoice status in /satistics

* Fix margin-bottom not applied in invoice info

* Only apply margin-left if there is something left
2024-09-07 10:01:00 -05:00
k00b
6b27f54502 fix improxy container 2024-09-06 10:04:03 -05:00
Keyan
2f546facb2
get dimensions for video and refactor images (#1366)
* get dimensions for video and refactor images

* improve rendering performance

* more rendering perf enhancements
2024-09-06 09:34:44 -05:00
ekzyis
3f0499b96e
Fix ephemeral events missed (#1367)
* Fix ephemeral events missed

The spec mentions the following:

> for kind n such that 20000 <= n < 30000, events are ephemeral, which means they are not expected to be stored by relays.

This applies to NWC events. This means that we need to subscribe _before_ we publish the request.

See https://github.com/nostr-protocol/nips/blob/master/01.md

* Verify events before accepting them
2024-09-06 08:20:49 -05:00
k00b
fe717e0169 fix image/video clicks in notifications 2024-09-04 13:36:56 -05:00
k00b
1cc897a7a3 don't enforce min-width on videos 2024-09-04 11:00:54 -05:00
k00b
5a00f7b825 allow video in CSP 2024-09-04 09:58:05 -05:00
Keyan
07b98c3253
Optout of display of images and video (show them as links) (#1358)
* optout of display of images/video

* fix disableFreebies warning in settings

* preview trusted images

Co-authored-by: ekzyis <ek@stacker.news>

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-09-04 09:23:06 -05:00
Keyan
6f68a700ce
recognize video links (#1357) 2024-09-03 18:35:14 -05:00
Keyan
adcf048f4e
improve dupes (#1356) 2024-09-03 14:29:45 -05:00
ekzyis
d9024ff837
Reinitialize wallet form if initial values change + fix readOnly hydration error (#1354)
* Reinitialize wallet form if initial values change

This fixes that enabled is not set on first render if only recv is configured

* Remove unnecessary old usage of ClientCheckbox

This isn't needed even without enableReinitialize since for send, enabled is correctly set on first render.

It was needed in the past when we were still validating wallets before enabling them on first page load but now, we simply load the configuration from localStorage which is immediately available on the client.

* Fix readOnly hydration error

* Replace repetitive isMounted logic with useIsClient hook
2024-09-03 09:15:04 -05:00
k00b
69916117b1 refine popover close timing 2024-09-02 18:25:02 -05:00
k00b
67799a508a image loading fixes (fixes #1345) 2024-09-02 18:15:21 -05:00
ekzyis
7428738b23
Update wallets/README.md (#1353)
* Remove warning about send+recv not tested

* Add file comment

* Fix createInvoice description
2024-09-02 17:15:46 -05:00
ekzyis
a7066a34cd
Use default-src 'self' a.stacker.news (#1349)
This should fix CSP errors in Firefox because scripts fetched via <link rel="prefetch"> don't use script-src.
2024-09-02 12:58:14 -05:00
ekzyis
07ebc60bc3
Use undefined instead of empty function for onHide (#1348) 2024-09-02 12:57:16 -05:00
ekzyis
5e77106297
Undelete bio items (#1346) 2024-09-02 12:02:47 -05:00
ekzyis
c43a171794
Fix onHide is not a function (#1347) 2024-09-02 09:33:17 -05:00
k00b
f42344497e update newsletter script 2024-08-31 14:04:49 -05:00
k00b
6dedda577b only queue autowithdraw if a wallet is enabled 2024-08-29 16:13:16 -05:00
k00b
b6e4f97668 fix env loading in worker 2024-08-28 09:38:41 -05:00
ekzyis
17da24ce24
Add a.stacker.news to script-src (#1339) 2024-08-28 09:33:26 -05:00
ekzyis
ae8cadd4be
Switch NWC from Damus to Primal relay (#1340) 2024-08-28 09:32:29 -05:00
ekzyis
2503a3cb6a
Update wallet security banner (#1338)
* fix condition
* update text
2024-08-27 17:16:41 -05:00
Keyan
3af43d74d3
Update awards.csv 2024-08-27 11:22:34 -05:00
ekzyis
4cec369005
Support Tor for LNbits recv (#1336)
* Add tor support to LNbits recv

* Only return agent
2024-08-27 11:16:02 -05:00
ekzyis
d09f7c5427
Fix websocket leaks (#1334) 2024-08-27 11:15:00 -05:00
ekzyis
ec6124ca62
NWC hardcoded keys (#1335)
* Include keys.json in NWC container image

* Update NWC ATTACH.md
2024-08-27 11:14:35 -05:00
ekzyis
9f194c5d8e
Fix preimage undefined in wallet logs (#1337)
* Fix preimage undefined in NWC wallet logs

* Return preimage as string
2024-08-27 11:13:52 -05:00
Keyan
266e9a892d
Improve freebies (#1333)
* remove free posts

* deleted and freebie comments are always last
2024-08-26 19:23:07 -05:00
ekzyis
cc003a9a3e
Phoenixd send+recv (#1322)
* Add genwallet script

* Add phoenixd as send+recv wallet

* phoenixd passwords are 64 hex chars
2024-08-26 18:20:45 -05:00
k00b
5cfefc1ca8 cancelled failed payment if not showing qr 2024-08-26 13:58:37 -05:00
k00b
5ae3084e53 remove defunct chats from footer 2024-08-26 12:59:39 -05:00
ekzyis
48d0cd1086
Fix full config saved on client on priority change (#1329)
* Fix full config saved on client on priority change

* Fix WebLN disabled on priority change

* Always merge configs
2024-08-25 18:40:55 -05:00
ekzyis
203db13553
Fix cost not shown in comment details (#1330) 2024-08-25 18:40:02 -05:00
k00b
467a9d6a76 fix lnc by always reusing the same lnc object 2024-08-24 18:56:15 -05:00
ekzyis
c0de29cb82
Rename NWC primary key column from 'int' to 'id' (#1328) 2024-08-23 10:11:13 -05:00
ekzyis
66cf97e832
Skip wallet tests on priority update (#1327)
* Skip wallet connection tests if only priority is changed

* Fix server priority overrides client priority

* Also add priorityOnly as last argument in generateMutation
2024-08-22 20:08:02 -05:00
ekzyis
382f16643d
Ignore no rows found in disableFreebies (#1326) 2024-08-22 18:09:22 -05:00
ekzyis
82788b35bf
Show item details via context menu (#1325)
* Show item details via context

* Use zappers instead of upvotes

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-08-21 20:35:13 -05:00
ekzyis
896265a4a8
Update QA question in PR template (#1324) 2024-08-21 19:30:06 -05:00
k00b
c4a96af5d3 fix crossposting toast 2024-08-21 14:59:28 -05:00
Keyan
df62cfb28c
paid action limits (#1323) 2024-08-21 14:45:51 -05:00
ekzyis
67d71ef0c8
Rename LNbits primary key column from 'int' to 'id' (#1321)
* Rename LNbits primary key column from 'int' to 'id'

* fix migration

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-08-21 10:49:48 -05:00
ekzyis
789d7626f7
Support receiving with NWC (#1310)
* Add NWC receives

* Refactor sendPayment+createInvoice with nwcCall function

* Update badge

* Add method support checks

* Add timeout to NWC test invoice

* Fix NWC isConfigured state

All NWC fields are marked as optional but NWC should only be considered configured if one of them is set.

* Fix relay.fetch() throws 'crypto is not defined' in node

nip04.encrypt() was failing in worker because 'crypto is not defined'. Updating to nostr-tools v2.7.2 fixed that.

However, now crypto.randomUUID() in relay.fetch() was throwing 'crypto is not defined'. Importing crypto from 'crypto' fixed that.

However, with the import, randomUUID() does not work so I switched to randomBytes().

Running relay.fetch() now works in browser and node.

* recv must not support pay_invoice

* Fix Relay connection check

* this.url was undefined
* error was an object

* Fix additional isConfigured check runs always

It was meant to only catch false positives, not turn negatives into false positives.

* Rename testConnectServer to testCreateInvoice

* Rename testConnectClient to testSendPayment

* Only run testSendPayment if send is configured

The return value of testSendPayment was used before but it only returned something for LNC.

And for LNC, we only wanted to save the transformation during validation, so it was not needed.

* Always use withTimeout in NWC test functions

* Fix fragment name

* Use get_info command exclusively

* Check permissions more efficiently

* Log NWC request-response flow

* Fix variable name

* Call ws.send after listener is added

* Fix websocket not closed after timeout

* Also check that pay_keysend etc. are not supported

* fix lnc session key save

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-08-21 10:13:27 -05:00
Keyan
bc94ec7d28
disable freebies setting (#1320)
* disable freebies setting

* await in disableFreebie resolver + better info modal
2024-08-21 09:37:25 -05:00
k00b
555601c7de no bios in random 2024-08-20 21:12:39 -05:00
k00b
85e91ede47 show only active items with comments on random 2024-08-20 21:09:52 -05:00
k00b
accf5212b2 fix ~jobs 2024-08-20 20:50:04 -05:00
Keyan
eca7e8df0d
Update awards.csv 2024-08-20 17:06:45 -05:00
Tom
4fd23a3694
Add Random link and basic query (#1306)
* Add Random link and basic query

* Use random

* refine random sort query

* make vote threshold higher

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-08-20 16:50:55 -05:00
ekzyis
17199e8f91
Fix missing fragment name update (#1319) 2024-08-19 17:41:52 -05:00
ekzyis
6801b775c1
Fix inconsistency between walletTypeToResolveType and generateTypeDefName (#1318) 2024-08-19 17:36:07 -05:00
k00b
a495c421ce select first status=null on withdrawal check 2024-08-19 10:10:34 -05:00
k00b
2ff839f3a5 check invoice after transitioning to cancel 2024-08-18 18:03:01 -05:00
Keyan
3264601dc6
add priority:low to readme 2024-08-18 17:30:25 -05:00
ekzyis
06b661625c
Use custom relay API (#1302)
* Use custom relay API

Relay from nostr-tools was cumbersome to use. This custom abstraction over window.WebSocket makes interacting with nostr relays easier.

* Use variables for nostr message parts

* Fix NWC save

* Use try/finally

* Refactor crossposting code

* use custom replay API
* simplify callWithTimeout

* Use isomorphic-ws for nip57 zap receipts

* Use async map

* Reject with timeout error

* Move time functions into lib/time

* Remove outdated comment regarding relay.close()
2024-08-18 17:28:39 -05:00
ekzyis
ccbc28322e
Add wasm-unsafe-eval to CSP for LNC (#1313) 2024-08-18 17:20:46 -05:00
k00b
df7baf4d7c don't filter freebies/outlaws on profiles + fix ~sub/recent/all 2024-08-18 16:43:19 -05:00
k00b
3608d133d7 remove redundant info help text from wallets 2024-08-18 15:20:31 -05:00
k00b
506bb364d1 fix walletTypeToResolveType 2024-08-18 14:57:50 -05:00
k00b
9932a782b2 improve autowithdraw recent failure check 2024-08-18 14:06:17 -05:00
Keyan
11ddbe3983
Update awards.csv 2024-08-18 11:45:39 -05:00
Riccardo Balbo
2d139bed85
Blink wallet sending attachment (#1293)
* blink attachment

* support staging

* add staging dashboard link

* Revert "add staging dashboard link"

This reverts commit a43fa2204f03d74e733063aedd6862c6d71e4a46.

* Revert "support staging"

This reverts commit 93c15aa5083e60b1dafc77c30e999fb90fef8589.

* handle pending payments, code cleanup and comments

* stable sats -> stablesats

* catch HTTP errors

* print wallet currency in debug

* disable autocomplete

* schema without test()

* Fix save since default is not applied for empty strings

Formik validation must see 'currency' as undefined and apply the default but the validation before save sees an empty string.

* Save transformed config

* Remove unnecessary defaults

* Prefix HTTP error with text

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-08-18 11:36:55 -05:00
ekzyis
3d8ae4a7a3
Generate more server wallet code (#1309)
* Generate more code from wallet defs

* generate "type WalletLND { ... }"
* generate "union WalletDetails = WalletLND | ..."
* hardcode function for __resolveType
* add comments where updates are needed if another server wallet is added

* Fix type for LN addresses

* Generate __resolveType from wallet.type column
2024-08-18 11:32:25 -05:00
k00b
d2d04ce141 lnbits doesn't support msats either 2024-08-17 18:43:39 -05:00
Keyan
ffc156df2b
Msats to sats floor (#1307)
* make wallet invoice creation tests make full sat invoice

* handle rounded/floored msats for receiving wallets

* msats flooring to sats function
2024-08-16 19:33:17 -05:00
ekzyis
ab80873a57
Fix all wallets deleted on logout (#1308)
Every wallet returned by the useWallet hook has sendPayment set even if it doesn't support payments since wallet.sendPayment is wrapped with a useCallback hook.
2024-08-16 15:50:11 -05:00
ekzyis
3cd9991aa9
Hide wallet name in logs for only a single wallet (#1305) 2024-08-16 09:02:09 -05:00
k00b
ec2383b998 bug is only in wildwestmode 2024-08-14 15:43:07 -05:00
k00b
5a65eaae18 try to fix satsFilter clause 2024-08-14 15:36:21 -05:00
ekzyis
8e18fa0760
Remove TODO about errors in createInvoice (#1300) 2024-08-14 14:56:02 -05:00
ekzyis
4000522773
Fix WebLN checkbox unclickable (#1299)
Checkbox was unclickable because wallet.isConfigured is false if wallet.config was null.
2024-08-14 14:29:24 -05:00
k00b
ddb32e0bb7 fix null initial values for wallet forms 2024-08-14 13:22:17 -05:00
k00b
11ac605cd9 fix clicking on p2p in satistics 2024-08-14 12:59:27 -05:00
k00b
71b639be5d reduce p2p max 2024-08-14 12:11:08 -05:00
ekzyis
56d55027ab
Fix freebie comment edits like a post (#1298) 2024-08-14 09:31:52 -05:00
k00b
cb2efb0a7f fix sats filter for notifications 2024-08-13 16:27:22 -05:00
k00b
4d7e0a8296 fix settings mutation after greeterMode removal 2024-08-13 16:11:54 -05:00
k00b
cfd63f4efb fix relative path env in sndev 2024-08-13 15:48:22 -05:00
k00b
ac87322ac8 fix user/item popover 2024-08-13 15:30:43 -05:00
ekzyis
53465e3f46
Remove unnecessary me from addWalletLog params (#1296) 2024-08-13 09:53:44 -05:00
Keyan
cc289089cf
not-custodial zap beta (#1178)
* not-custodial zap scaffolding

* invoice forward state machine

* small refinements to state machine

* make wrap invoice work

* get state machine working end to end

* untested logic layout for paidAction invoice wraps

* perform pessimisitic actions before outgoing payment

* working end to end

* remove unneeded params from wallets/server/createInvoice

* fix cltv relative/absolute confusion + cancelling forwards

* small refinements

* add p2p wrap info to paidAction docs

* fallback to SN invoice when wrap fails

* fix paidAction retry description

* consistent naming scheme for state machine

* refinements

* have sn pay bounded outbound fee

* remove debug logging

* reenable lnc permissions checks

* don't p2p zap on item forward splits

* make createInvoice params json encodeable

* direct -> p2p badge on notifications

* allow no tls in dev for core lightning

* fix autowithdraw to create invoice with msats

* fix autowithdraw msats/sats inconsitency

* label p2p zaps properly in satistics

* add fees to autowithdrawal notifications

* add RETRYING as terminal paid action state

* Update api/paidAction/README.md

Co-authored-by: ekzyis <ek@stacker.news>

* Update api/paidAction/README.md

Co-authored-by: ekzyis <ek@stacker.news>

* Update api/lnd/index.js

Co-authored-by: ekzyis <ek@stacker.news>

* ek suggestions

* add bugetable to nwc card

* get paranoid with numbers

* better finalize retries and better max timeout height

* refine forward failure transitions

* more accurate satistics p2p status

* make sure paidaction cancel in state machine only

* dont drop bolt11s unless status is not null

* only allow PENDING_HELD to transition to FORWARDING

* add mermaid state machine diagrams to paid action doc

* fix cancel transition name

* cleanup readme

* move forwarding outside of transition

* refine testServerConnect and make sure ensureB64 transforms

* remove unused params from testServerConnect

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: k00b <k00b@stacker.news>
2024-08-13 09:48:30 -05:00
ekzyis
454ad26bd7
Add migration to fix missing item bio marker (#1295) 2024-08-12 19:19:18 -05:00
k00b
ce7d2b888d add back greeterMode for backwards compat 2024-08-12 17:49:01 -05:00
ekzyis
ae73b0c19f
Support receiving via LNbits (#1278)
* Support receiving with LNbits

* Remove hardcoded LNbits url on server

* Fix saveConfig ignoring save errors

* saveConfig was meant to only ignore validation errors, not save errors
* on server save errors, we redirected as if save was successful
* this is now fixed with a promise chain
* logging payments vs receivals was also moved to correct place

* Fix enabled falsely disabled on SSR

If a wallet was configured for payments but not for receivals and you refreshed the configuration form, enabled was disabled even though payments were enabled.

This was the case since we don't know during SSR if it's enabled since this information is stored on the client.

* Fix missing 'receivals disabled' log message

* Move 'wallet detached for payments' log message

* Fix stale walletId during detach

If page was reloaded, walletId in clearConfig was stale since callback dependency was missing.

* Add missing callback dependencies for saveConfig

* Verify that invoiceKey != adminKey

* Verify LNbits keys are hex-encoded

* Fix local config polluted with server data

* Fix creation of duplicate wallets

* Remove unused dependency

* Fix missing error message in logs

* Fix setPriority

* Rename: localConfig -> clientConfig

* Add description to LNbits autowithdrawals

* Rename: receivals -> receives

* Use try/catch instead of promise chain in saveConfig

* add connect label to lnbits for no url found for lnbits

* Fix adminKey not saved

* Remove hardcoded LNbits url on server again

* Add LNbits ATTACH.md

* Delete old docs to attach LNbits with polar

* Add missing callback dependencies

* Set editable: false

* Only set readOnly if field is configured

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-08-12 17:23:39 -05:00
Keyan
68758b3443
Update awards.csv 2024-08-12 16:58:49 -05:00
Keyan
c5f043c625
replace greeter mode with investment filter (#1291)
* replace greeter mode with investment filter

* change name to satsFilter

* drop freebie column

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-08-11 18:47:03 -05:00
Keyan
e897a2d1dc
Update awards.csv 2024-08-11 16:31:44 -05:00
Anis Khalfallah
ed6ef2f82f
fix: constrain less important services in docker compose (#1289)
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-08-11 16:29:46 -05:00
ekzyis
bcae5e6d2e
Fix callback set to NextJS data URL (#1292) 2024-08-10 14:38:35 -05:00
Keyan
ef229b378e
Update awards.csv 2024-08-10 11:17:22 -05:00
ekzyis
3863edb871
Remove commented out webpack.IgnorePlugin code (#1290) 2024-08-08 16:16:11 -05:00
ekzyis
f7a170fff0
Crosspost errors are warnings now (#1285) 2024-08-05 18:05:09 -05:00
ekzyis
288fa37197
Only validate tipRandom if enabled (#1284)
* Only validate tipRandom if enabled

* Use consistent naming scheme for zap settings

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-08-03 19:51:15 -05:00
keyan
5d0e071939 better random zaps 2024-08-03 19:37:43 -05:00
ekzyis
0c3337fb97
More zap undo fixes IV (#1282)
* Add margin-top to zap undo input

* Update zap undo hint

* Fix zap undo pulse not reflecting zap amount
2024-08-02 17:41:47 -05:00
ekzyis
9c66a5aafc
Fix duplicate 'Wallet' in resolver name (#1281) 2024-08-01 16:06:58 -05:00
Keyan
8000886e72
dont expect unrun services in dev (#1279) 2024-07-31 19:44:08 -05:00
keyan
4b391dd6ee add separators to all unabbreviated numbers 2024-07-31 19:02:33 -05:00
Keyan
e6f6895ce0
Update awards.csv 2024-07-31 12:22:30 -05:00
Keyan
837767a323
Update awards.csv 2024-07-31 12:18:26 -05:00
keyan
7901f2fe61 fix anon zaps (no tip settings) 2024-07-30 18:09:46 -05:00
SatsAllDay
dc0370ba17
random zap amounts (#1263)
* add random zapping support

adds an option to enable random zap amounts per stacker

configurable in settings, you can enable this feature and provide
an upper and lower range of your random zap amount

* rename github eslint check to lint

this has been bothering me since we aren't using eslint for linting

* fixup! add random zapping support

* fixup! rename github eslint check to lint

* fixup! fixup! add random zapping support

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-07-26 22:37:03 -05:00
keyan
4964e2c7d1 order all zap recipients 2024-07-26 12:25:48 -05:00
keyan
3d1f7834ca fix potential zap deadlock 2024-07-25 20:46:17 -05:00
ekzyis
628a0466fd
Use validation for WebLN wallet (#1277)
* remove available prop
* 'enabled' checkbox is now always enabled but uses validation
* CheckboxGroup was missing to show error message
2024-07-24 10:08:09 -05:00
ekzyis
d3ca87a78b
Add WebLN for sending payments (#1274)
* Add WebLN for sending payments

* attach docs for alby

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-07-23 13:23:48 -05:00
Keyan
c20a954cfc
This day on ... automated post (#1273)
* add this day posting job

* put in proper timezone

* make sure we're in central timezone

* schedule thisDay job
2024-07-23 10:35:15 -05:00
ekzyis
7fd4f58e81
Fix subscription ignores OP replies (#1272) 2024-07-22 16:14:48 -05:00
ekzyis
ea8ad5c4c9
Fix notification indicator shown for own replies (#1271) 2024-07-22 16:13:07 -05:00
keyan
de38a683d5 remove saloon from top posts 2024-07-21 13:57:18 -05:00
ekzyis
e517abb061
Fix+improve wallet README (#1269) 2024-07-21 13:26:33 -05:00
h0dlr
1ef88664d4
Update awards.csv (#1267) 2024-07-21 11:47:34 -05:00
Anis Khalfallah
887354820b
fix: modified docker link, added free disk space requirement (#1265) 2024-07-21 11:47:12 -05:00
Anis Khalfallah
f051afff35
feat: added NGN currency (#1264) 2024-07-21 11:46:52 -05:00
ekzyis
371e7417ce
Wallet definitions with uniform interface (#1243)
* wip: Use uniform interface for wallets

* Fix import error

* Update wallet logging + other stuff

* add canPay and canSend to wallet definition
* rename 'default payment method' to 'enabled' and add enable + disable method

* Set canPay, canReceive in useWallet

* Enable wallet if just configured

* Don't pass logger to sendPayment

* Add logging to attach & detach

* Add schema to wallet def

* Add NWC wallet

* Fix unused isDefault saved in config

* Fix enableWallet

* wrong storage key was used
* broke if wallets with no configs existed

* Run validation during save

* Use INFO level for 'wallet disabled' message

* Pass config with spread operator

* Support help, optional, hint in wallet fields

* wip: Add LNC

* Fix 20s page load for /settings/wallets.json?nodata=true

For some reason, if nodata is passed (which is the case if going back), the page takes 20s to load.

* Fix extremely slow page load for LNC import

I noticed that the combination of

```
import { Form, PasswordInput, SubmitButton } from '@/components/form'
```

in components/wallet/lnc.js and the dynamic import via `await import` in components/wallet/index.js caused extremely slow page loads.

* Use normal imports

* Revert "Fix 20s page load for /settings/wallets.json?nodata=true"

This reverts commit deb476b3a966569fefcfdf4082d6b64f90fbd0a2.

Not using the dynamic import for LNC fixed the slow page load with ?nodata=true.

* Remove follow and show recent logs first

* Fix position of log start marker

* Add FIXMEs for LNC

I can't get LNC to connect. It just hangs forever on lnc.connect(). See FIXMEs.

* Remove logger.error since already handled in useWallet

* Don't require destructuring to pass props to input

* wip: Add LND autowithdrawals

* receiving wallets need to export 'server' object field
* don't print macaroon error stack
* fix missing wallet logs order update
* mark autowithdrawl settings as required
* fix server wallet logs deletion
* remove canPay and canReceive since it was confusing where it is available

TODO

* also use numeric priority for sending wallets to be consistent with how status for receiving wallets is determined
* define createInvoice function in wallet definition
* consistent wallet logs: sending wallets use 'wallet attached'+'wallet enabled/disabled' whereas receiving wallets use 'wallet created/updated'
* see FIXMEs

* Fix TypeError

* Fix sendPayment called with empty config

* removed useEffect such that config is available on first render
* fix hydration error using dynamic import without SSR

* Fix confusing UX around enabled

* Remove FIXMEs

Rebase on master seemed to have fixed these, weird

* Use same error format in toast and wallet log

* Fix usage of conditional hooks in useConfig

* Fix isConfigured

* Fix delete wallet logs on server

* Fix wallet logs refetch

onError does not exist on client.mutate

* Fix TypeError in isConfigured if no enabled wallet found

* Only include local/server config if required

* Fix another hydration error

* Fix server config not updated after save or detach

* Also use 'enabled' for server wallets

* Fix wallet logs not updated after server delete

* Consistent logs between local and server wallets

* 'wallet attached' on create
* 'wallet updated' on config updates
* 'wallet enabled' and 'wallet disabled' if checkbox changed
* 'wallet detached' on delete

* Also enable server wallets on create

* Disable checkbox if not configured yet

* Move all validation schema into lib/validate

* Implement drag & drop w/o persistence

* Use dynamic import for WalletCard

This fixes a lot of issues with hydration

* Save order as priority

* Fix autowithdrawSettings not applied

Form requires config in flat format but mutation requires autowithdraw settings in a separate 'settings' field.

I have decided that config will be in flat form format. It will be transformed into mutation format during save.

* Save dedicated enabled flag for server wallets

* wallet table now contains boolean column 'enabled'
* 'priority' is now a number everywhere
* use consistent order between how autowithdrawals are attempted and server wallets cards

* Fix onCanceled missing

* Fix typo

* Fix noisy changes in lib/validate

I moved the schema for lnbits, nwc and lnc out of lib/validate only to put them back in there later.

This commit should make the changeset cleaner by removing noise.

* Split arguments into [value,] config, context

* Run lnbits url.replace in validate and sendPayment

* Remove unnecessary WALLETS_QUERY

* Generate wallet mutation from fields

* Generate wallet resolver from fields

* Fix import inconsistency between app and worker

* Use wallet.createInvoice for autowithdrawals

* Fix success autowithdrawal log

* Fix wallet security banner shown for server wallets

* Add autowithdrawal to lightning address

* Add optional wallet short name for logging

* Fix draggable

* Fix autowithdraw loop

* Add missing hints

* Add CLN autowithdrawal

* Detach wallets and delete logs on logout

* Remove Wallet in lib/constants

* Use inject function for resolvers and typeDefs

* Fix priority ignored when fetching enabled wallet

* Fix draggable false on first page load due to SSR

* Use touches instead of dnd on mobile

Browsers don't support drag events for touch devices.

To have a consistent implementation for desktop and mobile, we would need to use mousedown/touchstart, mouseup/touchend and mousemove/touchmove.

For now, this commit makes changing the order possible on touch devices with simple touches.

* Fix duplicate CLN error

* Fix autowithdraw priority order

* Fix error per invalid bip39 word

* Update LNC code

* remove LNC FIXMEs

Mhh, I guess the TURN server was down or something? It now magically works. Or maybe it only works once per mnemonic?

* also removed the lnc.lnd.lightning.getInfo() call since we don't ask and need permission for this RPC for payments.

* setting a password does not work though. It fails with 'The password provided is not valid' which is triggered at https://github.com/lightninglabs/lnc-web/blob/main/lib/util/credentialStore.ts#L81.

* Fix order if wallet with no priority exists

* Use common sort

* Add link to lnbits.com

* Add example wallet def

* Remove TODOs

TODO in components/wallet-logger.js was handled.
I don't see a need for the TODO in lib/wallet.js anymore. This function will only be called with the wallet of type LIGHTNING_ADDRESS anyway.

* Remove console.log

* Toast priority save errors

* Fix leaking relay connections

* Remove 'tor or clearnet' hint for LN addresses

* Remove React dependency from wallet definitions

* Generate resolver name from walletField

* Move wallets into top level directory wallet/

* Put wallets into own folder

* Fix generateMutation

* remove resolverName property from wallet defs
* move function into lib/wallet
* use function in generateMutation on client to fix wrongly generated mutation

* Separate client and server imports by files

* wallets now consist of an index.js, a client.js and a server.js file
* client.js is imported on the client and contains the client portion
* server.js is imported on the server and contains the server porition
* both reexport index.js so everything in index.js can be shared by client and server

* every wallet contains a client.js file since they are all imported on the client to show the cards

* client.js of every wallet is reexported as an array in wallets/client.js
* server.js of every wallet is reexported as an array in wallets/server.js

FIXME: for some reason, worker does not properly import the default export of wallets/server.js

* Fix worker import of wallets/server

* Fix wallet.server usage

* I removed wallet.server in a previous commit
* the client couldn't determine which wallet was stored on the server since all server specific fields were set in server.js
* walletType and walletField are now set in index.js
* walletType is now used to determine if a wallet is stored on the server

* also included some formatting changes

* Fix w.default usage

Since package.json with { "type": "module" } was added, this is no longer needed.

* Fix id access in walletPrioritySort

* Fix autowithdrawal error log

* Generate validation schema for LNbits

* Generate validation schema for NWC

* Rename to torAllowed

* Generate validation schema for LNC

* Generate validation schema for LND

* Generate validation schema for LnAddr

* Remove stringTypes

* Generate validation schema for CLN

* Make clear that message belongs to test

* validate.message was used in tandem with validate.test
* it might be confused as the message if the validation for validate.type failed
* now validate.test can be a function or an object of { test, message } shape which matches Yup.test

* Remove validate.schema as a trap door

* make lnc work

* Return null if no wallet was found

* Revert code around schema generation

* Transform autowithdrawSchemaMembers into an object

* Rename schema to yupSchema

* Fix missing required for LNbits adminKey

* Support formik form-level validation

* Fix missing addWalletLog import

* Fix missing space after =

* fix merge conflict resolution mistake

* remove non-custodial* badges

* create guides for attaching wallets in sndev

* Use built-in formik validation or Yup schema but not both

* Rename: validate -> testConnectClient, testConnect -> testConnectServer

* make lnaddr autowithdraw work in dev

* move ATTACH docs to ./wallets and add lnaddr doc

* Fix missing rename: yupSchema -> fieldValidation

* Remove unused context

* Add documentation how to add wallets

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-07-20 17:51:46 -05:00
keyan
cadfc47eb5 make sure all notifications have the same padding 2024-07-19 14:47:12 -05:00
keyan
c025c85855 fix referrals showing in meRewards 2024-07-19 12:38:49 -05:00
keyan
573d4d8452 fix #1266 2024-07-19 11:50:21 -05:00
keyan
3a748b8d38 exclude saloon from rewards 2024-07-16 13:54:21 -05:00
keyan
98efe763a0 remove done referral todos 2024-07-16 13:44:58 -05:00
keyan
c6581b2cb1 allow db connection limit and timeout configuration 2024-07-16 13:42:09 -05:00
keyan
dd4806c1a3 avoid float in prisma template param 2024-07-14 16:53:06 -05:00
keyan
e045c46811 fix Item.root resolver for anon 2024-07-14 15:53:40 -05:00
keyan
e1d6632445 fix action tooltip container 2024-07-12 17:34:46 -05:00
keyan
da65191cd8 refine comment padding 2024-07-12 15:18:13 -05:00
keyan
3f9d509a52 more notification refinement 2024-07-12 10:55:24 -05:00
keyan
bc2cb29c41 improve notification header styling 2024-07-12 10:38:47 -05:00
keyan
cb6b85345c attempt fix for popovers 2024-07-12 10:15:57 -05:00
keyan
35cf792ff8 refine notifications some 2024-07-12 09:51:03 -05:00
Keyan
b31a8dbf2c
Update awards.csv 2024-07-11 18:23:33 -05:00
keyan
2e90f02997 more popover leave events 2024-07-11 17:48:57 -05:00
keyan
0aea695d8a higher contrast outline for new notifications 2024-07-11 17:32:28 -05:00
keyan
729aab12eb better component reuse in notifications 2024-07-11 17:29:05 -05:00
keyan
9ac31095c8 docker-compose isn't a thing anymore 2024-07-11 17:28:13 -05:00
keyan
07042c57ca improve UX of notifications 2024-07-11 16:59:07 -05:00
keyan
28c4fa160c fix item spacing 2024-07-11 16:58:55 -05:00
keyan
501ac9f220 add action state indices 2024-07-11 12:49:23 -05:00
keyan
6c6d2dab18 fix typo 2024-07-11 11:55:19 -05:00
keyan
64eb22cc5e new rewards banner 2024-07-10 19:59:05 -05:00
keyan
ce45574bce fix #1261 2024-07-10 19:39:53 -05:00
Keyan
c6554d3ca7
Referral Rewards (#1262)
* referral rewards

* make referral notifications consistent

* remove plpgsql from earn job

* remove dead code

* remove debug logging
2024-07-10 19:23:05 -05:00
keyan
6cf16d3da7 don't toast on invoice cancellation 2024-07-09 13:10:41 -05:00
keyan
94d9d9513c hide overflow of toasts 2024-07-09 11:46:38 -05:00
keyan
f05b6fab84 add wallets profile to allow exclusion on attached wallet containers 2024-07-09 11:37:55 -05:00
keyan
e0f91ace41 prevent lnc-web's wasm loading side effects from breaking everything 2024-07-08 16:10:19 -05:00
keyan
0312012089 make sure stackedMsats is updated for forwardees 2024-07-08 09:35:29 -05:00
ekzyis
02472bb81f
Fix missing stackedMsats update + wrong sybil fee (#1256)
* Fix 'stackedMsats' not updated

* Fix 1% fee instead of 10%
2024-07-08 09:07:35 -05:00
keyan
3710840167 upgrade prisma for https://github.com/prisma/prisma/issues/16611 fix 2024-07-07 11:14:12 -05:00
Keyan
3bada4b5da
new referral scheme (#1255)
* capture/store data for new referral scheme

* simplify signup/forever referral rules

* no self-referrals and other fixes

* better post/comment distinction and support /items/1/related
2024-07-07 11:12:02 -05:00
keyan
fc781047d5 fix autowithdraw flag for lightning address 2024-07-06 12:56:20 -05:00
keyan
9213e3ad1a fix settleHodlInvoice timing out paid action transition 2024-07-06 11:37:32 -05:00
keyan
bcdbf9cede print imgproxy dimensions error 2024-07-04 17:54:54 -05:00
Keyan
79f0df17b2
improve pessimistic paid actions by letting the server perform actions and settle invoice on HELD (#1253)
* get rid of hash and hmac based pessimism

* fix readme
2024-07-04 12:30:42 -05:00
ekzyis
7c294478fb
Nostr crossposting backlink + content fix (#1251)
* Add backlink in nostr events

* Remove unnecessary async

* Use itemToContent function

* Fix duplicate title in discussion event
2024-07-03 10:11:24 -05:00
keyan
1a3785a865 only assume insufficient funds in paid action if the error says so 2024-07-03 09:10:04 -05:00
ekzyis
2dd96f4b83
Fix item mention of own items (#1250) 2024-07-02 14:22:58 -05:00
keyan
9145f290dc fix lastCommentAt denorm 2024-07-02 09:26:40 -05:00
keyan
63e60fe2bc subtle change to usePaidMutation callback order 2024-07-01 17:04:10 -05:00
keyan
0aa5ba4955 move invoice creation outside of interactive tx 2024-07-01 17:03:25 -05:00
keyan
6e8d7ef1b8 allow slog logs to be disabled/configured 2024-07-01 16:48:54 -05:00
ekzyis
e57c930f0c
Fix push notifications (#1249)
* Fix wrong author in reply push notification

* Fix duplicate mentions push notifications on save

* Fix item mention push notification argument

* Fix zap push notification using stale msats
2024-07-01 15:51:59 -05:00
keyan
6e1d67b3c0 fix optimistic responses of bounties and poll votes 2024-07-01 14:58:53 -05:00
keyan
1e9d1ce66c fix zap optimistic response 2024-07-01 14:56:17 -05:00
Keyan
ca11ac9fb8
backend payment optimism (#1195)
* wip backend optimism

* another inch

* make action state transitions only happen once

* another inch

* almost ready for testing

* use interactive txs

* another inch

* ready for basic testing

* lint fix

* inches

* wip item update

* get item update to work

* donate and downzap

* inchy inch

* fix territory paid actions

* wip usePaidMutation

* usePaidMutation error handling

* PENDING_HELD and HELD transitions, gql paidAction return types

* mostly working pessimism

* make sure invoice field is present in optimisticResponse

* inches

* show optimistic values to current me

* first pass at notifications and payment status reporting

* fix migration to have withdrawal hash

* reverse optimism on payment failure

* Revert "Optimistic updates via pending sats in item context (#1229)"

This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1.

* add onCompleted to usePaidMutation

* onPaid and onPayError for new comments

* use 'IS DISTINCT FROM' for NULL invoiceActionState columns

* make usePaidMutation easier to read

* enhance invoice qr

* prevent actions on unpaid items

* allow navigation to action's invoice

* retry create item

* start edit window after item is paid for

* fix ux of retries from notifications

* refine retries

* fix optimistic downzaps

* remember item updates can't be retried

* store reference to action item in invoice

* remove invoice modal layout shift

* fix destructuring

* fix zap undos

* make sure ItemAct is paid in aggregate queries

* dont toast on long press zap undo

* fix delete and remindme bots

* optimistic poll votes with retries

* fix retry notifications and invoice item context

* fix pessimisitic typo

* item mentions and mention notifications

* dont show payment retry on item popover

* make bios work

* refactor paidAction transitions

* remove stray console.log

* restore docker compose nwc settings

* add new todos

* persist qr modal on post submission + unify item form submission

* fix post edit threshold

* make bounty payments work

* make job posting work

* remove more store procedure usage ... document serialization concerns

* dont use dynamic imports for paid action modules

* inline comment denormalization

* create item starts with median votes

* fix potential of serialization anomalies in zaps

* dont trigger notification indicator on successful paid action invoices

* ignore invoiceId on territory actions and add optimistic concurrency control

* begin docs for paid actions

* better error toasts and fix apollo cache warnings

* small documentation enhancements

* improve paid action docs

* optimistic concurrency control for territory updates

* use satsToMsats and msatsToSats helpers

* explictly type raw query template parameters

* improve consistency of nested relation names

* complete paid action docs

* useEffect for canEdit on payment

* make sure invoiceId is provided when required

* don't return null when expecting array

* remove buy credits

* move verifyPayment to paidAction

* fix comments invoicePaidAt time zone

* close nwc connections once

* grouped logs for paid actions

* stop invoiceWaitUntilPaid if not attempting to pay

* allow actionState to transition directly from HELD to PAID

* make paid mutation wait until pessimistic are fully paid

* change button text when form submits/pays

* pulsing form submit button

* ignore me in notification indicator for territory subscription

* filter unpaid items from more queries

* fix donation stike timing

* fix pending poll vote

* fix recent item notifcation padding

* no default form submitting button text

* don't show paying on submit button on free edits

* fix territory autorenew with fee credits

* reorg readme

* allow jobs to be editted forever

* fix image uploads

* more filter fixes for aggregate views

* finalize paid action invoice expirations

* remove unnecessary async

* keep clientside cache normal/consistent

* add more detail to paid action doc

* improve paid action table

* remove actionType guard

* fix top territories

* typo api/paidAction/README.md

Co-authored-by: ekzyis <ek@stacker.news>

* typo components/use-paid-mutation.js

Co-authored-by: ekzyis <ek@stacker.news>

* Apply suggestions from code review

Co-authored-by: ekzyis <ek@stacker.news>

* encorporate ek feeback

* more ek suggestions

* fix 'cost to post' hover on items

* Apply suggestions from code review

Co-authored-by: ekzyis <ek@stacker.news>

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 12:02:29 -05:00
ekzyis
30e29f709d
Add missing CANCELLED status (#1248) 2024-07-01 08:43:13 -05:00
keyan
fb52d5314d fix missing subname on this day in sn 2024-06-30 11:45:06 -05:00
keyan
9e44baa7f5 make a this day in sn generator page 2024-06-29 18:06:02 -05:00
keyan
658fe73920 upgrade compatibility for #1195 2024-06-29 10:35:51 -05:00
keyan
4fc832c7ed don't import css into /lib 2024-06-25 17:18:21 -05:00
Keyan
1e3d37d6b0
Update awards.csv 2024-06-25 16:36:20 -05:00
nichro
a95402e3be
Added support for <sub> <sup> in markdown (#1215)
* Add support for sub and superscript in markdown

* Removed empty line as per lint

* renamed schema to rehypeSanitizeSchema to make it less generic

* Linting fixes

* Update components/text.js

Co-authored-by: ekzyis <ek@stacker.news>

* Reverting changes: remove rehype-raw&sanitize, clean up

* Draft iteration of rehypeStyler plugin

* rehypeStyler visiting element nodes properly to catch tag-text-tag patterns

* Refreshed package-lock

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-06-25 14:23:18 -05:00
ekzyis
ddaec36617
Fix litd healthcheck (#1246) 2024-06-24 20:28:42 -05:00
ekzyis
3f6581f119
Fix user popover if mention inside link (#1245) 2024-06-23 12:15:26 -05:00
ekzyis
923b21610f
Fix item mention ref pattern (#1244) 2024-06-23 12:14:11 -05:00
keyan
87e198cc04 add this day in stacker news to newsletter 2024-06-22 15:23:30 -05:00
Keyan
a55e865222
Update awards.csv 2024-06-22 15:18:37 -05:00
Tom
4fe920d12b
Handle Peertube Embeds (#1223)
* Handle peertube embeds

* Permit full screen for Rumble and PeerTube

* Use sandbox='allow-scripts' for iframes

* Restore frame-src domains

* Use endsWith

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-06-20 11:28:25 -05:00
keyan
78520b787b dont use broken navigator.share on desktop safari 2024-06-14 13:44:36 -05:00
Keyan
8329da1f56
Update awards.csv 2024-06-13 16:40:16 -05:00
keyan
b1f850ee0e clear email hash when email is unlinked 2024-06-13 12:14:08 -05:00
ekzyis
8a19fc0905
Use <Alert> for auth banner in /settings (#1238) 2024-06-12 18:16:54 -05:00
ekzyis
286f53f2b3
Update zap undos info (#1237) 2024-06-12 18:16:41 -05:00
ekzyis
cbcae1d128
Fix downzaps (#1236)
* Add optimistic update for downzaps

* Add optimistic update for zaps via dropdown

* Also use lightning strike for downzaps
2024-06-12 13:24:04 -05:00
ekzyis
967b5b74fb
Fix anon payment verification (#1235)
* Enforce hash & hmac for anons in serialize

* Enforce logged in for idempotent zaps
2024-06-12 11:15:00 -05:00
ekzyis
93713b33df
Optimistic updates via pending sats in item context (#1229)
* Use context for pending sats

* Fix sats going negative on zap undo

We already handle undoing pending sats by wrapping the payment+mutation with try/finally.

* Remove unnecessary ItemContextProvider

* Rename to parentCtx

* Fix hierarchy of ItemContextProvider

If a comment was root and it was zapped, the pending sats contributed to the sats shown in <CommentsHeader>.

This was caused by <CommentsHeader> accessing the root item context for all comments, even for the root comment.

So even if the root comment was zapped, the pending sats contributed to the sats for the comment section.

This wasn't the case for posts since their item context was above the context used by <CommentsHeader>.

This was fixed by moving <ItemProviderContext> down into <Comments> and <Item> instead of declaring it at <ItemFull> which wraps the root item and all comments.

* Optimistic update for poll votes

* prevent twice optimistic zap

* enhance client notifications with skeleton and no redudant queries

* enlarge nwc amount limits

* Disable max amount and daily limit in NWC container

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-06-12 08:34:24 -05:00
Tom
569d0448c2
Remove toast error (#1231) 2024-06-11 11:22:20 -05:00
ekzyis
35be035850
More zap undo fixes III (#1228)
* Fix pending state not immediately updated

Before, the bolt wasn't rerendered if the user clicked again within the undo delay since no state changed.

* Fix zap undo pulse only shown on hover
2024-06-06 08:22:05 -05:00
ekzyis
09f9efa189
Remove strike delay (#1227) 2024-06-05 11:49:09 -05:00
ekzyis
79ed07ae74
Embed youtube shorts (#1225) 2024-06-05 09:08:27 -05:00
ekzyis
23c51df283
Revert reverse refs (#1224)
* Remove reverse internal refs

* Formatting
2024-06-05 08:21:01 -05:00
keyan
1dcb6461c7 give sndev logs better default params 2024-06-04 13:07:03 -05:00
keyan
2775b49ce7 fix inconsistency in url handling and don't let parseEmbedURL throw 2024-06-04 12:10:37 -05:00
ekzyis
ea97fbf4a4
Avoid manual optimistic updates for now (#1220)
* Avoid manual optimistic zap updates for now

* Remove manual optimistic updates for pay-bounty and poll
2024-06-04 03:02:34 -05:00
ekzyis
d8fe698963
Fix missing commentId parsing for item mentions (#1219) 2024-06-03 21:54:42 -05:00
keyan
061d3f220d fix local dev missing snl row 2024-06-03 16:55:03 -05:00
keyan
c90eb055c7 fix performance of sub nested resolvers 2024-06-03 16:50:38 -05:00
keyan
7b667821d2 fix notification indicator for item mentions 2024-06-03 16:34:38 -05:00
Keyan
b4c120ab39
Update awards.csv 2024-06-03 14:40:37 -05:00
SatsAllDay
e3571af1e1
Make Polls Anonymous (#1197)
* make polls anonymous

Introduce a `PollBlindVote` DB table that tracks when a user votes in a poll,
but does not track which choice they made.

Alter the `PollVote` DB table to remove the `userId` column, meaning `PollVote`
now tracks poll votes anonymously - it captures votes per poll option,
but does not track which user submitted the vote.

Update the `poll_vote` DB function to work with both tables now.

Update the `item.poll` resolver to calculate `meVoted` based on the `PollBlindVote`
table instead of `PollVote`.

* remove `meVoted` on `PollOption`s

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-06-03 13:56:43 -05:00
ekzyis
2597eb56f3
Item mention notifications (#1208)
* Parse internal refs to links

* Item mention notifications

* Also parse item mentions as URLs

* Fix subType determined by referrer item instead of referee item

* Ignore subType

Considering if the item that was referred to was a post or comment made the code more complex than initially necessary.

For example, notifications for /notifications are deduplicated based on item id and the same item could refer to posts and comments, so to include "one of your posts" or "one of your comments" in the title would require splitting notifications based on the type of referred item.

I didn't want to do this but also wanted to have consistent notification titles between push and /notifications, so I use "items" in both places now, even though I think using "items" isn't ideal from a user perspective. I think it might be confusing.

* Fix rootText

* Replace full links to #<id> syntax in push notifications

* Refactor mention code into separate functions
2024-06-03 12:12:42 -05:00
Tom
d454bbdb72
Fix issue with popover on full sn links (#1216)
* Fix issue with popover on full sn links

* allow custom text for internal links

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-06-03 12:05:31 -05:00
ekzyis
86b857b8d4
Allow SN users to edit special items forever (#1204)
* Allow SN users to edit special items

* Refactor item edit validation

* Create object for user IDs

* Remove anon from SN_USER_IDS

* Fix isMine and myBio checks

* Don't update author

* remove anon from trust graph

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-06-03 11:26:19 -05:00
SatsAllDay
e9aa268996
add fund_user sndev command (#1214)
* add `fund_user` sndev command

* update help and set msats rather than add

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-06-03 09:37:58 -05:00
Keyan
3d3dc52cec
Update awards.csv 2024-05-31 10:23:31 -05:00
Tom
0576716c4a
Close related section (#1207)
* Fix issue with closing accordian

* Use onSelect

* Remove change to AccoridanCard

* Fix className typo

* Restore conditional
2024-05-31 10:20:52 -05:00
ekzyis
a7bc757514
Use same button to generate and delete API keys (#1210) 2024-05-30 16:24:18 -05:00
ekzyis
d2a981ca5d
Add bot badge to items created with API key (#1209) 2024-05-30 16:23:07 -05:00
ekzyis
050122c665
Use global score for top (#1202)
* Use global score for comments ordered by top

* Use global score for global top feed
2024-05-30 09:21:55 -05:00
ekzyis
6047c37e4e
Use refs for modals (#1200)
This fixes concurrent showModal calls with stale values which lead to modals getting replaced instead of stacked.
2024-05-30 09:20:33 -05:00
ekzyis
2e346b488d
Fix missing pointer for anon info (#1201) 2024-05-29 08:32:57 -05:00
ekzyis
4e14e2b5d3
Remove kr from SN users (#1203) 2024-05-29 08:29:48 -05:00
ekzyis
30718b9e1b
Fix TypeError due to invalid URL (#1206) 2024-05-29 08:26:42 -05:00
Keyan
738333efa3
Update awards.csv 2024-05-28 19:30:30 -05:00
keyan
033270b6ae fix serverside rendering of notifications 2024-05-28 12:55:12 -05:00
ekzyis
94cce9155d
Frontend payment UX cleanup (#1194)
* Replace useInvoiceable with usePayment hook

* Show WebLnError in QR code fallback

* Fix missing removal of old zap undo code

* Fix payment timeout message

* Fix unused arg in super()

* Also bail if invoice expired

* Fix revert on reply error

* Use JIT_INVOICE_TIMEOUT_MS constant

* Remove unnecessary PaymentContext

* Fix me as a dependency in FeeButtonContext

* Fix anon sats added before act success

* Optimistic updates for zaps

* Fix modal not closed after custom zap

* Optimistic update for custom zaps

* Optimistic update for bounty payments

* Consistent error handling for zaps and bounty payments

* Optimistic update for poll votes

* Use var balance in payment.request

* Rename invoiceable to prepaid

* Log cancelled invoices

* Client notifications

We now show notifications that are stored on the client to inform the user about following errors in the prepaid payment flow:

- if a payment fails
- if an invoice expires before it is paid
- if a payment was interrupted (for example via page refresh)
- if the action fails after payment

* Remove unnecessary passing of act

* Use AbortController for zap undos

* Fix anon zap update not updating bolt color

* Fix zap counted towards anon sats even if logged in

* Fix duplicate onComplete call

* Fix downzap type error

* Fix "missing field 'path' while writing result" error

* Pass full item in downzap props

The previous commit fixed cache updates for downzaps but then the cache update for custom zaps failed because 'path' wasn't included in the server response.

This commit is the proper fix.

* Parse lnc rpc error messages

* Add hash to InvoiceExpiredError
2024-05-28 12:18:54 -05:00
keyan
b81c5bcc78 make sndev pr track upstream better 2024-05-28 09:29:42 -05:00
Keyan
d2daad5b20
Update awards.csv 2024-05-28 09:06:14 -05:00
nichro
dc59153663
Biopage userheader popover (#1198)
* Issue #1180: userheader popover on bio page

* Added import for ItemPopover in user-header.js

* fix linting

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-28 09:03:25 -05:00
Keyan
26fe7fce33
Update awards.csv 2024-05-28 08:39:34 -05:00
Keyan
30e24bfe47
Update awards.csv 2024-05-28 08:35:54 -05:00
Keyan
61852523fc
Update awards.csv 2024-05-28 08:23:01 -05:00
Tom
52f57f8ac5
Embed Rumble Video (#1191)
* Render Rumble video in preview and posts

* Display Rumble video

* Remove workspace

* Add util function

* Use searchParam for id

* Update check for Rumble

* Update youtube match strings

* fix hostname conditions

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-28 08:18:32 -05:00
keyan
9c5bec06fb fix scroll on profile pages 2024-05-23 10:23:13 -05:00
keyan
211f549a50 fix item skeleton 2024-05-23 10:07:33 -05:00
Keyan
566f7726de
Update awards.csv 2024-05-23 09:42:38 -05:00
ekzyis
0719ec114d
Fix no red square for territory posts (#1187) 2024-05-23 09:35:06 -05:00
Keyan
517aff1da3
Update awards.csv 2024-05-22 16:30:28 -05:00
Keyan
3e83c51286
Update awards.csv 2024-05-19 16:01:17 -05:00
keyan
1cda3eb936 update lnbits dev port to not conflict on mac 2024-05-19 15:54:32 -05:00
SatsAllDay
852d2cf304
@remindme bot support (#1159)
* @remindme bot support

support reminders via @remindme bot, just like @delete bot

* minor cleanup

* minor query cleanup

* add db migration

* various fixes and updates:

* hasNewNotes implementation
* actually return notification component in ui
* delete reminder and job on item delete
* other goodies

* refactor to use prisma for deleting existing reminder

* * switch to deleteMany to delete existing Reminders upon edit/delete of post to satisfy prisma

* update wording in form toast for remindme bot usage

* update wording in the push notification sent

* transactional reminder inserts and expirein

* set expirein on @delete too

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-19 15:52:02 -05:00
ekzyis
b7353ddd69
Fix inverted lnbitsSchema env check (#1185) 2024-05-18 10:14:12 -05:00
ekzyis
7e1c62fdcb
Fix provider default setting (#1183) 2024-05-17 20:27:57 -05:00
ekzyis
91192025e5
Remove NWC script (#1182) 2024-05-17 20:27:47 -05:00
ekzyis
7119c2ef0b
Fix QR rerender on WebLN payment (#1181) 2024-05-17 20:27:34 -05:00
Keyan
cf995d0a9f
Update awards.csv 2024-05-17 10:50:01 -05:00
Tom Smith
2b2f2d589c
Show Alert Message on Settings Page (#1179)
* Show alert message

* Add extra line

* Use css var

* Restore lightning in filter

* Show alert at top of page
2024-05-17 10:47:06 -05:00
keyan
9dfdfe7329 update awards.csv payouts 2024-05-17 10:20:08 -05:00
Keyan
4aa3c67527
Update awards.csv 2024-05-17 08:41:22 -05:00
keyan
a71b9be03f fix subscription check on mute (#1177) 2024-05-17 08:30:14 -05:00
ekzyis
a585ba7f0a
Allow HTTP and HTTPS over Tor for LNbits (#1176) 2024-05-16 08:41:49 -05:00
keyan
c83ff02a85 update awards.csv 2024-05-15 13:26:41 -05:00
keyan
85a4839538 update awards.csv absent date paid 2024-05-15 13:11:30 -05:00
Felipe Bueno
471888563e
Item popover (#1162)
* WIP Item Popover

* Hide user on ItemSumarry to avoid infinite popovers

* Introduce HoverablePopover

* Delete itempopover & userpopover css

* Fix excess bottom padding on the ItemPopover

* Fix ItemSummary: Use text for itens that doesn't have a title

* Handling #itemid/something links + Tweaks for rendering comment summary

* refine hoverable popover

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-15 12:05:50 -05:00
Tom Smith
691818e779
[1124] - Use Mempool For Fee Rate (#1171)
* Use mempool for fee rate

* Add minor logic change

* Restore carousel items
2024-05-15 10:26:49 -05:00
ekzyis
c6ab776091
Add nostr-wallet-connect-lnd container (#1174)
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-05-15 10:10:24 -05:00
ekzyis
47bfa24b57
Add lnbits container (#1173) 2024-05-15 10:09:15 -05:00
ekzyis
7090ea3b70
Fix stacker_cln missing in channdler dependency (#1172) 2024-05-14 16:59:05 -05:00
keyan
93e0b3ed6e fix broken lnc localstorage namespace since 5c593ce 2024-05-13 11:08:06 -05:00
keyan
7121317990 more accordian show states 2024-05-13 10:14:35 -05:00
Ben Allen
57fbab31b3
Force post options open when dirty or on errors (#1129)
* wip submit will open options

* fix: options show on error discussions

* lint

* feat: all types check for dirty or errors

* lint

* fix ordering

* dirty and error useEffects

* use formik context

* update dirty checks on forms

* revert dirty logic

* simplify handle error functions

* lint

* add myself to contributors and update awards

* use Formik context in adv-post-form

* move all logic into accordian item

* move logic up to adv-post-form

* lint

* errors open options every time

* refine dirty form accordians

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-12 16:17:41 -05:00
SatsAllDay
77c87dae80
honor mutes when sending push notifications (#1145)
* honor mutes when sending push notifications for:
* territory subscriptions
* mentions
* user subscriptions

Also, don't allow you to mute a subscribed user, or vice versa

* refactor mute detection for more code reuse

update mute/subscribe error messages for consistency

* variable rename

* move `isMuted` to shared user lib, reuse in user resolver and webpush

* update awards.csv

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-05-12 13:55:56 -05:00
Abhi Shandilya
512b08997e
fix: deny zaps for deleted items (#1158) 2024-05-11 21:00:08 -05:00
Abhi Shandilya
0fda8907e7
fix: show related items on pinned items (#1157)
* fix: show related items on pinned items

* fix condition

* use subName since sub could be undefined

* Update components/item-full.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Revert "Update components/item-full.js"

This reverts commit d1b785b8490c9356548ef1bfe246ae526f0237c6.

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-05-11 20:41:59 -05:00
Hezron Karani
0c7ff80fb8
add shellcheck workflow (#1147)
* add shellcheck workflow

* add shellcheck workflow

* Shellcheck to fail only with error severity

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-05-11 20:40:44 -05:00
Abhi Shandilya
6ed0e53fc1
fix: custom calendar dark theme (#1123)
* fix: custom calendar dark theme

* refine custom date picker

* color the triangle

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-11 20:32:44 -05:00
ekzyis
aacf7c6170
Ignore invalid certificate over Tor for CLN autowithdrawals (#1165)
* Use env var for tor proxy

* Disable cert validation over Tor
2024-05-09 21:07:47 -05:00
keyan
46efb770fb actually fix worker in local dev 2024-05-08 16:25:01 -05:00
keyan
32f0b1722f fix local dev worker env loading 2024-05-08 15:15:51 -05:00
keyan
7f77c59bd9 fix worker env load from relative path 2024-05-08 10:33:20 -05:00
ekzyis
8bc719b3a5
Don't repeat upvote fill color (#1156) 2024-05-07 14:20:22 -05:00
ekzyis
902875bd55
Fix images from media domain not loaded if imgproxyOnly enabled (#1153) 2024-05-06 16:39:20 -05:00
ekzyis
f0403a2bbe
Add script to use NWC with stacker lnd (#1151) 2024-05-06 15:05:06 -05:00
ekzyis
3f86981339
Fix wallet detach unavailable (#1150) 2024-05-06 15:04:05 -05:00
keyan
051cb69f5e fix sharing imgproxy link directly 2024-05-06 12:53:31 -05:00
keyan
ce9e146a06 fix missing embellishment in rewards leaderboard 2024-05-06 11:41:02 -05:00
keyan
111053006a fix 'startsWith' call on non-string 2024-05-06 11:26:19 -05:00
keyan
bc2155c7aa update awards.csv 2024-05-06 11:01:31 -05:00
keyan
1f2aa46319 give lnurlp invoices longer expirations for tor channels 2024-05-06 10:08:28 -05:00
keyan
1b5e513f5e update awards.csv 2024-05-06 09:07:13 -05:00
keyan
1e3042e536 update awards.csv 2024-05-04 19:17:04 -05:00
keyan
2e65bf9126 update awards.csv 2024-05-04 18:53:09 -05:00
keyan
30550e48be reorder mobile bottom nav to be more intuitive 2024-05-04 18:07:09 -05:00
SatsAllDay
15f9950477
Store hashed and salted email addresses (#1111)
* first pass of hashing user emails

* use salt

* add a salt to .env.development (prod salt needs to be kept a secret)
* move `hashEmail` util to a new util module

* trigger a one-time job to migrate existing emails via the worker

so we can use the salt from an env var

* move newsletter signup

move newsletter signup to prisma adapter create user with email code path
so we can still auto-enroll email accounts without having to persist the email address
in plaintext

* remove `email` from api key session lookup query

* drop user email index before dropping column

* restore email column, just null values instead

* fix function name

* fix salt and hash raw sql statement

* update auth methods email type in typedefs from str to bool

* remove todo comment

* lowercase email before hashing during migration

* check for emailHash and email to accommodate migration window

update our lookups to check for a matching emailHash, and then a matching
email, in that order, to accommodate the case that a user tries to login
via email while the migration is running, and their account has not yet been migrated

also update sndev to have a command `./sndev email` to launch the mailhog inbox in your browser

also update `./sndev login` to hash the generated email address and insert it into the db record

* update sndev help

* update awards.csv

* update the hack in next-auth to re-use the email supplied on input to `getUserByEmail`

* consolidate console.error logs

* create generic open command

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-04 18:06:15 -05:00
ekzyis
6220eb06ee
Use proxy agents for CLNRest over Tor (#1136) 2024-05-03 17:00:28 -05:00
itsrealfake
ed9fc5d3de
Add Footer to User Page (#1135)
* Add Footer to User Page

this closes #1016

* Hide Footer when no user.bio

* apply review-bot suggestion

* refine profile footer

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-03 16:55:07 -05:00
ekzyis
5c593ce280
Fix unintended sharing of wallets and logs (#1127)
* Suffix localStorage key for attached wallets with me.id

* Suffix IndexedDB database name with me.id

* Fix TypeError: Cannot destructure property of 'config' as it is null

* Detach wallet on logout

* Migrate to new storage keys

* Use Promise.catch for togglePushSubscription on logout

It's more concise

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-05-03 16:42:00 -05:00
Felipe Bueno
72c27e339c
UserPopover (#1094)
* WIP UserPopover

* Add show delay on UserPopover

* UserDetails -> StackingSince on UserPopover

* Make UserPopover hoverable

* Add felipe to contributors.txt

* Remove export from SocialLink

* Remove @ outside of UserPopover

* userQuery -> useLazyQuery + Handling user not found

* Move styles to user-popover.module.css

* Update components/user-popover.module.css

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Remove poll + SSR check from useLazyQuery

* USER_FULL -> USER (we are only using stacking since, for now)

* refine user popover

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-03 16:39:21 -05:00
keyan
cdeaa35ff4 fix new logger.error undefined 2024-05-03 14:49:31 -05:00
itsrealfake
a34c8dc7e9
add TypePolicy to Fact (#1138)
closes #995

enables apollo cache to work for 'stacked' 'spent' in /statistics page.
2024-05-03 14:35:16 -05:00
Ben Allen
64de3c3b94
Add explicit word break for items (#1137)
* add word break for notification items when they are too long

* don't use global css
2024-05-03 14:18:50 -05:00
ekzyis
98a27caaa9
Allow http: and ws: in dev CSP (#1126)
* Allow HTTP in dev build

* Also allow ws://
2024-05-03 14:17:10 -05:00
ekzyis
4961cc045b
Allow deletion of wallet logs (#1101)
* Allow deletion of wallet logs

* Refactor wallet logs client<>server glue code

* Use variant='link' and className='text-muted fw-bold nav-link' for clear & cancel

There is a bug though: 'clear' stays highlighted after modal is closed

* Include wallet in toast

* Delete logs on logout

* Fix ugly wallet name in confirm dialog

* Fix clear still highlighted after modal closed

* Only delete client wallet logs

* Fix ugly wallet name in toast

* Fix bad search and replace

* Use Wallet object as constant

* Also delete LNC logs on logout

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-05-03 14:14:33 -05:00
itsrealfake
e5f8c4e8e8
closes #1096 (#1130) 2024-05-03 14:09:27 -05:00
keyan
6aa5991520 litcli passthrough 2024-05-02 19:46:27 -05:00
keyan
990128da86 fix daily rewards 2024-05-02 00:18:45 -05:00
Keyan
fd2008e5d1
reintroduce daily rewards (#1134)
* reintroduce daily rewards

* update reward sponsor

* daily rewards countdown

* update rewards job schedule
2024-05-01 09:30:36 -05:00
keyan
54bbb0cc52 fix #1132 broken satistics links 2024-04-30 17:52:34 -05:00
keyan
be20500c06 account for fixed 1m for msm april 2024-04-30 14:51:48 -05:00
ekzyis
f2f3f71dd5
Fix migration name (#1131) 2024-04-30 14:14:10 -05:00
ekzyis
df631878e0
Fix duplicate autowithdrawal logs (#1121)
* Fix duplicate autowithdrawal success log

* Fix duplicate autowithdrawal error log
2024-04-29 20:39:31 -05:00
Keyan
e9a33ae12e
Update awards.csv 2024-04-29 20:05:55 -05:00
ekzyis
84b4d98c5c
Fix missing logger.error and setStatus (#1122) 2024-04-29 11:57:35 -05:00
ekzyis
bd37ec17cc
Use njump.me for nostr links (#1120)
njump is what is powering the preview in nostr.com so it seems more fitting to directly go to njump.me

See https://github.com/fiatjaf/njump
2024-04-28 17:25:25 -05:00
Keyan
ccc9cefb68
Update awards.csv 2024-04-28 17:21:07 -05:00
Abhi Shandilya
bddc2b1508
fix: hide related posts in deleted item (#1119) 2024-04-28 17:17:19 -05:00
keyan
4a6e3ed735 update awards.csv 2024-04-28 17:11:09 -05:00
keyan
71f3dba891 benalleng awards catchup 2024-04-28 16:27:55 -05:00
Ben Allen
8a735791ce
Add dashboard to satistics page (#1099)
* updated graphs ad queries

* fixed query

* fixed graph data pull

* converted msats to sats

* linter fix

* Fixed labels for graphs

* linter

* linter

* feat: style header

* lint

* fix: mobile navbar link and graph titles

* style charts

* change key names

* refine satistics graphs

---------

Co-authored-by: Dillon <dilloncortez@gmail.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-04-28 16:05:23 -05:00
ekzyis
4be54b74df
Use !configured to disable LNC default checkbox (#1117) 2024-04-28 13:03:37 -05:00
ekzyis
1039cd7586
Make checklist questions bold (#1118) 2024-04-28 13:03:00 -05:00
ekzyis
21d61b9c8a
Update PR template (#1114)
Co-authored-by: ekzyis <ekzyis@ekzy.is>
2024-04-28 11:04:16 -05:00
ekzyis
0f95eb6c36
Only provide WebLN if enabled (#1115)
Co-authored-by: ekzyis <ekzyis@ekzy.is>
2024-04-28 11:03:07 -05:00
keyan
b8e153a4be deal with webln unlock in sendPayment 2024-04-27 20:00:54 -05:00
keyan
4a14e0342b don't allow lnc edits because they won't work 2024-04-27 19:02:16 -05:00
keyan
2da3762d40 unattach -> detach 2024-04-27 18:37:57 -05:00
keyan
da71103e42 clear lnc state on detach 2024-04-27 18:37:57 -05:00
keyan
b9d30b4076 count pending withdrawals toward balance 2024-04-27 18:37:57 -05:00
ekzyis
10e58d41c7
Remove .env.local from env_file (#1113)
Arrays for env_file are only supported in Docker Compose >=v2.24 which is too new (from January 2024). Most distros distribute older packages.

Since --env-file as defined in the sndev script acts as an override for env_file anyway, we can safely remove it here.

Co-authored-by: ekzyis <ekzyis@ekzy.is>
2024-04-27 18:25:37 -05:00
Keyan
c3d709b025
add lnc attached wallet (#1104)
* add litd to docker env

* lnc payments

* handle locked wallet configuration

* create new lnc connection for every action

* ensure creds are decrypted before reconnecting

* perform permissions check
2024-04-26 21:22:30 -05:00
keyan
2340df3d8f update footer 2024-04-26 11:26:01 -05:00
keyan
2180afaed0 lower wallet limit to 100k 2024-04-25 19:37:22 -05:00
ekzyis
95b03c4bbf
Show preimage of confirmed withdrawals (#1106)
* Show preimage of confirmed withdrawals

* Assign preimage in one-liner
2024-04-25 19:33:24 -05:00
keyan
13eda4c120 add geninvites script 2024-04-24 13:30:08 -05:00
ekzyis
cc7d9d734c
Support LNURL-verify (#1103) 2024-04-23 20:28:25 -05:00
Ben Allen
255f97a2b3
flip the icons for consistent UX (#1100) 2024-04-23 09:46:27 -05:00
Keyan
d41b2e14f1
Update awards.csv 2024-04-22 10:08:33 -05:00
Keyan
15c6843d80
Update awards.csv 2024-04-21 17:44:45 -05:00
Ben Allen
ecedbd1527
Add PasswordInput component (#1090)
* feat: add PasswordHider

* feat: add PasswordInput

* fix typo and require requirement

* merge state

* use ...props and lnbits password required

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-04-21 17:28:57 -05:00
SatsAllDay
fbd3f8efed
introduce persistOnNavigate option for toasts (#1095)
ensure that post auto-delete success toasts are preserved
when navigating back to the prior page
2024-04-21 17:25:48 -05:00
keyan
fa5adac297 improve newsletter top stories format 2024-04-21 14:12:29 -05:00
SatsAllDay
01de4e7ba7
fix sndev to make .env.local optional in docker compose files (#1098) 2024-04-20 16:50:43 -05:00
SatsAllDay
d7ecbbae3a
Search bookmarks (#1075)
* Support `is:bookmarked` search option to search my bookmarked items

* Update the worker search module to include `bookmarkedBy: Array<Number>` which
contains the list of user ids which have bookmarked a given item

* Add a trigger on the `Bookmark` DB table to re-index the corresponding item when
a bookmark is added/removed

* Update the Search resolver to check for a `is:bookmarked` query option. If provided,
include it as an option in the search request. This updates search to look for items
which are bookmarked by the current user. By default, this preserves stacker privacy
so you can only search your own bookmarks

* Update the search page UI to show how to invoke searching your own bookmarks

* undo `is:bookmarked` support, add `bookmarks` item in search select

* short circuit return empty payload for anon requesting bookmarks

* remove console.log for debugging

* fix indexing a new item that has yet to be bookmarked

* update db migration to re-index all existing bookmarked items one time

* fix the case where deleting a bookmark doesn't trigger a new index of items

explictly specify a `updatedAt` value when deleting a bookmark, to ensure that
deleting a bookmark results in a new indexed version of the bookmarked item

* update search indexer to use the latest of all three choices for the latest version

* give bookmark index jobs longer expiration

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-04-19 13:24:48 -05:00
ekzyis
0552736dc7
More concise PR template (#1092)
The big HTML comments were getting in the way.
2024-04-19 10:56:08 -05:00
Ben Allen
9e6675b8d2
Add 200ms delay to scroll in capture worker (#1088)
* fix: add 200ms delay to scroll in capture worker

* use: new Promise to wait for timeout
2024-04-18 10:49:42 -05:00
keyan
a77f778f27 fix hidden wallet balance layout shift 2024-04-17 17:22:17 -05:00
ekzyis
59b3d1c375
Round sats in FORWARDEDTIP push notification (#1086) 2024-04-17 14:24:07 -05:00
keyan
f9a99a7deb give 1 sat fee button overlay but not button text 2024-04-17 14:22:31 -05:00
ekzyis
dd6e921e2e
Fix local env in docker-compose.yaml (#1085)
* Allow docker env override via .env.local

* Make .env.local optional

* Fix env var expansion ignoring .env.local

* Rename .env.development to .env.docker

* Use YAML anchors

* Revert rename of .env.development
2024-04-17 13:46:18 -05:00
keyan
0ea261428c give edit and countdown space again 2024-04-17 13:19:10 -05:00
ekzyis
40463d526c
Fix TypeError: Failed to construct 'URL': Invalid URL (#1083) 2024-04-17 12:15:36 -05:00
Keyan
7d587c7cf8
Update awards.csv 2024-04-16 19:27:58 -05:00
Ben Allen
6a13c22ad2
Add comment scrolling in capture (#1079)
* feat: add comment scrolling in capture

* lint

* feat use .waitForSelectore method

* revert timeout
2024-04-16 19:25:56 -05:00
Keyan
bc85a63091
better env (#1082) 2024-04-16 19:23:36 -05:00
keyan
058f88da49 add halving to price carousel 2024-04-16 17:58:26 -05:00
keyan
a0f3e338a8 fix shift when loading paid invoice 2024-04-16 16:28:22 -05:00
keyan
052e36f6ed adjust qr skeleton for bolt11info 2024-04-16 16:20:13 -05:00
keyan
8eee1c2a71 update withdrawal skeleton 2024-04-16 16:13:06 -05:00
keyan
02fe4d5d92 update qr skeleton 2024-04-16 16:05:59 -05:00
keyan
1a25179a98 modal spacing fixes 2024-04-16 15:33:00 -05:00
ekzyis
e30dfbae57
Fix autowithdrawal logs (#1073)
* Also log autowithdrawal routing errors

* Only log autowithdrawal success in worker

* Use WalletType for WalletLog.wallet

* Fix autowithdrawal success message

* Infer walletName from walletType in upsertWallet
2024-04-16 13:59:46 -05:00
Keyan
c19c9124ec
Update awards.csv 2024-04-16 13:43:02 -05:00
ekzyis
927eaa8c5b
Exclude bios from spam detection (#1080) 2024-04-16 13:39:42 -05:00
Michael Bumann
dec4452d62
Use password field to NWC connection (#1076)
This should prevent the browser from saving and auto-completing the NWC connection string
2024-04-16 10:53:33 -05:00
ekzyis
796bd4dc4b
Add autowithdrawal badge in notifications (#1078) 2024-04-16 10:53:05 -05:00
keyan
00ca35465c replace node-fetch usage with existing cross-fetch 2024-04-15 19:26:40 -05:00
keyan
5689378b07 turn #1063 logic into a component for use in all comment lists 2024-04-15 16:23:26 -05:00
ekzyis
1faf309c00
Fix TypeError if autowithdrawal creation failed (#1072) 2024-04-15 09:17:37 -05:00
ekzyis
448f028ec1
Update node-fetch to v2.6.7 (#1070)
dependabot reported vuln with CVSS score of 8.8
2024-04-15 09:17:00 -05:00
ekzyis
2ec0a1a559
Use crypto.randomBytes for unique CLN invoice label (#1074) 2024-04-15 09:16:32 -05:00
Keyan
40131da8bd
Update awards.csv 2024-04-14 18:22:08 -05:00
keyan
72fb8a490c refine readme a bit 2024-04-14 18:02:13 -05:00
keyan
75771bfb7e remove deprecated version from readme override example 2024-04-14 17:44:19 -05:00
keyan
40e07be6d8 update sndev docs and auto-start for capture service 2024-04-14 17:43:55 -05:00
keyan
b05c1b6734 update README with up to date sndev help output 2024-04-14 17:42:39 -05:00
keyan
51f1c08a7e get rid of docker compose version number deprecation warning 2024-04-14 17:40:52 -05:00
keyan
d3f18c7cff get rid of docker nagware 2024-04-14 17:40:24 -05:00
ekzyis
9f4d5e13aa
CLN autowithdrawal (#1042)
* Add CLN node to docker-compose.yml

* Attach CLN wallet via CLNRest

* Remove leading space

* Implement autowithdrawal to CLN in worker

* Fix UnhandledSchemeError during build

See https://github.com/vercel/next.js/discussions/33982

* Refactor CLN invoice code into @/lib/cln

* Fix missing env vars

* Fix validation error if rune invalid

* Update header

* Add rune placeholder

* Fix missing expiry for test invoice

* Remove nonsensical comment

* Remove unnecessary async

* Show level SUCCESS as OK in logs

* Add stacker_cln commands to sndev

* fix sndev posix compliance, add cln_withdraw

* give stacker_cln larger channels

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-04-14 17:34:21 -05:00
Ben Allen
3aee93ee16
add media query sizing for rewards pie chart (#1068) 2024-04-14 16:28:09 -05:00
Ben Allen
de24b4a037
use the zindex-sticky variable (#1066) 2024-04-14 13:26:41 -05:00
Keyan
b1c45a4ab3
Update awards.csv 2024-04-13 13:17:22 -05:00
Keyan
0fb2e95729
anchor box for notifications (#1063)
* anchor box for notifications closes #202

* fix lint

* give priority click children pointer events too

* add iframe and simplify css
2024-04-13 13:14:17 -05:00
keyan
70fbe48e42 sndev matrix chat 2024-04-12 19:15:04 -05:00
Keyan
75232ba5fa
Update awards.csv 2024-04-12 18:39:47 -05:00
✨JP⚡
a28d690f28
Fix first zap when modal closed (#771) (#1055)
* Fix first zap when modal closed (#771)

 - Extract handlers
 - Remove unnecessary async keyword from callback
 - Assign a new key to force remounting of LongPressable component when modal is closed from long press
 - Remove hover state when closing modal, otherwise it stays colored

* Replace LongPressable with custom component

* Remove yarn.lock
2024-04-12 18:37:04 -05:00
Keyan
b477f23aac
Update awards.csv 2024-04-12 18:14:54 -05:00
ekzyis
7774910292
Fix cert required (#1057) 2024-04-11 18:59:51 -05:00
ekzyis
8956bc8809
Fix LND autowithdrawal error message (#1052) 2024-04-10 08:59:42 -05:00
ekzyis
f3c1ebefcf
Merge serializeInvoiceable with serialize without bug (#1051)
* Merge serializeInvoiceable with serialize

* Rename to verifyPayment

We already have a function named checkInvoice in the worker which can be confusing.

Also, we don't need to export this function.

* Use crypto.timingSafeEqual

* Fix missing unwrap for item creation and update
2024-04-09 19:49:20 -05:00
Keyan
c8480a4996
Update awards.csv 2024-04-09 18:42:20 -05:00
Ben Allen
255ad29897
fix: parseInternalLinks test (#1050) 2024-04-09 14:44:45 -05:00
abhiShandy
6d57bbffe5
fix: feetext defaults to free (#1031)
* fix: feetext defaults to free

* Update components/fee-button.js

Co-authored-by: ekzyis <ek@stacker.news>

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-04-09 11:12:24 -05:00
keyan
c7d926df30 define new env vars in service worker 2024-04-08 17:55:29 -05:00
keyan
81d3212ffb add NEXT_PUBLIC_URL 2024-04-08 17:54:39 -05:00
Ben Allen
5be6df0266
Internal links are not target=_blank by default (#1037)
* feat: internal links are not target=_blank by default

* feat: use <Link>

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-04-08 16:56:44 -05:00
Felipe Bueno
4633e8eb5e
Allow pasting image from clipboard (#1043)
* form.js: Allow pasting image from clipboard

* remove semicolons

* Check clipboardData.items before continuing

* == -> ===

* Remove semicolons... again =/

* fix Strings must use singlequote

* Ignore DataTransfer no-undef lint error

* new DataTransfer -> new window.DataTransfer()

* add multiple image pasting

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-04-08 16:31:02 -05:00
keyan
9f79ff1f89 Revert "Merge serializeInvoiceable with serialize (#1040)"
This reverts commit e7e7cbff0a54155a6cf059afe229d0456a3d6a93.
2024-04-08 15:50:39 -05:00
ekzyis
e7e7cbff0a
Merge serializeInvoiceable with serialize (#1040)
* Merge serializeInvoiceable with serialize

* Rename to verifyPayment

We already have a function named checkInvoice in the worker which can be confusing.

Also, we don't need to export this function.

* Use crypto.timingSafeEqual
2024-04-08 09:22:29 -05:00
SatsAllDay
e6b825dafe
Mute Management Settings Page (#1034)
* first pass of a mute mgmt page, ported from subscription mgmt page pr

* adjust error message for mutes

* muted users -> muted stackers

* fix typo in component name
2024-04-08 09:13:56 -05:00
SatsAllDay
91a0d1ccd7
env vars for polling intervals (#1038)
* env vars for polling intervals

add env vars for 4 different common polling intervals,
fast (1000), normal (30000), long (60000), extra long (300000)

use env vars in all `pollInterval` params to `useQuery`

* replace `setInterval`'s interval with `FAST_POLL_INTERVAL`
2024-04-08 09:13:12 -05:00
keyan
1d154ec9b5 attempt to fix lastChecked getting overwritten 2024-04-06 18:28:23 -05:00
keyan
1f466970b3 disallow referring self 2024-04-06 13:46:18 -05:00
keyan
3472670df5 add clientside fetch delay 2024-04-06 10:38:54 -05:00
keyan
9b98843541 fix Sub.createdAt -> Sub.created_at generically 2024-04-05 19:24:31 -05:00
ekzyis
0b07962e1c
Fix 404 for territories in profile (#1029) 2024-04-05 14:47:05 -05:00
ekzyis
ba8b37ffb9
Add missing space in front of founding date (#1027) 2024-04-05 11:52:11 -05:00
keyan
657ce1a4f8 fix misnamed field on sub resolver 2024-04-05 10:47:03 -05:00
Keyan
1d4ba7b9a6
Update awards.csv 2024-04-05 08:47:58 -05:00
Felipe Bueno
cc8b16a7e3
Fix for #1023 sndev: 317: Syntax error: redirection unexpected (#1024)
* printf -> echo

* Update sndev

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-04-05 08:40:12 -05:00
Keyan
76f90e0691
Update awards.csv 2024-04-04 19:53:19 -05:00
abhiShandy
2a08abd90c
fix: Reward page render error #1006 (#1018)
* fix: Reward page render error #1006

* accept coderabbit's suggestion

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* address lint issues

* use existing patterns

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-04-04 19:31:38 -05:00
Ben Allen
5fa7fd9a83
Bottom nav uses fixed position to fix firefox bug (#1011)
* use fixed position + div placeholder

* hide footer padding when not shown

* account for mobile inset

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-04-04 18:50:52 -05:00
keyan
a0b402b4d6 fix sndev pr (detatched version) 2024-04-04 18:31:07 -05:00
keyan
f0334b6719 Merge branch 'rerenders' 2024-04-04 18:10:13 -05:00
Keyan
5c18ef2317
prevent contexts causing rerenders (#1022) 2024-04-04 18:09:42 -05:00
keyan
e7fec21375 fix hidden wallet layout shift 2024-04-04 18:07:59 -05:00
keyan
b995035f46 prevent contexts causing rerenders 2024-04-04 17:52:59 -05:00
Keyan
2ee730f8b5
Update awards.csv 2024-04-04 16:36:01 -05:00
Felipe Bueno
a0e705b1c0
Just some minor fixes (#1012)
* Add .vscode/settings.json to .gitignore to allow local vscode settings without making the work tree dirty

* Swap (fix) Login & SignUp button ids + Make them both 112px wide
2024-04-04 16:31:53 -05:00
Keyan
c2d207fbbb
Update awards.csv 2024-04-04 16:07:21 -05:00
Keyan
168fd95bf0
Update awards.csv 2024-04-04 16:07:01 -05:00
Keyan
fa237d98c9
Update awards.csv 2024-04-04 16:04:58 -05:00
keyan
2bf11dc848 remove reliance on intersection observer 2024-04-04 15:38:27 -05:00
keyan
a785f907cb add sndev pr tracking option 2024-04-04 15:38:27 -05:00
ekzyis
9d897e9bf7
Use same onclose listener in sendPayment (#1020)
This is the onclose listener from getInfo.

This might also be a potential fix for undefined errors that I am getting.

With "undefined errors" I mean that the error itself is literally undefined.
2024-04-04 12:28:52 -05:00
ekzyis
6e75b9d274
Fix effect dependencies (#1019)
* Fix missing effect dependencies

* Remove unnecessary effect dependency
2024-04-04 12:23:49 -05:00
nl
32ea514286
less confusing expression for the hint of when the autowithdraw will be initiated (#1015)
* less confusing expression of when the autowithdraw will be initiated

* only change display value

* tentatively resolved linting errors: spacing around * and removed unused var import
2024-04-04 09:18:41 -05:00
ekzyis
ebcdc21728
Remove useRef for NWC relay (#1014)
* Remove useRef for NWC relay

* connect to relay for every payment for more reliable payments
* remove getInfoWithRelay method (no longer needed since we no longer use useRef)
* fix 'WebSocket is already in CLOSING or CLOSED state.' errors
* improve logging

* Log connection failures

* Fix no error thrown on validation error
2024-04-04 08:29:39 -05:00
ekzyis
9f06692e3e
Never notify on replies to self (#1013) 2024-04-03 19:39:46 -05:00
SatsAllDay
992fc54160
Subscription management page (#1000)
* first pass of a subscription management page under settings

* add tabs to settings ui

* NymActionDropdown

* update Apollo InMemoryCache to merge paginated list of my subscribed users

* various updates

* switch from UsersNullable to Users

* bake the nym action dropdwon into the user component

* add back fields to the user query

* `meSubscriptionPosts`, `meSubscriptionComments`, `meMute`

* Refetch my subscribed users when a user subscription is changed

* update user list to hide stats in the subscribed list users

* update my sub'd users fragment to remove unnecessary user fields

* memoize subscribe user context provider value to avoid re-renders

* use inner join instead of left join

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* throw error when unauthenticated

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-04-03 19:38:47 -05:00
ekzyis
15fb7f446b
Wallet Logs (#994)
* nwc wallet logs

* persist logs in IndexedDB

* Potential fix for empty error message

* load logs limited to 5m ago from IDB

* load logs from past via query param

* Add 5m, 1h, 6h links for earlier logs

* Show end of log

* Clamp to logStart

* Add log.module.css

* Remove TODO about persistence

* Use table for logs

* <table> fixes bad format with fixed width and message overflow into start of next row
* also using ---start of log--- instead of ---end of log--- now
* removed time string in header nav

* Rename .header to .logNav

* Simply load all logs and remove navigation

I realized the code for navigation was most likely premature optimization which even resulted in worse UX:
Using the buttons to load logs from 5m, 1h, 6h ago sometimes meant that nothing happened at all since there were no logs from 5m, 1h, 6h ago.
That's why I added a time string as "start of logs" so it's at least visible that it changed but that looked bad so I removed it.

But all of this was not necessary: I can simply load all logs at once and then the user can scroll around however they like.

I was worried that it would be bad for performance to load all logs at once since we might store a lot of logs but as mentioned, that's probably premature optimization.

WHEN a lot of logs are stored AND this becomes a problem (What problem even? Slow page load?), THEN we can think about this.

If page load ever becomes slow because of loading logs, we could probably simply not load the logs at page load but only when /wallet/logs is visited.

But for now, this works fine.

* Add follow checkbox

* Create WalletLogs component

* Embed wallet logs

* Remove test error

* Fix level padding

* Add LNbits logs

* Add logs for attaching LND and lnAddr

* Use err.message || err.toString?.() consistently

* Autowithdrawal logs

* Use details from LND error

* Don't log test invoice individually

* Also refetch logs on error

* Remove obsolete and annoying toasts

* Replace scrollIntoView with scroll

* Use constant embedded max-height

* Fix missing width: 100% for embedded logs

* Show full payment hash and preimage in logs

* Also parse details from LND errors on autowithdrawal failures

* Remove TODO

* Fix accidental removal of wss:// check

* Fix alignment of start marker and show empty if empty

* Fix sendPayment loop

* Split context in two
2024-04-03 17:27:21 -05:00
JP
a7105b90f2
Fix title from draft being dismissed (#718) (#991)
* Fix title from draft being dismissed

Add a way to bypass the title generation query when the url change is from local storage (draft) and not user interaction.

* Check draft title from storage

* Remove unused
2024-04-03 16:13:20 -05:00
SatsAllDay
4b77e7a1a9
Limit scope of API Keys (#989)
* first pass of disallowing certain APIs with API keys

Disallow the following APIs:
* item.act (zap)
* create withdrawal
* unlink auth method
* link unverified email

* disallow creating lnauths via API key to stop the flow of linking via lnauth

* undo the limitation on donating to rewards

* revert the assertion on createAuth

* assert no api key on createWithdrawal and sendToLNAddr

* incorporate PR feedback by adding API Key negative assertion to more mutations:

* `createInvite`
* `createAuth`
* `upsertWalletLND` by way of `upsertWallet`
* `upsertWalletLNAddr` by way of `upsertWallet`
2024-04-03 15:11:06 -05:00
Anis Khalfallah
0c0f303a11
Add capture's container from /capture (#1001)
* Modified docker-compose.yml to include capture from /capture

Signed-off-by: Anis Khalfallah <khafallah.anis@hotmail.com>

* Update capture's container to include health checks via /health api

* refine capure docker service

---------

Signed-off-by: Anis Khalfallah <khafallah.anis@hotmail.com>
Co-authored-by: Anis Khalfallah <khafallah.anis@hotmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-04-03 14:28:51 -05:00
Keyan
5a1f39d076
Merge pull request #1008 from stackernews/territory-header-createdAt
Add founding date to territory header
2024-04-03 13:00:00 -05:00
Keyan
e1e512deb7
Merge pull request #1009 from stackernews/refinv
Fix all known problems with invite and referral links
2024-04-03 10:10:16 -05:00
keyan
ffa86abdb3 fix referrer logic to work with lnauth/nostr auth 2024-04-02 20:25:35 -05:00
keyan
a764837776 update redirection logic for invites 2024-04-02 19:51:30 -05:00
keyan
f2589efc94 fix invite link parameter type 2024-04-02 19:14:06 -05:00
ekzyis
3cd80a54f6 Add createdAt to territory header 2024-04-03 01:17:32 +02:00
keyan
e2c7f4aa58 sndev logs command 2024-04-02 15:00:45 -05:00
keyan
77080f5acd use lnd event emitter from correct scope 2024-04-02 14:36:00 -05:00
keyan
d0e15eb887 give invite links a default limit 2024-04-02 10:09:21 -05:00
keyan
b997c19cf0 time is a bitch doc 2024-04-02 09:29:56 -05:00
keyan
6d55753eed fix daylight savings times issues in rewards 2024-04-01 20:56:09 -05:00
keyan
10a08f28f3 fix total rewards 2024-04-01 09:05:39 -05:00
keyan
302f5b5dfc refine copy for april msm 2024-04-01 08:56:13 -05:00
keyan
2c9e668177 april million sat madness 2024-04-01 08:56:13 -05:00
Keyan
6cc4f29382
Update README.md 2024-03-31 15:22:09 -05:00
keyan
b12cf53630 specify non-reward days better 2024-03-31 14:21:08 -05:00
keyan
1e3a836fbc update reward notification for msm 2024-03-30 18:46:01 -05:00
keyan
45f90bd3f9 update newsletter script with new search filters 2024-03-30 15:27:51 -05:00
Keyan
044cd19401
Merge pull request #1003 from stackernews/pr-template-headers
Use headers for sections in PR template
2024-03-30 12:20:04 -05:00
ekzyis
78f2a2cedf Use headers for section 2024-03-30 15:26:46 +01:00
Keyan
c0ffbc401a
Merge pull request #990 from stackernews/pr-template
Add PR template
2024-03-29 10:11:51 -05:00
Keyan
57b862431e
Merge pull request #997 from stackernews/996-qr-scanner-broken
Fix QR scanner collapsed to no width
2024-03-29 10:07:27 -05:00
Keyan
8a9e4b7472
Merge pull request #998 from stackernews/fix-middleware-csp-disabled
Fix CSP commented out in middleware
2024-03-29 10:06:34 -05:00
ekzyis
0434045f22 Refactor dev CSP logic
always uses string concatentation now
2024-03-29 15:35:25 +01:00
ekzyis
b7893634ac Fix CSP commented out in middleware 2024-03-29 15:27:51 +01:00
ekzyis
3a1ff7027e Fix QR scanner collapsed to no width 2024-03-29 12:50:57 +01:00
keyan
ef5a92dff4 match offcanvas nym position to navbar 2024-03-28 18:57:42 -05:00
keyan
9820055aee refine hiding bottom navbar when virtual keyboard opens 2024-03-28 18:18:44 -05:00
keyan
176d0e2e77 fix weird ios sticky behavior with virtual keyboard 2024-03-28 17:35:08 -05:00
ekzyis
009b09ece2 Add PR template 2024-03-28 23:14:50 +01:00
keyan
43ebb44866 consitent top padding settings, profiles, satistics 2024-03-28 17:09:57 -05:00
keyan
b577a4f4af account for sticky on clickToContext 2024-03-28 16:43:44 -05:00
keyan
b2616bdfdb change order of bell and post button for symettry 2024-03-28 16:35:21 -05:00
keyan
7dac5e79a6 prioritize showing wallet balance over price carousel 2024-03-28 16:32:53 -05:00
keyan
79b894514b fix sticky gap on some android devices 2024-03-28 16:17:05 -05:00
keyan
d5a018e48d hide bottom navigation when virtual keyboard is shown 2024-03-28 16:03:45 -05:00
keyan
ff37adba90 fix safari bottom inset problem 2024-03-28 15:00:23 -05:00
keyan
ce5d01037a use safe-area-inset-bottom for bottom nav padding 2024-03-28 12:57:27 -05:00
keyan
97927c1f9a consistent sticky search spacing 2024-03-28 12:37:26 -05:00
keyan
f08bd4ffc8 make api key nullable 2024-03-28 12:34:14 -05:00
keyan
dfc3d7dfa6 fix nav spacing 2024-03-28 12:33:58 -05:00
Keyan
5b18c1ff5b
Merge pull request #985 from stackernews/fix-modal-alignment
Fix alignment in modal
2024-03-28 11:22:43 -05:00
ekzyis
dcb7205278 Fix alignment in modal 2024-03-28 17:10:05 +01:00
Keyan
690d2849e9
Update awards.csv 2024-03-28 10:47:12 -05:00
Keyan
4c2fcec69b
Merge pull request #982 from stackernews/nav
Improved navigation with dedicated mobile navigation
2024-03-27 16:53:19 -05:00
keyan
6054afa10c simplify logic determining if territory select is shown 2024-03-27 16:29:04 -05:00
keyan
2575a4a494 offcanvas style and login fixes 2024-03-27 14:41:02 -05:00
keyan
f0911fde04 more spacing consistency 2024-03-27 14:04:04 -05:00
keyan
02ef61e1df add profile photo to me 2024-03-27 13:51:26 -05:00
keyan
316418327a make recent/top have consistent spacing 2024-03-27 13:44:36 -05:00
Keyan
df7e944bf8
Merge pull request #980 from stackernews/hashed-api-keys
Hash API keys with SHA-256 before storing them
2024-03-27 13:02:57 -05:00
Keyan
811942f567
Merge pull request #983 from stackernews/fix-typo-visibile
Fix typo 'visibile'
2024-03-27 11:23:41 -05:00
ekzyis
48342bc246 Fix typo 'visibile' 2024-03-27 15:45:17 +01:00
keyan
e1f183f48a navigation -> nav 2024-03-26 19:49:10 -05:00
keyan
b884bde24d check for new notifications in context/singleton 2024-03-26 19:35:18 -05:00
keyan
bfe5edcfe3 search all territories by default + more intuitive search filters 2024-03-26 18:37:40 -05:00
keyan
f2ba61e64b enhance navigation 2024-03-26 18:36:31 -05:00
ekzyis
8f39a229c3 Fix overlay trigger hitbox 2024-03-26 22:33:18 +01:00
ekzyis
121205fa4b Add delete obstacle 2024-03-26 22:33:18 +01:00
ekzyis
e68cbd8469 Remove unnecessary export of ApiKey component 2024-03-26 22:33:18 +01:00
ekzyis
17a0106fcc Hash API keys with SHA-256 and never show them again 2024-03-26 22:33:18 +01:00
Keyan
b4cef44a43
Update awards.csv 2024-03-26 14:32:43 -05:00
Keyan
a93fea4a1f
Merge pull request #975 from benalleng/option-key-prop
Add index to make unique key props on territory options
2024-03-26 14:31:43 -05:00
benalleng
e36b77ad68 fix: add index to make unique key props on territory options 2024-03-26 15:28:26 -04:00
Keyan
1b507a52db
fix extra awards.csv column 2024-03-26 14:22:59 -05:00
Keyan
e6f165df14
Update awards.csv 2024-03-26 14:22:12 -05:00
Keyan
b18b5d1638
Merge pull request #974 from felipebueno/reader_view_compatibility
Enable readerView compatibility (issue #884)
2024-03-26 14:00:50 -05:00
Keyan
89aaef5afb
Merge pull request #973 from stackernews/fix-zap-undo-wrong-default-value
Fix zap undo wrong default value shown
2024-03-26 13:58:17 -05:00
Felipe Bueno
170bf7464b fullItemContainer div -> article to make it compatible with browser's reader view 2024-03-26 14:10:18 -03:00
ekzyis
52b2b788f8 Fix zap undo wrong default value shown 2024-03-26 17:57:20 +01:00
Keyan
3730b89667
Merge pull request #972 from benalleng/issue923
Allow links to be fit to their content on the homepage
2024-03-26 11:00:56 -05:00
Keyan
acac75230d
Merge pull request #971 from stackernews/withdrawal-notifications
Withdrawal notifications
2024-03-26 10:20:38 -05:00
Keyan
812e76b399
Merge branch 'master' into issue923 2024-03-26 09:45:10 -05:00
keyan
0e015b65da fix lint problems from prior commit 2024-03-26 09:44:35 -05:00
benalleng
dc87594d92 feat: add max-width fit-content 2024-03-26 09:58:48 -04:00
keyan
dcc81c8b03 disable ios pwa prompt for more than a single page in #953 2024-03-25 20:46:51 -05:00
keyan
d15e060ff3 remove debug from prompt lol 2024-03-25 20:17:08 -05:00
keyan
f5e2ab8603 query param disable pwa prompt fixes #953 2024-03-25 20:10:56 -05:00
ekzyis
922d2394fd Remove unnecessary withdrawl field 2024-03-26 02:10:46 +01:00
ekzyis
a1317b97e9 Add missing unitSingular, unitPlural 2024-03-26 00:53:49 +01:00
ekzyis
d1f36b77d2 Add withdrawal push notifications 2024-03-26 00:50:48 +01:00
ekzyis
3388f818cf Add withdrawal notifications 2024-03-26 00:50:48 +01:00
Keyan
2f818fc968
Merge pull request #948 from felipebueno/mailhog
Add MailHog container to provide login with email without extra config
2024-03-25 17:54:57 -05:00
Keyan
2055ccaf41
Merge branch 'master' into mailhog 2024-03-25 17:46:56 -05:00
Keyan
3a00695041
Merge pull request #965 from stackernews/zap-undo-threshold
Use thresholds to trigger zap undos
2024-03-25 17:34:10 -05:00
Keyan
e60f1b80d3
Merge pull request #962 from stackernews/more-zap-undo-fixes
More zap undo fixes II
2024-03-25 17:32:30 -05:00
Keyan
cce5168b6f
Update awards.csv 2024-03-25 17:29:27 -05:00
Keyan
59b0027ad0
Merge pull request #970 from AustinKelsay/bugfix-crosspost-link-context
Add context field to crossposted link if present
2024-03-25 17:25:22 -05:00
Keyan
0cb2b7d4f6
Update awards.csv 2024-03-25 17:20:26 -05:00
austinkelsay
a9506c4532 Add context field to crossposted link if present 2024-03-25 17:16:45 -05:00
Keyan
811d46d9b5
Merge pull request #955 from SatsAllDay/901-chart-skeletons
Chart skeletons
2024-03-25 17:07:36 -05:00
Satoshi Nakamoto
263d0cc425 remove duplicative styles 2024-03-25 15:35:32 -04:00
Satoshi Nakamoto
32bc483e48 chart skeletons 2024-03-25 15:35:32 -04:00
ekzyis
81502f8645 Better zapUndos default 2024-03-25 20:34:28 +01:00
ekzyis
3da299bddc Fix zapUndos not nullable in schema 2024-03-25 20:34:28 +01:00
ekzyis
fe3724aa7d Rename to ZapUndosField
This makes the component name more consistent with the setting.
2024-03-25 20:34:28 +01:00
ekzyis
46f044552a Remove unnecessary false 2024-03-25 20:34:28 +01:00
ekzyis
c2aef34ba2 Add threshold for zap undos 2024-03-25 20:34:28 +01:00
ekzyis
42d7a31584 Fix custom zap modal closed after zap undo 2024-03-25 20:32:23 +01:00
ekzyis
0193ac97fe Fix toast progress bar jump due to end flow hack
If an underlying toast finished, an empty toast that automatically immediately hides was dispatched to end the flow ("end flow hack").

If this empty toast had the same tag, the code marked the top toast as hidden even though it was not hidden.

This meant that during render, the animation-delay for the top toast (which was already rendered) was added again, leading to a progress bar jump.

This is fixed by no longer using this "end flow hack" where a toast is dispatched but a dedicated function to end flows.
2024-03-25 20:32:23 +01:00
ekzyis
17071fa615 Add missing tag to custom zap toasts 2024-03-25 20:32:23 +01:00
Keyan
5efbe2e0a6
Merge pull request #968 from stackernews/sndev-keep-standard-ports
Keep standard ports inside container
2024-03-25 13:31:19 -05:00
ekzyis
f4a7819bb3 Keep standard ports inside container
This makes it possible to continue to use lncli without --rpcserver inside the docker container even if the GRPC port exposed on the host machine is different.

This is the case on my machine since I am running a mainnet LND node on my machine so port 10009 is already used.

I could change the port to 20009 via LND_GRPC_PORT and STACKER_LND_GRPC_PORT (they can even have the same port) but then lncli inside the container needs to be aware of that which means that the sndev script would need to parse .env.development (or some other "magic") to know the GRPC port inside the container.

However, I decided that using standard ports inside the container is better to keep the sndev script simple at the cost of having to think about host vs container ports since they are different now.

One reason for that is that I think one even does not need to think about the host ports since they aren't even needed? But that's another topic.
2024-03-25 19:02:32 +01:00
Keyan
7e10325309
Merge pull request #966 from stackernews/delete-api-webpush
Remove unused api/webPush/index.js
2024-03-25 09:26:20 -05:00
Keyan
2597d431c2
Merge pull request #967 from stackernews/remove-unnecessary-sndev-fn
Remove unused functions in sndev
2024-03-25 09:26:01 -05:00
Keyan
f6729125e7
Merge pull request #964 from stackernews/tsx-platform-fix
reinstall tsx without node_modules present
2024-03-25 09:24:45 -05:00
ekzyis
176b3ec468 Remove unused functions in sndev 2024-03-25 14:50:51 +01:00
Felipe Bueno
e919efc144 MailHog healthcheck with wget instead of curl 2024-03-25 09:26:45 -03:00
ekzyis
00447d5662 Remove unused api/webPush/index.js 2024-03-25 13:14:21 +01:00
Felipe Bueno
06005e3e40 Fix mailhog web UI url on README 2024-03-25 09:09:35 -03:00
keyan
2387c8c307 reinstall tsx without node_modules present 2024-03-24 17:36:24 -05:00
Keyan
a289c9a1f4
Merge pull request #963 from stackernews/lastzapat
Add a denormalized lastZapAt field to items for notifications performance
2024-03-24 14:31:03 -05:00
keyan
edb3dd365c denormalize last zap on item for notification querying 2024-03-24 14:16:29 -05:00
keyan
2502c176f1 improve sndev db healthcheck 2024-03-24 13:59:23 -05:00
Keyan
8aee4f41df
Update awards.csv for #960 2024-03-24 13:52:46 -05:00
SatsAllDay
9bc95d4bb1
gracefully handle errors when fetching lnurlp wellknown info (#960)
if `fetch` or `req.json` fails, catch those errors and return a default error to the user

if the res payload indicates error but doesn't return a `reason`, also return the same
default error message to the user
2024-03-24 13:25:14 -05:00
Keyan
a7b0272200
denormalize replies (#958) 2024-03-23 23:15:00 -05:00
keyan
e565e74e2d award for #956 2024-03-23 20:43:39 -05:00
Keyan
5dafe510eb
sndev start docker compose overrides (#957) 2024-03-23 20:34:13 -05:00
SatsAllDay
90c9e21ac4
validate nym arg presence on ./sndev login command (#956)
check to ensure that a nym is provided to the login command before
proceeding. if omitted, echo an error message, display the help
for the login command, then exit with a non-zero exit code
2024-03-23 20:09:45 -05:00
keyan
101926a8bc award for #926 2024-03-23 17:00:30 -05:00
SatsAllDay
7087647cc0
Add top stackers, spender, and cowboys to newsletter (#954)
* Add top cowboys, stackers, and spenders to newsletter

* Rearrange to match the issue title

* fix top spenders `by` variable

* Update user resolver for top users `spending` `by` value

* wrap in try catch to not have errors break the script execution

return the array as defined whenever an error occurs
2024-03-23 12:23:31 -05:00
Felipe Bueno
eef9b3de4d Add email to COMPOSE_PROFILES on sndev + Cleanup README instructions 2024-03-21 15:02:58 -03:00
Felipe Bueno
561e62481d Add 'profiles: email' to the MailHog container 2024-03-21 09:16:29 -03:00
keyan
4b4a5361ef fix #951 also retroactively 2024-03-20 20:56:40 -05:00
ekzyis
01d779723f
Check userId set in sendUserNotification (#949) 2024-03-20 19:59:48 -05:00
ekzyis
889d494eaf
Disable billing autorenew on transfer (#950) 2024-03-20 19:59:06 -05:00
ekzyis
a1a82b9680
Delete unused lib/push-notifications.js (#947) 2024-03-20 19:58:42 -05:00
ekzyis
a22db1a6a3
Fix timing between billedLastAt and billPaidUntil (#952) 2024-03-20 19:52:38 -05:00
Felipe Bueno
4d02bae826 Fix mailhog link on README.md 2024-03-20 20:40:13 -03:00
ekzyis
b35302ccdc
Fix territory transfer sent to everyone (#946) 2024-03-20 16:45:30 -05:00
Felipe Bueno
82dc9b076d Add MailHog container to provide login with email without extra config 2024-03-20 18:41:25 -03:00
keyan
e8f9a186c6 fix image url construction 2024-03-20 14:18:11 -05:00
keyan
ecd075f483 fix path alias preventing builds 2024-03-20 13:41:32 -05:00
keyan
39652ee275 make dev s3 endpoints clearer 2024-03-20 13:23:59 -05:00
keyan
1e0cd468cc award for #942 2024-03-20 11:26:38 -05:00
SatsAllDay
051ec50e87
Remove leading dash to opt-in to being treated as vendor code (#942) 2024-03-20 11:13:22 -05:00
keyan
27f522a2cf add missing jsconfig for worker 2024-03-19 20:06:33 -05:00
ekzyis
d237861ff5
Use module path aliases (#938)
* Use module path aliases

* fix broken refactor

* path mapping for svgs, style, and remaining places (bonus: lose babel dep)

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-03-19 19:37:31 -05:00
ekzyis
22ff832efb
Don't export sendUserNotification (#937)
* Rename file to webPush.js

* Move webPush code into lib/webPush

* Don't export sendUserNotification

* Fix null in deposit push notification

* restore deposit notification change

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-03-19 17:43:04 -05:00
ekzyis
687d71f246
Purchase archived territories (#897)
* Handle archived territories in territory form

* Use dedicated mutation

* Add sanity check for eternal territories

* Fix fields and cost ignored

* Remove no longer needed manual validation in upsertSub

* Remove founder check

* Always check if sub is archived

Using { abortEarly: false } now since previously, if no description was not given, we wouldn't detect if the sub was archived since validation would abort on empty descriptions.

Only on submission all fields would get validated but since we ignore archived errors during submission, the user would never see that the sub is archived before submission
+ the wrong mutation would run if archived is not already true before submission.

Hence, we need to validate all fields always.

There is currently still a bug where the validation does not immediately run but maybe this can be fixed by simply using validateImmediately on the Formik component.

* Fix archived warning not shown after first render

* Only create transfers if owner actually changes

* Reuse helper functions in lib/territory.js

* Rename var to editing

* Use onChange instead of validation override

* Run same validation on server for unarchiving

* Fix 'territory archived' shown during edits

* Use && instead of ternary operator for conditional query

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-03-19 17:23:59 -05:00
ekzyis
b03295ce59
Put all Web Push code into lib/webPush.js (#936)
* Rename file to webPush.js

* Move webPush code into lib/webPush
2024-03-19 15:48:13 -05:00
ekzyis
2f9a3cc12c
Fix container with empty name not found (#935) 2024-03-19 15:44:44 -05:00
ekzyis
f4513b6710
Fix duplicate deposit push messages sent (#940) 2024-03-19 15:43:29 -05:00
keyan
39991575d6 increase boost minimum to 50k 2024-03-18 16:15:37 -05:00
keyan
a3defdea94 add missed row to awards.csv 2024-03-18 12:46:23 -05:00
keyan
d73ccb0dc0 update contribution awards for #933 2024-03-18 12:45:28 -05:00
Keyan
9a1de666fd
Merge pull request #933 from stackernews/sndev-profiles
sndev profiles
2024-03-18 12:39:13 -05:00
keyan
b94a159854 update readme to use sh code blocks 2024-03-18 12:38:00 -05:00
Keyan
501ca3c0a2
Merge branch 'master' into sndev-profiles 2024-03-18 09:28:12 -05:00
keyan
a61c1f241e just use an env var for this 2024-03-18 09:27:26 -05:00
keyan
08f1db3f68 sndev profiles 2024-03-17 20:43:34 -05:00
Keyan
18d126c610
Merge pull request #929 from stackernews/referral-link-privacy-setting
Add setting for no referral links on copy
2024-03-17 12:55:33 -05:00
Keyan
60cf7760d7
Merge pull request #930 from stackernews/921-pinned-posts-in-hot
Don't filter by pins in home
2024-03-17 12:51:45 -05:00
Keyan
1b2bb6d0fb
Merge pull request #931 from stackernews/fix-toc-slugs
Fix ToC due to repeated slug calls
2024-03-17 12:49:12 -05:00
ekzyis
5b15fd88a9 Fix ToC due to repeated slug calls 2024-03-17 18:24:39 +01:00
ekzyis
3e56bbe9c0 Don't filter by pins in home 2024-03-17 16:47:13 +01:00
ekzyis
2ba4063645 Add setting for no referral links on copy 2024-03-17 16:23:03 +01:00
Keyan
18df381497
add link to awards.csv in readme 2024-03-16 17:56:08 -05:00
Keyan
51358f5b79
Update awards.csv 2024-03-16 16:52:45 -05:00
keyan
ec2dcdfd92 add awards.csv for tracking contributor awards 2024-03-16 16:52:00 -05:00
Keyan
44d1101943
Merge pull request #925 from SatsAllDay/readme-minor-typo
Super minor typo in the readme
2024-03-16 16:50:55 -05:00
Satoshi Nakamoto
21e17c2147 super minor typo in the readme
I found this while reading the new readme and felt compelled to fix it :D
2024-03-16 16:41:43 -04:00
ekzyis
3ae3971fbe
Update README (#924)
* Make 'before requesting review' bold and mention usage of drafts

* Update responsible disclosure rules

* Add link to PGP key
2024-03-16 12:39:45 -05:00
Keyan
2b99284a5a
Update README.md image 2024-03-16 11:38:22 -05:00
keyan
b451ecaf8c catch imgproxy pro unvailable errors 2024-03-15 21:59:58 -05:00
keyan
56c2682c9e fix service worker env imports 2024-03-15 21:54:29 -05:00
keyan
806f42ed40 don't use imgproxy pro, set opensearch user/pass 2024-03-15 21:16:26 -05:00
Keyan
f436290460
readme typo 2024-03-15 15:29:31 -05:00
keyan
9ebe413a57 inform github linguist to ignore seed.sql 2024-03-15 14:39:44 -05:00
Keyan
bf4286600f
fix lncli typo in README.md 2024-03-15 14:31:46 -05:00
Keyan
efaa2193e0
new readme with contribution awards (#922)
* new readme

* Update README.md

* Update README.md

* move more docs to README

* add proper mapping for opensearch item index

* add sndev lint

* fix index creation
2024-03-15 14:29:42 -05:00
ekzyis
687012d1a0
API Keys (#915)
* Generate API key in settings

* Check x-api-key for GraphQL API requests

* Don't fallback to cookie if x-api-key header was provided

* Select all session fields

* Fix error if API key not found

* Fix style in settings via form-label className

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-03-14 15:32:34 -05:00
keyan
05702456e9 upgrade apollo/client for memory improvements 2024-03-14 14:22:30 -05:00
ekzyis
501885cfa0
Ignore if sub belongs to user during existence check (#904)
* Ignore if sub belongs to user during existence check

* Remove code no longer needed

* Fix territory edit

Territory edits were broken because validation failed for existing territories and if you edit an territory, it obviously already exists.

This commit fixes this by ignoring the territory that we're currently editing.

* Fix existence check using stale cache

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-03-14 11:17:53 -05:00
keyan
5065b32bde sndev login: fix identifier on conflict 2024-03-13 20:05:29 -05:00
ekzyis
c8e65d5a23
Don't hide self in top even if hidden (#905)
* Don't hide self in top even if hidden

* Also don't hide self in top cowboys

* only use anon icon for anon stuff

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-03-13 19:26:59 -05:00
keyan
0b8d952e78 add sndev login 2024-03-13 18:52:58 -05:00
ekzyis
96d3034c66
Fix image fees undefined and not enforced if logged in (#909)
* Fix image fees not enforced

* Fix image fees undefined

* Add comment about return value
2024-03-13 16:08:55 -05:00
ekzyis
9b5e63cb9c
Add security.txt (#912) 2024-03-13 16:04:28 -05:00
keyan
06bde475e7 add sndev pr command 2024-03-13 15:00:58 -05:00
ekzyis
f94de0f3ac
Fix zap shadow on hover (#906) 2024-03-13 11:48:16 -05:00
keyan
25cc986ba7 Merge branch 'localdev' 2024-03-13 11:25:50 -05:00
keyan
0f68ed379f prompt in help if docker isn't installed 2024-03-13 11:21:51 -05:00
Keyan
23ee62fb21
add sndev shell script and enhance docker compose local dev
* add hot reloading worker:dev script

* refine docker config

* sndev bash script and docker reliability stuff

* make posix shell

* restart: always -> unless-stopped

* proper check for postgres health

* add db seed to sndev

* refinements after fresh builds

* begin adding regtest network

* add changes to .env.sample

* reorganize docker and add static certs/macroon to lnd

* copy wallet and macaroon dbs for deterministic wallets/macaroons

* fix perms of shared directories

* allow debian useradd with duplicate id

* add auto-mining

* make bitcoin health check dependent on blockheight

* open channel between ln nodes

* improve channel opens

* add sndev payinvoice

* add sndev withdraw

* ascii art

* add sndev status

* sndev passthrough to docker and containers

* add sndev psql command

* remove script logging

* small script cleanup

* smaller db seed

* pin opensearch version

Co-authored-by: ekzyis <ek@stacker.news>

* pin opensearch dashboard

Co-authored-by: ekzyis <ek@stacker.news>

* add sndev prisma

* add help for all commands

* set -e

* s3 and image proxy with broken name resolution

* finally fully working image uploads

* use a better diff algo

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-03-13 09:04:09 -05:00
keyan
46effa6992 use a better diff algo 2024-03-12 20:04:12 -05:00
keyan
8a96fbb4d0 finally fully working image uploads 2024-03-12 18:51:58 -05:00
keyan
72ecc7b266 s3 and image proxy with broken name resolution 2024-03-12 18:05:28 -05:00
keyan
bb41692acc set -e 2024-03-11 15:30:35 -05:00
keyan
32bc3f18d3 add help for all commands 2024-03-11 15:27:18 -05:00
keyan
1e9d40f152 add sndev prisma 2024-03-11 08:55:02 -05:00
Keyan
b374758ed3
pin opensearch dashboard
Co-authored-by: ekzyis <ek@stacker.news>
2024-03-10 19:44:24 -05:00
Keyan
fc5a4c528b
pin opensearch version
Co-authored-by: ekzyis <ek@stacker.news>
2024-03-10 19:44:02 -05:00
keyan
fa3aed2386 smaller db seed 2024-03-10 19:42:07 -05:00
keyan
09a30a43c4 small script cleanup 2024-03-10 18:41:21 -05:00
keyan
4b2596f86b remove script logging 2024-03-10 18:36:00 -05:00
keyan
5572791c91 add sndev psql command 2024-03-10 18:22:54 -05:00
keyan
a31d04a095 sndev passthrough to docker and containers 2024-03-10 17:30:11 -05:00
keyan
8cddbe74ff add sndev status 2024-03-10 17:10:38 -05:00
keyan
4ce413af6f ascii art 2024-03-10 16:59:15 -05:00
keyan
0500cbd2ca add sndev withdraw 2024-03-10 16:42:16 -05:00
keyan
714aecc477 add sndev payinvoice 2024-03-10 16:29:10 -05:00
keyan
e824261143 improve channel opens 2024-03-10 13:29:21 -05:00
keyan
c88ba74268 open channel between ln nodes 2024-03-09 23:47:32 -06:00
keyan
c63622b8a4 make bitcoin health check dependent on blockheight 2024-03-09 17:56:07 -06:00
keyan
c32782583f add auto-mining 2024-03-09 15:40:00 -06:00
keyan
bc85e4b3c5 allow debian useradd with duplicate id 2024-03-09 12:15:36 -06:00
keyan
eb7670e5c3 fix perms of shared directories 2024-03-08 20:07:43 -06:00
keyan
0e02aa7d8d copy wallet and macaroon dbs for deterministic wallets/macaroons 2024-03-08 18:40:10 -06:00
keyan
215f330771 reorganize docker and add static certs/macroon to lnd 2024-03-08 13:11:58 -06:00
keyan
7fe959a720 add changes to .env.sample 2024-03-07 21:29:13 -06:00
keyan
85c1303185 begin adding regtest network 2024-03-07 21:24:42 -06:00
keyan
1b275517fd refinements after fresh builds 2024-03-07 16:40:26 -06:00
Keyan
265f92af35
Merge branch 'master' into localdev 2024-03-07 15:24:41 -06:00
keyan
af2890075c add db seed to sndev 2024-03-07 15:19:49 -06:00
keyan
b3bf3dba28 proper check for postgres health 2024-03-07 13:18:23 -06:00
keyan
f2fd2eb1d7 restart: always -> unless-stopped 2024-03-07 10:19:37 -06:00
keyan
fab750352f make posix shell 2024-03-07 10:02:59 -06:00
ekzyis
179a539d4d
Parse numeric strings as numbers (#902)
* Parse numeric strings as numbers

* Additionally check for type of field value
2024-03-06 19:45:00 -06:00
keyan
51dba02569 sndev bash script and docker reliability stuff 2024-03-06 19:04:55 -06:00
keyan
0614cfe979 refine docker config 2024-03-06 17:50:01 -06:00
keyan
a3428d1edc add hot reloading worker:dev script 2024-03-06 16:50:48 -06:00
keyan
b38a5e653c make image aspect ratio a fraction 2024-03-06 15:15:11 -06:00
keyan
575a820a7a fix broken merge 2024-03-06 14:35:45 -06:00
keyan
6a6f4a88aa refine bounty icon margin for wrapping in comments 2024-03-06 14:20:03 -06:00
JP Melanson
ecf859ee4c
Change poll icon color when active (#680) (#898) 2024-03-06 14:02:48 -06:00
Keyan
2fc1ef44dd
Fix image rerender jitter and layout shift (#896)
* fix image jitter and layout shift

* prevent unecessary context rerenders
2024-03-06 13:53:46 -06:00
Keyan
48aef15a07
use keyset pagination for notifications (#899) 2024-03-06 13:53:13 -06:00
ekzyis
8d49c034c6
Fix alignment of info icon (#895) 2024-03-05 16:27:29 -06:00
ekzyis
b379e7467f
Territory transfers (#878)
* Allow founders to transfer territories

* Log territory transfers in new AuditLog table

* Add territory transfer notifications

* Use polymorphic AuditEvent table

* Add setting for territory transfer notifications

* Add push notification

* Rename label from user to stacker

* More space between cancel and confirm button

* Remove AuditEvent table

The audit table is not necessary for territory transfers and only adds complexity and unrelated discussion to this PR.

Thinking about a future-proof schema for territory transfers and how/what to audit at the same time made my head spin.

Some thoughts I had:

1. Maybe using polymorphism for an audit log / audit events is not a good idea

Using polymorphism as is currently used in the code base (user wallets) means that every generic event must map to exactly one specialized event.

Is this a good requirement/assumption? It already didn't work well for naive auditing of territory transfers since we want events to be indexable by user (no array column) so every event needs to point to a single user but a territory transfer involves multiple users.

This made me wonder: Do we even need a table? Maybe the audit log for a user can be implemented using a view? This would also mean no data denormalization.

2. What to audit and how and why?

Most actions are already tracked in some way by necessity: zaps, items, mutes, payments, ...

In that case: what is the benefit of tracking these things individually in a separate table?

Denormalize simply for convenience or performance? Why no view (see previous point)? Use case needs to be more clearly defined before speccing out a schema.

* Fix territory transfer notification id conflict

* Use include instead of two separate queries

* Drop territory transfer setting

* Remove trigger usage

* Prevent transfers to yourself
2024-03-05 13:56:02 -06:00
keyan
6573ce666b update register stacker number 2024-03-04 21:00:43 -06:00
keyan
2d20d1a8aa new email welcome gif 2024-03-04 21:00:28 -06:00
keyan
b5de515f5e render outlawed images and links as text 2024-03-04 19:29:50 -06:00
keyan
b16234630b better link rel attr handling 2024-03-04 19:20:14 -06:00
keyan
0a0bfbbb37 replace welcome banner with msm banner 2024-03-04 16:51:36 -06:00
keyan
fd7e3b04f9 make tabindex camel cased 2024-03-04 15:38:56 -06:00
keyan
f305013aaf fix back button to rewards again 2024-03-02 11:48:11 -06:00
JP Melanson
fb1281dfd2
Trigger image upload from keyboard (#891) (#893)
* Adding tabindex to ImageUpload div wrapper in order to make it selectable
* Adding keyboard event handler to listen to Enter key stroke and trigger file upload selection

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-03-01 18:42:47 -06:00
keyan
aa7c233177 fix bolt color hover on mobile 2024-03-01 18:32:40 -06:00
keyan
71619dfa2c fix back button to rewards page 2024-03-01 18:21:52 -06:00
Keyan
0b0e36e3cb
Monthly rewards (#890)
* show placeholder for hidden stackers in top

* top rewardability views

* make territory revenue idependent job

* monthly rewards and leaderboard on rewards pages

* fix earn reschedule

* add query for rewards leaderboard

* reduce likelihood of rewards racing with views

* fix earn and refine values views
2024-03-01 10:28:55 -06:00
ekzyis
508008f586
Fix Sub.meSubscription not resolved on page load (#888) 2024-02-28 09:31:31 -06:00
ekzyis
1c488b13df
Drop noteTerritoryPosts column (#887) 2024-02-28 09:16:20 -06:00
ekzyis
89de8a9907
Fix out of order undos for turbo zaps (#883)
Turbo zaps had different toast bodies so they weren't merged together. This gave stackers the option to undo these zaps out of order.

When zaps are undone out of order, the client cache can get in a bad state. Using the item id as a tag fixes that such that zaps for the same item will always get merged together.

This can be seen as a workaround for hacky zap undo code but I think it's also better UX so maybe we should do this anyway.
2024-02-26 18:10:43 -06:00
ekzyis
bbdd969394
Fix missing progress bar for custom zaps (#882) 2024-02-26 18:09:29 -06:00
ekzyis
38f2aa309d
Fix bolt hover ignores turbozaps (#881)
* Fix bolt hover color ignores turbo zaps

* Refactor next tip code into own function
2024-02-26 18:09:09 -06:00
ekzyis
9cb657ab9a
Fix territory context menu visible for anon (#877) 2024-02-25 10:21:10 -06:00
ekzyis
1ea28b748c
Drop unused function signature of create_item (#876)
We only use create_item at a single location. It uses the function signature with integer[] at the end.
2024-02-25 10:20:17 -06:00
ekzyis
5f602e24fa
Fix conflict on duplicate image in post (#875)
The create_item function was missing ON CONFLICT DO NOTHING for insertions into the ItemUpload table as in update_item.

This means that if the same image was used multiple times in the same item, creation failed. But it worked during editing.
2024-02-25 10:19:48 -06:00
ekzyis
8cd147f67f
Insert image at cursor position (#874) 2024-02-25 10:18:07 -06:00
ekzyis
a067a9fcf1
Use progress bar for pending payments (#873)
The progress bar indicates when the invoice will expire.

This works by passing in a timeout to the withToastFlow wrapper.

If timeout is set, progressBar option will be true for the toast and delay will be set to the timeout.

If progressBar is set, the progress bar will use the delay for its duration.
2024-02-24 14:33:08 -06:00
ekzyis
817234a7fa
More zap undo fixes (#872)
* Don't throw error if invoice attached

* Only show progress bar for undo toasts

* Update zap undo info in settings

* Skip zap undo toast flow for external payments
2024-02-24 11:38:40 -06:00
keyan
cd4f243106 fix missing field in schema 2024-02-23 09:35:20 -06:00
ekzyis
d987069fae
Fix toast progress bar desync (#871)
* Fix toast progress bar desync

If a toast gets rendered again with the same animation-delay, the animation-delay seems to get added.

This commit fixes that by ensuring that animation-delay is only set if the toast was not rendered before.

* Fix comment
2024-02-23 09:14:51 -06:00
ekzyis
fa4f09ddca
Territory notifications for everyone (#870)
* Territory notifications

* Migrate old setting to new table

* Auto subscribe founders to their territories on creation

* Fix (un)subscribe not shown to founder

* Rename to toggleSubSubscription

* Fix inconsistency between toggleSubSubscription and toggleMuteSub

* Add dedicated button in header for following territories

* Don't drop noteTerritoryPosts column

* Fix db dip in Sub.meSubscription resolver

* Move territory subscribe to new territory context menu

* Decrease space between share icon and mute button

* Fix eslint
2024-02-23 09:12:49 -06:00
keyan
96ff26f26e include comments in territory revenue 2024-02-22 15:32:42 -06:00
keyan
b42a4eabab reward everything again 2024-02-22 15:31:52 -06:00
mzivil
b0bf7add34
Show founded territories on profile (#868)
* add nterritories field to User

* add userSubs query

* show territories tab on user profiles

hide the tab if user has 0 territories, except when the
viewer navigated directly to the user's territories page

* add USER_WITH_SUBS query for user territories page

* add user territories page
2024-02-21 19:55:48 -06:00
ekzyis
6f8b6c36d8
Remove console.log (#869) 2024-02-21 19:34:51 -06:00
Austin Kelsay
565e939245
Nostr crossposting all item types (#779)
* crosspost-item

* crosspost old items, update with nEventId

* Updating noteId encoding, cleaning up a little

* Fixing item-info condition, cleaning up

* Linting

* Add createdAt variable back

* Change instances of eventId to noteId

* Adding upsertNoteId mutation

* Cleaning up updateItem, using toasts to communivate success/failure in crosspost-item

* Linting

* Move crosspost to share button, make sure only OP can crosspost

* Lint

* Simplify conditions

* user might have no nostr extension installed

Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com>

* change upsertNoteId to updateNoteID for resolver and mutations, change isOp to mine, remove unused noteId params

* Basic setup for crossposting poll / link items

* post rebase fixes and Bounty and job crossposts

* Job crossposting working

* adding back accidentally removed import

* Lint / rebase

* Outsource as much crossposting logic from discussion-form into use-crossposter as possible

* Fix incorrect property for user relays, fix itemId param in updateNoteId

* Fix toast messages / error cases in use-crossposter

* Update item forms to for updated use-crossposter hook

* CrosspostDropdownItem in share updated to accomodate use-crossposter update

* Encode paramaterized replacable event id's in naddress format with nostr-tools, bounty to follw nip-99 spec

* Increase timeout on relay connection / cleaning up

* No longer crossposting job

* Add blastr, fix crosspost button in item-info for polls/discussions, finish removing job crosspostr code

* Fix toaster error, create reusable crossposterror function to surface toaster

* Cleaning up / comments / linting

* Update copy

* Simplify CrosspostdropdownItem, keep replies from being crossposted

* Moved query for missing item fields when crossposting to use-crossposter hook

* Remove unneeded param in CrosspostDropdownItem, lint

* Small fixes post rebase

* Remove unused import

* fix nostr-tools version, fix package-lock.json

* Update components/item-info.js

Co-authored-by: ekzyis <ek@stacker.news>

* Remove unused param, determine poll item type from pollCost field, add mutiny strfry relay to defaults

* Update toaster implementations, use no-cache for item query, restructure crosspostItem to use await with try catch

* crosspost info modal that lives under adv-post-form now has dynamic crossposting info

* Move determineItemType into handleEventCreation, mover item/event handing outside of do ... while loop

* Lint

* Reconcile skip method with onCancel function in toaster

* Handle failedRelays being undefined

* determine item type from router.query.type if available otherwise use item fields

* Initiliaze failerRelays as undefined but handle error explicitly

* Lint

* Fix crosspost default value for link, poll, bounty forms

---------

Co-authored-by: ekzyis <27162016+ekzyis@users.noreply.github.com>
Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-02-21 19:18:36 -06:00
ekzyis
c57fcd6518
Allow zap undo's for short period of time (#857)
* Cancel zaps

* Hide zap error toast

* Immediately throw error about insufficient funds

* Optimistic UX

* Also hide success zap toast

* Show undo instead of cancel

* Include sat amount in toast

* Fix undo toasts removed on navigation

* Add setting for zap undos

* Add undo to custom zaps

* Use WithUndos suffix

* Fix toast flow transition

* Fix setting not respected

* Skip undo flow if funds insufficient

* Remove brackets around undo

* Fix insufficient funds detection

* Fix downzap undo

* Add progress bar to toasts

* Use 'button' instead of 'notification' in zap undo info

* Remove console.log

* Fix toast progress bar restarts

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-02-21 18:48:42 -06:00
mzivil
46a0af19eb
Make poll expiration configurable (#860)
* add poll expires at column to Item table

* update upsertPoll mutation for pollExpiresAt param

* use pollExpiresAt to show time left for poll

* correctly pluralize days for timeLeft

* correctly update pollExpiresAt when item is updated to remove poll expiration

* add DateTimePicker and DateTimeInput components to select datetimes

* update pollExpiresAt to be nullable and more than 1 day in the future

* hide time left text if poll has no expiration

* initialize pollExpiresAt with current value or default of 25 hours in the future

we add a one hour time buffer so that the user doesn't get a validation error
for pollExpiresAt if they post their poll within an hour from creation. there's
still a chance they'll hit the validation error but they should see the error
message toast

* add DateTimeInput into the options part of the poll form

add right padding to make room for the "clear" button.

allow field to be cleared (i.e. null pollExpiresAt) to allow
non-ending polls.

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-02-21 12:18:43 -06:00
ekzyis
843471e5dc
Fill bolts on hover with next zap color (#867) 2024-02-20 17:46:27 -06:00
ekzyis
5de014cba8
Toast flows (#856)
* Use toast flows

"Toast flows" are a group of toasts that should be shown after each other.

Before this commit, they were implemented by manually removing previous toasts in the same flow.

Now a flowId can be passed and ToastProvider will make sure that there always only exists one toast with the same flowId.

This is different to toast tags since tags don't replace toasts with the same tag, they only are shown "above" them.

* Create wrapper for toast flows
2024-02-19 19:03:30 -06:00
keyan
fe0d960208 handle other possible base64-like string encodings 2024-02-18 15:08:55 -06:00
keyan
a9ade0354d wip 2024-02-18 15:08:55 -06:00
ekzyis
a7018e25c6
Refactor removeToast (#854) 2024-02-18 13:33:32 -06:00
benthecarman
2d5b1f090d
Handle space separated NWC info event (#855) 2024-02-18 13:29:53 -06:00
ekzyis
6e6c355a3f
Add tests for internal linking (#808)
* Add tests for internal linking

* Add workflow for unit tests

* Use jest
2024-02-17 15:53:36 -06:00
ekzyis
81ab960d92
Fix NWC support detection (#845)
* Fix NWC support detection

* Fix missing toast if support for pay_invoice not detected
2024-02-17 10:30:13 -06:00
keyan
7065008f5d remove overspecified condition 2024-02-16 13:26:15 -06:00
keyan
0d549abff9 fix erronous reference to me in user profile 2024-02-16 13:15:00 -06:00
keyan
3106850ce9 fix reference to old materialized view 2024-02-16 12:58:50 -06:00
ekzyis
03b1b173ad
Rename HODL to JIT in frontend comments (#843) 2024-02-16 12:27:15 -06:00
ekzyis
3d1bcd38c6
Fix onSubmit not ignoring payment cancels from WebLN payments (#842) 2024-02-16 12:26:43 -06:00
ekzyis
5c56267aaa
Changed my nym to ek (#844) 2024-02-16 12:25:43 -06:00
Keyan
798fab097d
Make territory billing period changeable (#840)
* allow updates to territory billing

* simplify prorating

* handle updates during grace period and rehydrating archive
2024-02-16 12:25:12 -06:00
ekzyis
cfd762a5b6
Fix toast autohide (#839) 2024-02-15 16:49:54 -06:00
keyan
b5bf0c88e3 fix sub apollo cache merge errors 2024-02-15 11:32:09 -06:00
ekzyis
afe096e516
Fix QR code interaction with WebLN provider (#834)
* Fix passing of bolt11 for QR payments

* Fix missing provider check

* Only cancel invoice if hash and hmac were given

* Fix duplicate toast on error

* Fix relay might not be set yet when sendPayment is called
2024-02-15 11:20:15 -06:00
mzivil
8727f95fd9
remove filter for ItemAct where act is TIP (#835)
The spent and stacked calculations are showing the same number because
we only select ItemActs with act = 'TIP'.

The fix is to remove the `act` filter
2024-02-15 09:43:38 -06:00
keyan
57917d47a2 fix lnaddr autowithdraw 2024-02-15 08:59:45 -06:00
Dillon
35d212573e
auto canceling bolt11s from lnd when auto dropped from DB (#793)
* auto canceling bolt11s from lnd when auto dropped from DB

* auto canceling bolt11s from lnd when auto dropped from DB

* removed semicolon for lint

* changed cancleHodlInvoic to deletePayment function

* updated code to account for failed LND deletes

* linter fixes

* updated to only remove hashes and bolt11's from model when successfully deleted from LND

* updated to revert unsuccessful deletes from LND and add those values back into the db

* linter fix and renaming for clarity

* updated WITH query

* added if statement to account for invoices not returning from db

* fixed linter

* reverted docker-compose.yml

* made it dry

* made it dry

* added a comment because the query might be confusing

* made query moar dry

* Query formatting

* Fix query returns number of rows instead of rows

* updated  to

* fixed linter

* removed lnbits dir

* removed gitignore and docker-compose.yml from pr

* added deleting from LND in wallet resolver

* linter + added missing import

* fixed merge conflict

* refine invoice deletion

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-02-14 17:31:25 -06:00
ekzyis
1444ff476e
Validate pubkey, relay URL and secret of NWC URL (#810)
* Validate pubkey, relay URL and secret of NWC URL

* Fix NWC secret regexp

* Use sequential validation in Yup schema

* Add note about possible mismatch between hostnames and pubkeys

* Remove unused param
2024-02-14 15:09:13 -06:00
keyan
bbbd5060d0 add numbering to top stacker and territories 2024-02-14 15:06:42 -06:00
mzivil
f59ee5df17
Add ranked territories to 'top' page (#828)
* add subViewGroup function to create view to read sub stats from

* add topSubs resolver to graphql query

* add TOP_SUBS query fragment

* add SUB_SORTS for top territory sorting

* add custom cache policy for topSubs

* add territories to top header select

* add top territories page

* add db views for sub stats

* configure sub_stats views to refresh by worker

* filter rows with empty subName

* update msats_spent calculation to include all ItemAct in sub

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-02-14 14:27:00 -06:00
Alex Lewin
b3498fe277
Add Opt-in to Display Linked Accounts in Profile (#826)
* Add display linked accounts to settings

* Apply suggestions from code review

Co-authored-by: ekzyis <ek@stacker.news>

* small styling enhancements

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-02-14 13:33:31 -06:00
keyan
5c3c7fb185 allow more restricted macroons fix #827 2024-02-14 12:58:25 -06:00
ekzyis
0c3be0cd08
Fix saloon comments hidden in profile (#831) 2024-02-14 11:58:29 -06:00
keyan
e57671ba9b dont imply required websocket fix #830 2024-02-14 11:55:08 -06:00
ekzyis
2587483bbe
Merge pull request #830 from stackernews/fix-settings-relay-required
Fix settings save requires relay set
2024-02-14 18:33:48 +01:00
ekzyis
5eeaf671f8 Fix settings save requires relay set 2024-02-14 18:27:35 +01:00
keyan
2b851edb02 remove debug comment 2024-02-14 09:17:21 -06:00
keyan
2ce2580e8e fix posts not showing up in bookmarks 2024-02-14 09:12:00 -06:00
ekzyis
30bc3b612a
Fix comment (unsafe-eval isn't used in prod) (#825) 2024-02-14 08:45:00 -06:00
keyan
04991b4ddf try url object with next/link to fix #822 2024-02-13 19:44:03 -06:00
ekzyis
2bbcbfbb26
Force SSR to include CSP nonces on page reload (#818) 2024-02-13 16:40:26 -06:00
keyan
a6aebd7004 fix comment bookmarks 2024-02-13 16:22:36 -06:00
ekzyis
bff9342272
Allow blob: scheme (#817) 2024-02-13 16:11:34 -06:00
ekzyis
d6465162bd
Fix missing setInitialized (#815)
Payment methods were not marked as initialized if the local storage item did not exist on page load.
2024-02-13 14:30:54 -06:00
Keyan
ec4e1b5da7
LND autowithdraw (#806)
* wip

* wip

* improved validatation, test connection before save, code reuse

* worker send to lnd

* autowithdraw priority
2024-02-13 13:17:56 -06:00
ekzyis
fc18a917e3
Add Content Security Policy headers (#805)
* Basic CSP with unsafe-inline, unsafe-eval

* Allow 'self' for img-src and connect-src

Apparently, there is a bug for Chrome on iOS if connect-src does not allow 'self'.

See known issues at https://caniuse.com/contentsecuritypolicy

* Use nonces for strict CSP

* More CSP comments

* Add frame-ancestors directive

* Add more useful headers

* Add HSTS header

* Allow youtube and twitter embeds

For some reason, www.youtube.com is enough. It also works for youtube.com and youtube-nocookie.com.

For twitter embeds from twitter.com or x.com, platform.twitter.com is enough.

* Allow CDN and media domain in CSP

* Only allow unsafe-eval in dev build

* Ignore _next/webpack-hmr in middleware
2024-02-13 13:10:06 -06:00
ekzyis
a4e84e7a2e
Fix local prod builds assuming CDN (#814)
* Fix local prod builds assuming CDN

Prod builds assumed that we're running in an AWS environment and use a CDN. This commit changes that.

Now, if the AWS way to fetch the commit failed _and_ the normal git command fails and only if, we assume we're running the prod build locally and don't configure the CDN.

* Fix path to app_version_manifest.json

Was autoformatted by linter. Probably before I added eslint-next-disable-line above.
2024-02-13 09:53:34 -06:00
ekzyis
894a73d713
Show item page in internal links (#807)
* Include item page in link text

* Fix invalid URLs parsed
2024-02-12 13:34:33 -06:00
ekzyis
8238d4d5be
Enforce HTTPS for LNbits (#809)
* Enforce HTTPS for LNbits

* Use URL constructor
2024-02-11 17:39:06 -06:00
mzivil
6355d7eabc
Add nsfw setting to territories (#788)
* add nsfw column to sub

* add nsfw boolean to territorySchema

* save nsfw value in upsertSub mutation

* return nsfw value from Sub query for correct value in edit territory form

* add nsfw checkbox to territory form

* add nsfw badge to territory header

* add nsfwMode to user

* show nsfw badge next to item territory

* exclude nsfw sub from items query

* show nsfw mode checkbox on settings page

* fix nsfw badge formatting

* separate user from current, signed in user

* update relationClause to join with sub table

* refactor to simplify hide nsfw sql

* filter nsfw items when viewing user items

* hide nsfw posts for logged out users

* filter nsfw subs based on user preference

* show nsfw sub name if logged out user is viewing the page

* show current sub at the top of the list instead of bottom

* always join item with sub to check nsfw

* check for sub presence before showing nsfw badge on item

* skip manually adding sub to select if sub is null

* fix relationClause to join with root item

* move moderation and nsfw into accordion

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-02-09 20:35:32 -06:00
ekzyis
b3d485e8c4
Refactor default payment method setting (#803)
* Refactor setting of default providers

* fixed warning about component update while rendering another component
* individual providers no longer need to know if they are the default or not
* default setting is now handled by WebLNContext -- the same context that returns the provider. this makes a lot more sense and is a lot easier to read
* default payment checkbox is now also disabled if there is only one enabled provider or if it is the default provider

* Fix order lost on page reload

On page reload, the providers were synced in the order they were loaded.

This means that the default payment provider setting was lost.

Fixed this by syncing order to local storage and on page reload, only syncing providers when they were initialized (else the order would have been lost again).
2024-02-09 09:42:26 -06:00
ekzyis
ec3e8f0079
Remove deprecated comment (#801) 2024-02-09 09:39:59 -06:00
ekzyis
b6dd4c1dba
Update image fees to 50 MB free per day (#798)
Almost every stacker was below 50 MB per day (except one stacker at one day).

Since storage is cheap, we can allow 50 MB per day for free; especially since UX around image fees suck.
2024-02-08 19:06:20 -06:00
ekzyis
310011f05d
Expose WebLN interface via React Context (#749)
* Add LNbits card

* Save LNbits Provider in WebLN context

* Check LNbits connection on save

* refactor: put LNbitsProvider into own file

* Pay invoices using WebLN provider from context

* Remove deprecated FIXME

* Try WebLN provider first

* Fix unhandled promise rejection

* Fix this in sendPayment

* Be optimistic regarding WebLN zaps

This wraps the WebLN payment promise with Apollo cache updates.

We will be optimistics and assume that the payment will succeed and update the cache accordingly.

When we notice that the payment failed, we undo this update.

* Bold strike on WebLN zap

If lightning strike animation is disabled, toaster will be used.

* Rename undo variable to amount

* Fix zap undo

* Add NWC card

* Attempt to check NWC connection using info event

* Fix NaN on zap

Third argument of update is reserved for context

* Fix TypeError in catch of QR code

* Add basic NWC payments

* Wrap LNbits getInfo with try/catch

* EOSE is enough to check NWC connection

* refactor: Wrap WebLN providers into own context

I should have done this earlier

* Show red indicator on error

* Fix useEffect return value

* Fix wrong usage of pubkey

The event pubkey is derived from the secret. Doesn't make sense to manually set it. It's also the wrong pubkey: we're not the wallet service.

* Use p tag in NWC request

* Add comment about required filter field

* Aesthetic changes to NWC sendPayment

* Add TODO about receipt verification

* Fix WebLN attempted again after error

* Fix undefined name

* Add code to mock NWC relay

* Revert "Bold strike on WebLN zap"

This reverts commit a9eb27daec0cd2ef30b56294b05e0056fb5b4184.

* Fix update undo

* Fix lightning strike before payment

* WIP: Wrap WebLN payments with toasts

* add toasts for pending, error, success
* while pending, invoice can be canceled
* there are still some race conditions between payiny the invoice / error on payment and invoice cancellation

* Fix invoice poll using stale value from cache

* Remove unnecessary if

* Make sure that pay_invoice is declared as supported

* Check if WebLN provider is enabled before calling sendPayment

* Fix bad retry

If WebLN payments failed due to insufficient balances, the promise resolved and thus the action was retried but failed immediately since the invoice (still) wasn't paid.

* Fix cache undo update

* Fix no cache update after QR payment

* refactor: Use fragments to undo cache updates

* Remove console.log

* Small changes to NWC relay mocking

* Return SendPaymentResponse

See https://www.webln.guide/building-lightning-apps/webln-reference/webln.sendpayment

* Also undo cache update on retry failure

* Disable NWC mocking

* Fix initialValue not set

But following warning is now shown in console:

"""
Warning: A component is changing a controlled input to be uncontrolled.
This is likely caused by the value changing from a defined to undefined, which should not happen.
Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components
"""

* Remove comment since only relevant for blastr (mutiny relay)

* Remove TODO

* Fix duplicate cache update

* Fix QR modal not closed after payment

* Ignore lnbits variable unused

* Use single relay connection for all NWC events

* Fix missing timer and subscription cleanup

* Remove TODO

Confirmed that nostr-tools verifies events and filters for us.

See https://github.com/nbd-wtf/nostr-tools/blob/master/abstract-relay.ts#L161

* Fix switch from controlled to uncontrolled input

* Show 'configure' on error

* Use budgetable instead of async

* Remove EOSE listener

Only nostr.mutinywallet.com didn't respond with info events due to implementation-specific reasons. This is no longer the case.

* Use invoice expiry for NWC timeout

I don't think there was a specific reason why I used 60 seconds initially.

* Validate LNbits config on save

* Validate NWC config on save

* Also show unattach if configuration is invalid

If unattach is only shown if configuration is valid, resetting the configuration is not possible while it's invalid. So we're stuck with a red wallet indicator.

* Fix detection of WebLN payment

It depended on a Apollo cache update function being available. But that is not the case for every WebLN payment.

* Fix formik bag lost

* Use payment instead of zap in toast

* autoscale capture svc by response time

* docs and changes for testing lnbits locally

* Rename configJSON to config

Naming of config object was inconsistent with saveConfig function which was annoying.

Also fixed other inconsistencies between LNbits and NWC provider.

* Allow setting of default payment provider

* Update TODO comment about provider priority

The list 'paymentMethods' is not used yet but is already implemented for future iterations.

* Add wallet security disclaimer

* Update labels

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-02-08 12:33:13 -06:00
keyan
6adb57c8ec autoscale capture svc by response time 2024-02-07 19:34:33 -06:00
Noah
02278c6073
Improved ux for domain only searches (#782)
* improves ux for url only searches

* updates with sn nym

* add back original implementation when query has more than url: filter

* eliminates use of wildcards

* adds docs for testing search in a way that more closely resembles prod

* fixes lint issues

---------

Co-authored-by: utanapishtim <utnapishtim.utanapishtim@gmail.com>
2024-02-07 18:45:11 -06:00
Keyan
65cc1dbcc0
Merge pull request #785 from stackernews/773-fix-pins-prevent-post-show-up-in-feeds
Fix pinned items don't show up in home
2024-02-04 16:46:49 -06:00
ekzyis
46eeb729c3 Fix pinned items don't show up in home 2024-02-04 22:15:18 +01:00
Keyan
42e491b59d
Merge pull request #784 from mzivil/fix-territory-form-labels
Fix territory form label clicks not toggling correct input
2024-02-04 10:11:54 -06:00
keyan
1135fff77c refine capture svc 2024-02-03 20:14:54 -06:00
keyan
05d866883a make capture svc a little more robust 2024-02-03 17:49:34 -06:00
mzivil
f2f39f4c22 fix clicks on billing types label not toggling correct radio
Currently, all the billing types radios are being assigned the
same "billingType" id, so clicking on any of the labels
always selects the monthly one. If you inspect the HTML, all the
billing type labels have 'for="billingType"' which is how the HTML
knows which input to select.

We have to keep the "name" attribute the same because that's how
the input values are linked to the billingType form field.

To fix, we explicitly assign the "id" prop for each radio so
that the <label>'s "for" attribute is tied to the correct
radio input.
2024-02-03 16:59:37 -05:00
mzivil
3328c1daa3 fix clicks on post types label not toggling correct checkbox
Currently, all the post types checkbox are being assigned the
same "postTypes" id, so clicking on any of the post type labels
always toggles the first one. If you inspect the HTML, all the
post type labels have 'for="postTypes"' which is how the HTML
knows which checkbox to toggle.

We have to keep the "name" attribute the same because that's how
the checkbox values are linked to the postTypes field.

To fix, we explicitly assign the id prop for each checkbox so
that the <label>'s "for" attribute is tied to the correct
checkbox input.
2024-02-03 16:59:37 -05:00
Keyan
4789a93778
Merge pull request #783 from stackernews/fixpins
allow pins to be zapped but not from pin position
2024-02-03 15:33:41 -06:00
keyan
c23f1f82bc allow pins to be zapped but not from pin position 2024-02-03 15:27:36 -06:00
Keyan
cb5c12b82d
Merge pull request #781 from mzivil/fix-hn-and-bitcointalk-dupes
Fix hacker news and bitcointalk dupes
2024-02-02 15:16:15 -06:00
mzivil
986ba582e5 fix lint errors 2024-02-02 16:06:33 -05:00
mzivil
7ff02ebe30 rename mutated hostname and pathname variables to avoid confusion 2024-02-02 15:57:34 -05:00
mzivil
db7c4c3d76 rename uri to uriRegex to avoid confusion 2024-02-02 15:50:18 -05:00
mzivil
99e547e6ae fix all hacker news and bitcoin talk links showing up as dupes
It looks like a regression was introduced at some point, because
the `uri` that's compared against the `whitelist` is a regular
expression and not the url hostname + pathname as it was originally
written.

This brings back the original behavior of comparing the whitelist
against the hostname + pathname
2024-02-02 15:46:30 -05:00
mzivil
068f1e9eba use stripTrailingSlash for uriRegex in dupes 2024-02-02 15:45:49 -05:00
mzivil
a039f29cdf add stripTrailingSlash utility function 2024-02-02 15:44:37 -05:00
Keyan
bbfb008d5f
Merge pull request #780 from mzivil/fix-yewtube-dupes
Fix yewtube links showing up as dupes of each other
2024-02-02 13:48:54 -06:00
mzivil
d861890d35 add sn nym to contributors 2024-02-02 14:19:23 -05:00
mzivil
4076727ed3 fix yewtu.be links showing up as dupes of each other 2024-02-02 12:43:24 -05:00
Keyan
b2ba333905
Merge pull request #777 from stackernews/toast-tags
Add tags and onCancel to toasts
2024-02-01 11:10:10 -06:00
Keyan
dbde163c74
Merge pull request #778 from stackernews/faster-expiry-after-payment
Finalize hodl invoices after payment within 60 seconds
2024-02-01 11:04:02 -06:00
ekzyis
730158fd5c Finalize hodl invoices after payment within 60 seconds 2024-02-01 17:28:06 +01:00
ekzyis
1fa129272a Use toast body as default tag 2024-02-01 16:17:54 +01:00
ekzyis
878d661154 Add tag and cancel support to toasts 2024-02-01 16:17:54 +01:00
Keyan
50c4a9c8e6
Merge pull request #775 from benthecarman/rm-desc-hash-check
Remove description hash check for lnurl
2024-01-31 09:21:48 -06:00
benthecarman
e4c1c9bade
Remove description hash check for lnurl 2024-01-31 12:21:26 +00:00
keyan
7c67c4049e run npm install 2024-01-30 20:08:48 -06:00
keyan
a62cdd288f upgrade puppeteer in capture microservice + add emojis 2024-01-30 19:53:53 -06:00
keyan
565950c875 install psql on servers 2024-01-30 18:47:01 -06:00
keyan
3cc1f23902 remove no longer needed chromium install 2024-01-30 18:43:03 -06:00
keyan
38e42255dd remove no longer needed puppeteer config 2024-01-30 18:41:26 -06:00
Keyan
5b376bf5b9
Merge pull request #774 from stackernews/microservices
Microservices
2024-01-30 18:38:59 -06:00
keyan
60a02f4c8a npm audit fix 2024-01-30 18:28:13 -06:00
keyan
31cdf572fb add microservice definitions to repo 2024-01-30 18:23:46 -06:00
keyan
61c64646b5 use capture microservice 2024-01-30 18:22:40 -06:00
ekzyis
d1ed72bb85
Allow territory founders to pin items (#767)
* Add pinning of items

* Fix empty section in context menu

* Pin comments

* Fix layout shift during comment pinning

* Add comments, rename, formatting

* Max 3 pins allowed

* Fix argument

* Fix missing position update for other items

* Improve error message

* only show saloon in home

* refine pinItem style and transaction usage

* pin styling enhancements

* simpler handling of excess pins

* fix pin positioning like mergePins

* give existing pins null subName

* prevent empty items on load

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-01-30 11:04:56 -06:00
keyan
35c76c077e improved footnote styling and list styling 2024-01-28 13:26:32 -06:00
keyan
2338be774d better quoteblock styling 2024-01-28 13:26:32 -06:00
keyan
6179d14a68 use greedy connection from view refreshes 2024-01-28 13:26:32 -06:00
keyan
5086c2036d add ids to all toasts to help id rogue toasts 2024-01-28 13:26:32 -06:00
keyan
d8f4071afb refine serialization retries 2024-01-28 13:26:32 -06:00
ekzyis
4bc0a930b3
Fix image deletion batch size (#769) 2024-01-28 12:29:56 -06:00
ekzyis
e4aff5d11b
Format internal links as #<id> (#765)
* Format internal links

* Fix URL text ignored
2024-01-24 17:29:52 -06:00
ekzyis
bd23c91fda
Fix typo in FILTER for posts in user stats (#766) 2024-01-23 18:22:34 -06:00
keyan
a5d7867a13 make sure to check only pending withdrawals 2024-01-21 13:25:10 -06:00
keyan
a3842a9af3 newsletter script meme monday search fix 2024-01-21 13:25:10 -06:00
ekzyis
30850acd8f
Fix stale cursor used on reconnect (#764) 2024-01-20 16:57:52 -06:00
keyan
dc8d35fdcf only open related posts when there aren't comments 2024-01-20 15:17:34 -06:00
keyan
70aa7dd1ad fix single date in datepicker 2024-01-19 20:38:35 -06:00
keyan
e9a78e2d07 fix spending growth view 2024-01-19 16:40:29 -06:00
keyan
968263ba6a have datepicker store unix timestamps 2024-01-19 15:30:36 -06:00
keyan
eb99fcef9e improve stat gathering 2024-01-19 15:19:26 -06:00
keyan
0134968fc1 don't trigger date picker onchange until 2 dates are picked 2024-01-19 15:14:52 -06:00
keyan
c41ad5d469 fix for 404 on empty search results 2024-01-19 15:12:47 -06:00
keyan
64e49934d4 disconnect greedy rewards connection after they run 2024-01-19 15:11:44 -06:00
keyan
d14123cc42 improved comment linking 2024-01-17 19:03:49 -06:00
ekzyis
2ceada66d6
Remove plausible link (#762) 2024-01-17 18:11:00 -06:00
keyan
61a66127d1 search/related posts refinements 2024-01-17 17:39:48 -06:00
Ben Allen
f915b2b8b3
add support for scanning bip21 qr (#755) 2024-01-17 15:17:18 -06:00
keyan
c58d3c778a search filter help text for exact phrase 2024-01-17 09:28:05 -06:00
keyan
fe5991112e support query quoting for exact matches 2024-01-16 20:01:06 -06:00
ekzyis
9ef0c81245
refactor: replace recursion with promise sequence in useInvoiceable (#752)
* refactor: replace recursion with promise sequence

This commit refactors `useInvoicable`. The hard-to-follow recursion was replaced by awaiting promises which resolve or reject when one step of our JIT invoice flow is done.

Therefore, `onSubmit` is now fully agnostic of JIT invoices. The handler only returns when payment + action was successful or canceled - just like when a custodial zap was successful.

* refactor more and fix bugs

* move invoice cancel logic into hook where invoice is also created
* fix missing invoice cancellation if user closes modal or goes back.
* refactor promise logic: it makes more sense to wrap the payment promise with the modal promise than the other way around.

* Fix unhandled rejection

* Fix unnecessary prop drilling

* Fix modal not closed after successful action

* Fix unnecessary async promise executor

* Use function to set state
2024-01-16 18:40:11 -06:00
Keyan
1f355140f3
Merge pull request #751 from stackernews/fix-title-in-logs
Fix logged title
2024-01-16 15:18:02 -06:00
keyan
9af3388353 semantic search 2024-01-15 17:22:57 -06:00
ekzyis
f450a00073 Fix logged title 2024-01-14 21:14:14 +01:00
Keyan
75bf4aced9
Merge pull request #750 from stackernews/fix-day-filter
Fix 'column Item.day does not exist'
2024-01-14 13:01:00 -06:00
ekzyis
b7413e9e32 Fix 'column Item.day does not exist' 2024-01-14 19:10:31 +01:00
keyan
d45a46e7a4 improve copy on autowithdraw 2024-01-13 11:28:14 -06:00
Keyan
11b05b0f8a
Merge pull request #748 from stackernews/524-autowithdraw-lnaddr-review
Small changes regarding automated withdrawals
2024-01-13 11:11:48 -06:00
ekzyis
40f2697675 Disallow automated withdrawals to same node 2024-01-13 17:32:54 +01:00
ekzyis
5dd4136c0b Fix missing HTTPS support for automated withdrawals during development 2024-01-13 17:32:54 +01:00
ekzyis
12f9c4761d Fix isNaN checks 2024-01-13 17:32:53 +01:00
keyan
59ff146cc9 fix fee percent typo 2024-01-12 23:50:17 -06:00
keyan
09f459b569 fix fee percent type error 2024-01-12 23:47:05 -06:00
keyan
6288db1b83 fix autowithdraw wording 2024-01-12 23:45:38 -06:00
keyan
b530b611f5 disable self sends in autowithdraw 2024-01-12 09:37:50 -06:00
keyan
1d388942e9 fix attach wallet styling for mobile 2024-01-12 08:56:39 -06:00
keyan
1dae33312f allow territories to be renamed 2024-01-11 17:48:08 -06:00
Keyan
cd2979b022
Merge pull request #746 from stackernews/524-autowithdraw-lnaddr
autowithdraw to lightning address
2024-01-11 13:31:04 -06:00
keyan
86e8350994 autowithdraw to lightning address 2024-01-11 13:10:07 -06:00
keyan
efc566c3da improve newsletter script 2024-01-11 13:08:37 -06:00
keyan
572103b2e3 improve wallet limit phrasing 2024-01-11 13:08:37 -06:00
ekzyis
bdf9b1f0fd
Territory post notifications (#745)
* Notify founders of new posts

* Only merge notifications of same territory

* Show territory posts in /notifications

* Don't notify on own posts
2024-01-11 11:27:54 -06:00
ekzyis
39c9775c4c
Fix TypeError on item creation if JIT invoicing is used (#744)
* Fix TypeError on item creation if JIT invoicing is used

* Fix bad if body

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-01-10 19:24:49 -06:00
ekzyis
c267bf92fd
Fix TypeError (reading 'catch') (#742)
Co-authored-by: ekzyis <ek@stacker.news>
2024-01-10 14:18:41 -06:00
keyan
c243a6d8be robust lnd subscriptions and robust held recording 2024-01-10 09:50:57 -06:00
ekzyis
df1edd5b79
Fix push notification counting on iOS devices (#739)
Co-authored-by: ekzyis <ek@stacker.news>
2024-01-10 09:25:30 -06:00
keyan
9a9e81b109 refine territory details on post form 2024-01-08 19:02:00 -06:00
ekzyis
f8cbd43be7
Show territory details in post form (#725)
* Show territory details in post form

* Style territory details in post form

* Keep details closed by default

* Use SUB_FULL

* Undo unused changes to specify accordian default

---------

Co-authored-by: ekzyis <ek@stacker.news>
2024-01-08 17:46:23 -06:00
ekzyis
2151323c8d
Use LND subscriptions (#726)
* Use parallel invoice subscriptions

* Fix missing idempotency

* Log error

* Use cursor for invoice subscription

* Subscribe to outgoing payments for withdrawals

* Add TODO comments regarding migration to LND subscriptions

* Also use isPoll variable in checkInvoice

* Queue status check of pending withdrawals

* Use for loop to check pending withdrawals

* Reconnect to LND gRPC API on error

* Fix hash modified of applied migrations

* Separate wallet code from worker index

* refactor subscription code some more

* remove unnecessary subWrapper abstraction
* move all wallet related code into worker/wallet.js such that only a single import is needed in worker/index.js

* Migrate from polling to LND subscriptions

* Remove unnecessary reconnect code

* Add FIXME

* Add listener for HODL invoice updates

* Remove obsolete comment

* Update README

* Add job to cancel hodl invoice if expired

* Fix missing else

* small bug fixes and readability enhancements

* refine and add periodic redundant deposit/withdrawal checks

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-01-08 16:37:58 -06:00
SatsAllDay
cb076eca77
add link to email signup page for newsletter in newsletter signature (#737) 2024-01-07 10:43:00 -06:00
702 changed files with 110406 additions and 26611 deletions

View File

@ -1,20 +0,0 @@
{
"presets": [
"next/babel"
],
"plugins": [
[
"inline-react-svg",
{
"svgo": {
"plugins": [
{
"name": "removeViewBox",
"active": false
}
]
}
}
]
]
}

View File

@ -1,5 +0,0 @@
commands:
00_install_epel:
command: 'sudo amazon-linux-extras install epel'
01_install_chromium:
command: 'sudo yum install -y chromium'

View File

@ -0,0 +1,3 @@
commands:
00_install_psql:
command: 'sudo amazon-linux-extras install -y postgresql13'

186
.env.development Normal file
View File

@ -0,0 +1,186 @@
PRISMA_SLOW_LOGS_MS=
GRAPHQL_SLOW_LOGS_MS=
NODE_ENV=development
COMPOSE_PROFILES='minimal,images,search,payments,wallets,email,capture'
############################################################################
# OPTIONAL SECRETS #
# put these in .env.local, and don't commit them to git #
############################################################################
# github
GITHUB_ID=
GITHUB_SECRET=
# twitter
TWITTER_ID=
TWITTER_SECRET=
# email list
LIST_MONK_AUTH=
# VAPID for Web Push
VAPID_MAILTO=
NEXT_PUBLIC_VAPID_PUBKEY=
VAPID_PRIVKEY=
# slack
SLACK_BOT_TOKEN=
SLACK_CHANNEL_ID=
# lnurl ... you'll need a tunnel to localhost:3000 for these
LNAUTH_URL=http://localhost:3000/api/lnauth
LNWITH_URL=http://localhost:3000/api/lnwith
########################################
# SNDEV STUFF WE PRESET #
# which you can override in .env.local #
########################################
# email
LOGIN_EMAIL_SERVER=smtp://mailhog:1025
LOGIN_EMAIL_FROM=sndev@mailhog.dev
# email salt
# openssl rand -hex 32
EMAIL_SALT=202c90943c313b829e65e3f29164fb5dd7ea3370d7262c4159691c2f6493bb8b
# static things
NEXTAUTH_URL=http://localhost:3000/api/auth
SELF_URL=http://app:3000
PUBLIC_URL=http://localhost:3000
NEXT_PUBLIC_URL=http://localhost:3000
LND_CONNECT_ADDRESS=03cc1d0932bb99b0697f5b5e5961b83ab7fd66f1efc4c9f5c7bad66c1bcbe78f02@xhlmkj7mfrl6ejnczfwl2vqik3xim6wzmurc2vlyfoqw2sasaocgpuad.onion:9735
NEXTAUTH_SECRET=3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI
JWT_SIGNING_PRIVATE_KEY={"kty":"oct","kid":"FvD__hmeKoKHu2fKjUrWbRKfhjimIM4IKshyrJG4KSM","alg":"HS512","k":"3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI"}
INVOICE_HMAC_KEY=a4c1d9c81edb87b79d28809876a18cf72293eadb39f92f3f4f2f1cfbdf907c91
# lnd
# xxd -p -c0 docker/lnd/sn/regtest/admin.macaroon
LND_CERT=2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494943516a43434165696741774942416749516139493834682b48653350385a437541525854554d54414b42676771686b6a4f50515144416a41344d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d5255774577594456515144457778694e6a41785a5749780a4d474d354f444d774868634e4d6a51774d7a41334d5463774d6a45355768634e4d6a55774e5441794d5463774d6a4535576a41344d523877485159445651514b0a45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d5255774577594456515144457778694e6a41785a5749784d474d354f444d770a5754415442676371686b6a4f5051494242676771686b6a4f50514d4242774e4341415365596a4b62542b4a4a4a37624b6770677a6d6c3278496130364e3174680a2f4f7033533173382b4f4a41387836647849682f326548556b4f7578675a36703549434b496f375a544c356a5963764375793941334b6e466f3448544d4948510a4d41344741315564447745422f775145417749437044415442674e56485355454444414b4267677242674546425163444154415042674e5648524d42416638450a425441444151482f4d4230474131556444675157424252545756796e653752786f747568717354727969466d6a36736c557a423542674e5648524545636a42770a676778694e6a41785a5749784d474d354f444f4343577876593246736147397a64494947633235666247356b6768526f62334e304c6d52765932746c636935700a626e526c636d356862494945645735706549494b64573570654842685932746c64494948596e566d59323975626f6345667741414159635141414141414141410a41414141414141414141414141596345724273414254414b42676771686b6a4f5051514441674e4941444246416945413873616c4a667134476671465557532f0a35347a335461746c6447736673796a4a383035425a5263334f326f434943794e6e3975716976566f5575365935345143624c3966394c575779547a516e61616e0a656977482f51696b0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a
LND_MACAROON=0201036c6e6402f801030a106cf4e146abffa5d766befbbf4c73b5a31201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e6572617465120472656164000006202c3bfd55c191e925cbffd73712c9d4b9b4a8440410bde5f8a0a6e33af8b3d876
LND_SOCKET=sn_lnd:10009
# nostr (NIP-57 zap receipts)
# openssl rand -hex 32
NOSTR_PRIVATE_KEY=5f30b7e7714360f51f2be2e30c1d93b7fdf67366e730658e85777dfcc4e4245f
# imgproxy
NEXT_PUBLIC_IMGPROXY_URL=http://localhost:3001
IMGPROXY_KEY=9c273e803fd5d444bf8883f8c3000de57bee7995222370cab7f2d218dd9a4bbff6ca11cbf902e61eeef4358616f231da51e183aee6841e3a797a5c9a9530ba67
IMGPROXY_SALT=47b802be2c9250a66b998f411fc63912ab0bc1c6b47d99b8d37c61019d1312a984b98745eac83db9791b01bb8c93ecbc9b2ef9f2981d66061c7d0a4528ff6465
IMGPROXY_ENABLE_WEBP_DETECTION=1
IMGPROXY_ENABLE_AVIF_DETECTION=1
IMGPROXY_MAX_ANIMATION_FRAMES=2000
IMGPROXY_MAX_SRC_RESOLUTION=50
IMGPROXY_MAX_ANIMATION_FRAME_RESOLUTION=200
IMGPROXY_READ_TIMEOUT=10
IMGPROXY_WRITE_TIMEOUT=10
IMGPROXY_DOWNLOAD_TIMEOUT=9
IMGPROXY_ENABLE_VIDEO_THUMBNAILS=1
# IMGPROXY_DEVELOPMENT_ERRORS_MODE=1
# IMGPROXY_ENABLE_DEBUG_HEADERS=true
NEXT_PUBLIC_AWS_UPLOAD_BUCKET=uploads
NEXT_PUBLIC_MEDIA_DOMAIN=localhost:4566
NEXT_PUBLIC_MEDIA_URL=http://localhost:4566/uploads
# search
OPENSEARCH_URL=http://opensearch:9200
OPENSEARCH_USERNAME=admin
OPENSEARCH_PASSWORD=mVchg1T5oA9wudUh
OPENSEARCH_INDEX=item
OPENSEARCH_MODEL_ID=
# prisma db url
DATABASE_URL="postgresql://sn:password@db:5432/stackernews?schema=public"
DB_APP_CONNECTION_LIMIT=2
DB_WORKER_CONNECTION_LIMIT=2
DB_TRANSACTION_TIMEOUT=5000
# polling intervals
NEXT_PUBLIC_FAST_POLL_INTERVAL=1000
NEXT_PUBLIC_NORMAL_POLL_INTERVAL=30000
NEXT_PUBLIC_LONG_POLL_INTERVAL=60000
NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL=300000
# containers can't use localhost, so we need to use the container name
IMGPROXY_URL_DOCKER=http://imgproxy:8080
MEDIA_URL_DOCKER=http://s3:4566/uploads
# postgres container stuff
POSTGRES_PASSWORD=password
POSTGRES_USER=sn
POSTGRES_DB=stackernews
# opensearch container stuff
OPENSEARCH_INITIAL_ADMIN_PASSWORD=mVchg1T5oA9wudUh
DISABLE_SECURITY_DASHBOARDS_PLUGIN=true
# bitcoind container stuff
RPC_AUTH='7c68e5fcdba94a366bfdf629ecc676bb$0d0fc087c3bf7f068f350292bf8de1418df3dd8cb31e35682d5d3108d601002b'
RPC_USER=bitcoin
RPC_PASS=bitcoin
RPC_PORT=18443
P2P_PORT=18444
ZMQ_BLOCK_PORT=28334
ZMQ_TX_PORT=28335
ZMQ_HASHBLOCK_PORT=29000
# sn_lnd container stuff
SN_LND_REST_PORT=8080
SN_LND_GRPC_PORT=10009
SN_LND_P2P_PORT=9735
# docker exec -u lnd sn_lnd lncli newaddress p2wkh --unused
SN_LND_ADDR=bcrt1q7q06n5st4vqq3lssn0rtkrn2qqypghv9xg2xnl
SN_LND_PUBKEY=02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490
# sn_lndk stuff
SN_LNDK_GRPC_PORT=10012
# lnd container stuff
LND_REST_PORT=8081
LND_GRPC_PORT=10010
# docker exec -u lnd lnd lncli newaddress p2wkh --unused
LND_ADDR=bcrt1qfqau4ug9e6rtrvxrgclg58e0r93wshucumm9vu
LND_PUBKEY=028093ae52e011d45b3e67f2e0f2cb6c3a1d7f88d2920d408f3ac6db3a56dc4b35
# cln container stuff
CLN_REST_PORT=9092
# docker exec -u clightning cln lightning-cli newaddr bech32
CLN_ADDR=bcrt1q02sqd74l4pxedy24fg0qtjz4y2jq7x4lxlgzrx
CLN_PUBKEY=03ca7acec181dbf5e427c682c4261a46a0dd9ea5f35d97acb094e399f727835b90
# sndev cli eclair getnewaddress
# sndev cli eclair getinfo
ECLAIR_ADDR="bcrt1qdus2yml69wsax3unz8pts9h979lc3s4tw0tpf6"
ECLAIR_PUBKEY="02268c74cc07837041131474881f97d497706b89a29f939555da6d094b65bd5af0"
# router lnd container stuff
ROUTER_LND_REST_PORT=8082
ROUTER_LND_GRPC_PORT=10011
# docker exec -u lnd router_lnd lncli newaddress p2wkh --unused
ROUTER_LND_ADDR=bcrt1qfkmwfpwgn6wt0dd36s79x04swz8vleyafsdpdr
ROUTER_LND_PUBKEY=02750991fbf62e57631888bc469fae69c5e658bd1d245d8ab95ed883517caa33c3
LNCLI_NETWORK=regtest
# localstack container stuff
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
PERSISTENCE=1
SKIP_SSL_CERT_DOWNLOAD=1
# tor proxy
TOR_PROXY=http://tor:7050/
grpc_proxy=http://tor:7050/
# lnbits
LNBITS_WEB_PORT=5001
# CPU shares for each category
CPU_SHARES_IMPORTANT=1024
CPU_SHARES_MODERATE=512
CPU_SHARES_LOW=256
NEXT_TELEMETRY_DISABLED=1

25
.env.production Normal file
View File

@ -0,0 +1,25 @@
LIST_MONK_URL=https://mail.stacker.news
LNAUTH_URL=https://stacker.news/api/lnauth
LND_CONNECT_ADDRESS=03cc1d0932bb99b0697f5b5e5961b83ab7fd66f1efc4c9f5c7bad66c1bcbe78f02@52.5.194.83:9735
LNWITH_URL=https://stacker.news/api/lnwith
LOGIN_EMAIL_FROM=login@stacker.news
NEXTAUTH_URL=https://stacker.news
NEXTAUTH_URL_INTERNAL=http://127.0.0.1:8080/api/auth
NEXT_PUBLIC_AWS_UPLOAD_BUCKET=snuploads
NEXT_PUBLIC_IMGPROXY_URL=https://imgprxy.stacker.news/
NEXT_PUBLIC_MEDIA_DOMAIN=m.stacker.news
PUBLIC_URL=https://stacker.news
SELF_URL=http://127.0.0.1:8080
grpc_proxy=http://127.0.0.1:7050
NEXT_PUBLIC_FAST_POLL_INTERVAL=1000
NEXT_PUBLIC_NORMAL_POLL_INTERVAL=30000
NEXT_PUBLIC_LONG_POLL_INTERVAL=60000
NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL=300000
NEXT_PUBLIC_URL=https://stacker.news
TOR_PROXY=http://127.0.0.1:7050/
PRISMA_SLOW_LOGS_MS=50
GRAPHQL_SLOW_LOGS_MS=50
DB_APP_CONNECTION_LIMIT=4
DB_WORKER_CONNECTION_LIMIT=2
DB_TRANSACTION_TIMEOUT=10000
NEXT_TELEMETRY_DISABLED=1

View File

@ -1,92 +0,0 @@
############################################################################
# AUTH / OPTIONAL #
# if you want to work on logged in features, you'll need some kind of auth #
############################################################################
# github
GITHUB_ID=<YOUR GITHUB ID>
GITHUB_SECRET=<YOUR GITHUB SECRET>
# twitter
TWITTER_ID=<YOUR TWITTER ID>
TWITTER_SECRET=<YOUR TWITTER SECRET>
# email
LOGIN_EMAIL_SERVER=smtp://<YOUR EMAIL>:<YOUR PASSWORD>@<YOUR SMTP DOMAIN>:587
LOGIN_EMAIL_FROM=<YOUR FROM ALIAS>
LIST_MONK_AUTH=
#####################################################################
# OTHER / OPTIONAL #
# configuration for push notifications, slack and imgproxy are here #
#####################################################################
# VAPID for Web Push
VAPID_MAILTO=
NEXT_PUBLIC_VAPID_PUBKEY=
VAPID_PRIVKEY=
# slack
SLACK_BOT_TOKEN=
SLACK_CHANNEL_ID=
# imgproxy
NEXT_PUBLIC_IMGPROXY_URL=
IMGPROXY_KEY=
IMGPROXY_SALT=
# search
OPENSEARCH_URL=http://opensearch:9200
OPENSEARCH_USERNAME=
OPENSEARCH_PASSWORD=
#######################################################
# WALLET / OPTIONAL #
# if you want to work with payments you'll need these #
#######################################################
# lnd
LND_CERT=<YOUR LND HEX CERT>
LND_MACAROON=<YOUR LND HEX MACAROON>
LND_SOCKET=<YOUR LND GRPC HOST>:<YOUR LND GRPC PORT>
# lnurl
LNAUTH_URL=<PUBLIC URL TO /api/lnauth>
LNWITH_URL=<PUBLIC URL TO /api/lnwith>
# nostr (NIP-57 zap receipts)
NOSTR_PRIVATE_KEY=<YOUR NOSTR PRIVATE KEY IN HEX>
###############
# LEAVE AS IS #
###############
# static things
NEXTAUTH_URL=http://localhost:3000/api/auth
SELF_URL=http://app:3000
PUBLIC_URL=http://localhost:3000
LND_CONNECT_ADDRESS=03cc1d0932bb99b0697f5b5e5961b83ab7fd66f1efc4c9f5c7bad66c1bcbe78f02@xhlmkj7mfrl6ejnczfwl2vqik3xim6wzmurc2vlyfoqw2sasaocgpuad.onion:9735
NEXTAUTH_SECRET=3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI
JWT_SIGNING_PRIVATE_KEY={"kty":"oct","kid":"FvD__hmeKoKHu2fKjUrWbRKfhjimIM4IKshyrJG4KSM","alg":"HS512","k":"3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI"}
INVOICE_HMAC_KEY=a4c1d9c81edb87b79d28809876a18cf72293eadb39f92f3f4f2f1cfbdf907c91
# imgproxy options
IMGPROXY_ENABLE_WEBP_DETECTION=1
IMGPROXY_ENABLE_AVIF_DETECTION=1
IMGPROXY_MAX_ANIMATION_FRAMES=2000
IMGPROXY_MAX_SRC_RESOLUTION=50
IMGPROXY_MAX_ANIMATION_FRAME_RESOLUTION=200
IMGPROXY_READ_TIMEOUT=10
IMGPROXY_WRITE_TIMEOUT=10
IMGPROXY_DOWNLOAD_TIMEOUT=9
# IMGPROXY_DEVELOPMENT_ERRORS_MODE=1
# IMGPROXY_ENABLE_DEBUG_HEADERS=true
# prisma db url
DATABASE_URL="postgresql://sn:password@db:5432/stackernews?schema=public"
# postgres container stuff
POSTGRES_PASSWORD=password
POSTGRES_USER=sn
POSTGRES_DB=stackernews

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
docker/db/seed.sql linguist-vendored

View File

@ -1,35 +0,0 @@
---
name: Bug report
about: Report a problem
title: ''
labels: bug
assignees: ''
---
*Note: this template is meant to help you report the bug so that we can fix it faster, ie not all of these sections are required*
**Description**
A clear and concise description of what the bug is.
**Steps to Reproduce**
A clear and concise way we might be able to reproduce the bug.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
If applicable, add your browsers console logs.
**Environment:**
If you only experience the issue on certain devices or browsers, provide that info.
- Device: [e.g. iPhone6]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

65
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,65 @@
name: 🐞 Bug report
description: Create a bug report to help us fix it
title: "bug report"
labels: [bug]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. Include images if relevant.
placeholder: I accidentally deleted the internet. Here's my story ...
validations:
required: true
- type: textarea
attributes:
label: Screenshots
description: |
Add screenshots to help explain your problem. You can also add a video here.
Tip: You can attach images or video files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the bug.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll to '...'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen
validations:
required: true
- type: textarea
attributes:
label: Logs
description: If applicable, add your browser's console logs here
- type: textarea
attributes:
label: Device information
placeholder: |
- OS: [e.g. Windows]
- Browser: [e.g. chrome, safari, firefox]
- Browser Version: [e.g. 22]
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: |
Do you have links to discussions about this on SN or other references?
validations:
required: false

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Questions
url: https://stacker.news/~meta
about: If you simply have a question, you can ask it in ~meta or the saloon.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest a feature
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,32 @@
name: ✨ Feature request
description: Request a feature you'd like to see in SN!
title: "feature request"
labels: [feature]
body:
- type: markdown
attributes:
value: |
We're always looking for suggestions on how we could improve SN!
- type: textarea
attributes:
label: Describe the problem you're trying to solve
description: |
Is your feature request related to a problem? Add a clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: |
A clear and concise description of any alternative solutions or features you have considered.
- type: textarea
attributes:
label: Additional context
description: |
Add any other additional context or screenshots about the feature request here.

22
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,22 @@
## Description
_A clear and concise description of what you changed and why._
## Screenshots
## Additional Context
_Was anything unclear during your work on this PR? Anything we should definitely take a closer look at?_
## Checklist
**Are your changes backwards compatible? Please answer below:**
**On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:**
**For frontend changes: Tested on mobile, light and dark mode? Please answer below:**
**Did you introduce any new environment variables? If so, call them out explicitly here:**

35
.github/workflows/extend-awards.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: extend-awards
on:
pull_request_target:
types: [ closed ]
branches:
- master
permissions:
pull-requests: write
contents: write
issues: read
jobs:
if_merged:
if: |
github.event_name == 'pull_request_target' &&
github.event.action == 'closed' &&
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref != 'extend-awards/patch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- run: pip install requests
- run: python extend-awards.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_CONTEXT: ${{ toJson(github) }}
- uses: peter-evans/create-pull-request@v7
with:
add-paths: awards.csv
branch: extend-awards/patch
commit-message: Extending awards.csv
title: Extending awards.csv
body: A PR was merged that solves an issue and awards.csv should be extended.

View File

@ -1,8 +1,8 @@
name: Eslint Check
name: Lint Check
on: [pull_request]
jobs:
eslint-run:
lint-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -11,7 +11,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18.17.0"
node-version: "18.20.4"
- name: Install
run: npm install

17
.github/workflows/shell-check.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: ShellCheck
on: [pull_request]
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- name: Shellcheck
uses: actions/checkout@v3
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: error
scandir: ./sndev

20
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Tests
on: [pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18.20.4"
- name: Install
run: npm install
- name: Test
run: npm test

35
.gitignore vendored
View File

@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
node_modules/
/.pnp
.pnp.js
.cache
@ -27,12 +27,12 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env
envbak
.env.local
.env.development.local
.env.test.local
.env.production.local
.env*
!.env.development
!.env.production
# local settings
.vscode/settings.json
# vercel
.vercel
@ -42,12 +42,27 @@ envbak
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
# copilot
copilot/
# service worker
public/sw.js*
sw/precache-manifest.json
public/workbox-*.js*
public/*-development.js
.cache_ggshield
docker-compose.*.yml
*.sql
!/prisma/migrations/*/*.sql
!/docker/db/seed.sql
# nostr wallet connect
scripts/nwc-keys.json
# lnbits
docker/lnbits/data
# lndk
!docker/lndk/tls-*.pem
# nostr link extract
scripts/nostr-link-extract.config.json
scripts/nostr-links.db

View File

@ -1,9 +0,0 @@
const {join} = require('path');
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
// Changes the cache location for Puppeteer.
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};

View File

@ -1,11 +1,19 @@
# syntax=docker/dockerfile:1
FROM node:18.17.0-bullseye
FROM node:18.20.4-bullseye
ENV NODE_ENV=development
ARG UID
ARG GID
RUN groupadd -fg "$GID" apprunner
RUN useradd -om -u "$UID" -g "$GID" apprunner
USER apprunner
WORKDIR /app
EXPOSE 3000
CMD npm install --loglevel verbose --legacy-peer-deps; npx prisma migrate dev; npm run dev
COPY package.json package-lock.json ./
RUN npm ci --legacy-peer-deps --loglevel verbose
CMD ["sh","-c","npm install --loglevel verbose --legacy-peer-deps && npx prisma migrate dev && npm run dev"]

486
README.md
View File

@ -1,46 +1,476 @@
# contributing
We pay sats for PRs. Sats will be proportional to the impact of the PR. If there's something you'd like to work on, suggest how much you'd do it for on the issue. If there's something you'd like to work on that isn't already an issue, whether its a bug fix or a new feature, create one.
<p align="center">
<a href="https://stacker.news">
<img height="50" alt="Internet Communities with Bitcoin Economies" src="https://github.com/stackernews/stacker.news/assets/34140557/a8ccc5dc-c453-46dc-be74-60dd0a42ce09">
</a>
</p>
We aim to have a quick PR turnaround time, but we are sometimes slower than we'd like. In most cases, if your PR is unambiguously good, it shouldn't take us more than 1 week.
If you have a question about contributing start a [discussion](https://github.com/stackernews/stacker.news/discussions).
- Stacker News is trying to fix online communities with economics
- What You See is What We Ship (look ma, I invented an initialism)
- 100% FOSS
- We pay bitcoin for PRs, issues, documentation, code reviews and more
- Next.js, postgres, graphql, and lnd
# responsible disclosure
<br>
If you found a vulnerability, we would greatly appreciate it if you contact us via [kk@stacker.news](mailto:kk@stacker.news) or t.me/k00bideh.
# Getting started
# stacker.news
[Stacker News](https://stacker.news) is like Hacker News but we pay you Bitcoin. We use Bitcoin and the Lightning Network to provide Sybil resistance and any karma earned is withdrawable as Bitcoin.
Launch a fully featured SN development environment in a single command.
# wen decentralization
We're experimenting with providing an SN-like service on nostr in [Outer Space](https://outer.space). It's our overarching goal to align SN with Bitcoin's ethos yet still make a product the average bitcoiner loves to use.
```sh
$ ./sndev start
```
# local development
1. [Install docker-compose](https://docs.docker.com/compose/install/) and deps if you don't already have it installed
2. `git clone git@github.com:stackernews/stacker.news.git sn && cd sn`
3. `docker-compose up --build`
Go to [localhost:3000](http://localhost:3000).
You should then be able to access the site at `localhost:3000` and any changes you make will hot reload. If you want to login locally or use lnd you'll need to modify `.env.sample` appropriately. More details [here](./docs/local-auth.md) and [here](./docs/local-lnd.md). If you have trouble please open an issue so I can help and update the README for everyone else.
<br>
# web push
## Installation
To enable Web Push locally, you will need to set the `VAPID_*` env vars. `VAPID_MAILTO` needs to be an email address using the `mailto:` scheme. For `NEXT_PUBLIC_VAPID_KEY` and `VAPID_PRIVKEY`, you can run `npx web-push generate-vapid-keys`.
- Clone the repo
- ssh: `git clone git@github.com:stackernews/stacker.news.git`
- https: `git clone https://github.com/stackernews/stacker.news.git`
- Install [docker](https://docs.docker.com/compose/install/)
- If you're running MacOS or Windows, I ***highly recommend*** using [OrbStack](https://orbstack.dev/) instead of Docker Desktop
- Please make sure that at least 10 GB of free space is available, otherwise you may encounter issues while setting up the development environment.
# imgproxy
<br>
To configure the image proxy, you will need to set the `IMGPROXY_` env vars. `NEXT_PUBLIC_IMGPROXY_URL` needs to point to the image proxy service. `IMGPROXY_KEY` and `IMGPROXY_SALT` can be set using `openssl rand -hex 64`.
## Usage
# stack
The site is written in javascript using Next.js, a React framework. The backend API is provided via GraphQL. The database is PostgreSQL modeled with Prisma. The job queue is also maintained in PostgreSQL. We use lnd for our lightning node. A customized Bootstrap theme is used for styling.
Start the development environment
# processes
There are two. 1. the web app and 2. the worker, which dequeues jobs sent to it by the web app, e.g. polling lnd for invoice/payment status
```sh
$ ./sndev start
```
# wallet transaction safety
To ensure stackers balances are kept sane, all wallet updates are run in serializable transactions at the database level. Because prisma has relatively poor support for transactions all wallet touching code is written in plpgsql stored procedures and can be found in the prisma/migrations folder.
View all available commands
# code
The code is linted with standardjs.
```sh
$ ./sndev help
# license
888
888
888
.d8888b 88888b. .d88888 .d88b. 888 888
88K 888 '88b d88' 888 d8P Y8b 888 888
'Y8888b. 888 888 888 888 88888888 Y88 88P
X88 888 888 Y88b 888 Y8b. Y8bd8P
88888P' 888 888 'Y88888 'Y8888 Y88P
manages a docker based stacker news development environment
USAGE
$ sndev [COMMAND]
$ sndev help [COMMAND]
COMMANDS
help show help
env:
start start env
stop stop env
restart restart env
status status of env
logs logs from env
delete delete env
sn:
login login as a nym
set_balance set the balance of a nym
lightning:
fund pay a bolt11 for funding
withdraw create a bolt11 for withdrawal
db:
psql open psql on db
prisma run prisma commands
dev:
pr fetch and checkout a pr
lint run linters
test run tests
other:
cli service cli passthrough
open open service GUI in browser
onion service onion address
cert service tls cert
compose docker compose passthrough
```
### Modifying services
#### Running specific services
By default all services will be run. If you want to exclude specific services from running, set `COMPOSE_PROFILES` in a `.env.local` file to one or more of `minimal,images,search,payments,wallets,email,capture`. To only run mininal necessary without things like payments in `.env.local`:
```.env
COMPOSE_PROFILES=minimal
```
To run with images and payments services:
```.env
COMPOSE_PROFILES=images,payments
```
#### Merging compose files
By default `sndev start` will merge `docker-compose.yml` with `docker-compose.override.yml`. Specify any overrides you want to merge with `docker-compose.override.yml`.
For example, if you want to replace the db seed with a custom seed file located in `docker/db/another.sql`, you'd create a `docker-compose.override.yml` file with the following:
```yml
services:
db:
volumes:
- ./docker/db/another.sql:/docker-entrypoint-initdb.d/seed.sql
```
You can read more about [docker compose override files](https://docs.docker.com/compose/multiple-compose-files/merge/).
<br>
# Table of Contents
- [Getting started](#getting-started)
- [Installation](#installation)
- [Usage](#usage)
- [Modifying services](#modifying-services)
- [Running specific services](#running-specific-services)
- [Merging compose files](#merging-compose-files)
- [Contributing](#contributing)
- [We pay bitcoin for contributions](#we-pay-bitcoin-for-contributions)
- [Pull request awards](#pull-request-awards)
- [Code review awards](#code-review-awards)
- [Issue specification awards](#issue-specification-awards)
- [Responsible disclosure of security or privacy vulnerability awards](#responsible-disclosure-of-security-or-privacy-vulnerability-awards)
- [Development documentation awards](#development-documentation-awards)
- [Helpfulness awards](#helpfulness-awards)
- [Contribution extras](#contribution-extras)
- [Dev chat](#dev-chat)
- [Triage permissions](#triage-permissions)
- [Contributor badges on SN profiles](#contributor-badges-on-sn-profiles)
- [What else you got](#what-else-you-got)
- [Development Tips](#development-tips)
- [Linting](#linting)
- [Database migrations](#database-migrations)
- [Connecting to the local database](#connecting-to-the-local-database)
- [Running lncli on the local lnd nodes](#running-lncli-on-the-local-lnd-nodes)
- [Testing local auth](#testing-local-auth)
- [Login with Email](#login-with-email)
- [Login with Github](#login-with-github)
- [Login with Lightning](#login-with-lightning)
- [Enabling web push notifications](#enabling-web-push-notifications)
- [Internals](#internals)
- [Stack](#stack)
- [Services](#services)
- [Wallet transaction safety](#wallet-transaction-safety)
- [Need help?](#need-help)
- [Responsible Disclosure](#responsible-disclosure)
- [License](#license)
<br>
# Contributing
We want your help.
<br>
## We pay bitcoin for contributions
- pull requests closing existing issues
- code review
- issue specification whether for bugs, features, or enhancements
- discovery of security vulnerabilities
- discovery of privacy vulnerabilities
- improvements to development documentation
- helpfulness
[View a current list of granted awards](awards.csv)
<br>
## Just in case
*This document in no way legally entitles you to payments for contributions, entitles you to being a contributor, or entitles you to the attention of other contributors. This document lays out the system we **can** use to determine contribution awards which we generally intend to abide by but importantly we reserve the right to refuse payments or contributions, modify rules and award amounts, make exceptions to rules or reward amounts, and withhold awards for any reason at anytime, even just for the heck of it, at our sole discretion. If you need more certainty than what I've just described, don't participate. We provide awards as an experiment to make FOSS less sucky.*
<br>
## Pull request awards
### Rules
1. PRs closing an issue will be awarded according to the `difficulty` tag on an issue, e.g. `difficulty:easy` pays 100k sats.
2. Issues are occasionally marked with a `priority` tag which multiplies the award of a PR closing an issue, e.g. an issue marked with `priority:high` and `difficulty:hard` awards 2m sats.
3. An award is reduced by 10% of the award amount for each substantial change requested to the PR on code review, e.g. if two changes are requested on a PR closing an issue tagged with `difficulty:hard`, 800k sats will be awarded.
- Reductions are applied before `priority` multipliers, e.g. a PR closing a `priority:high` and `difficulty:hard` issue that's approved after two changes are requested awards 1.6m sats.
- You are responsible for understanding the issue and requirements **before requesting review on a PR**.
- There is no award reduction for asking specific questions on the issue itself or on the PR **before requesting review**
- There is no award reduction for asking more general questions in a discussion
4. A PR must be merged by an SN engineer before a PR receives an award
_Due to Rule 3, make sure that you mark your PR as a draft when you create it and it's not ready for review yet._
### Difficulty award amounts
| tag | description | award |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `difficulty:good-first-issue` | at most a couple lines of code in a couple files and does not require much familiarity with the codebase | `20k sats` |
| `difficulty:easy` | at most a couple lines of code in a couple files but does require familiarity with the code base | `100k sats` |
| `difficulty:medium` | more code, more places and could require adding columns in the db and some modification chunky db queries | `250k sats` |
| `difficulty:medium-hard` | even more code, even more places and requires either more sophisticated logic, more significant db modeling eg adding a table, and/or a deeper study of a something | `500k sats` |
| `difficulty:hard` | either a bigger lift than the what's required of medium-hard or very tricky in a particular way that might not require a lot of code but does require a lot of context/troubleshooting/expertise | `1m sats` |
### Priority multipliers
| tag | multiplier |
| ----------------- | ---------- |
| `priority:low` | 0.5 |
| `priority:medium` | 1.5 |
| `priority:high` | 2 |
| `priority:urgent` | 3 |
### Requesting modifications to reward amounts
We try to assign difficulty and priority tags to issues accurately, but we're not perfect. If you believe an issue is mis-tagged, you can request a change to the issue's tags.
<br>
## Code review awards
Code reviewers will be awarded the amount their code review reduced from the PR author's reward, e.g. two substantial problems/areas of improvement identified in a PR closing a `priority:high` and `difficulty:hard` issue awards 400k sats.
### Rules
1. The problem or improvement must be acknowledged as such by SN engineers explicitly
2. A PR must be merged by an SN engineer before a PR's code reviewers receive an award
Code review approvals are more than welcome, but we can't guarantee awards for them because the work performed to approve a PR is unverifiable.
<br>
## Issue specification awards
Issue specifiers will be awarded up to 10% of a PR award for issues resulting in a PR being merged by an SN engineer that closes the issue. In addition to being subject to PR award amounts and reductions, specification amounts are awarded on the basis of how much additional help and specification is required by other contributors.
### Rules
1. The issue must directly result in PR being merged by an SN engineer that closes the issue
2. Issue specification award amounts are based on the final PR award amounts
- that is, they are subject to PR award code review reductions and priority multipliers
3. Award amounts will be reduced on the basis of how much additional help and specification is required by other contributors
4. Issue specifiers who can close their own issues with their own PRs are also eligible for this 10%
- e.g an issue tagged as `difficulty:hard` that is both specified and closed by a PR from the same contributor without changes requested awards 1.1m sats
### Relative awards
| circumstances | award |
| ---------------------------------------------------------------------------------------------------------- | ----- |
| issue doesn't require further help and/or specification from other contributors | 10% |
| issue requires little help and/or specification from other contributors | 5% |
| issue requires more help and/or specification from other contributors than the issue specifier contributed | 1% |
| issue is vague and/or incomplete and must mostly be entirely specified by someone else | 0% |
For example: a specified issue that's tagged as `difficulty:hard`, doesn't require additional specification and disambiguation by other contributors, and results in PR being merged without changes requested awards the issue specifier 100k sats.
<br>
## Responsible disclosure of security or privacy vulnerability awards
Awards for responsible disclosures are assessed on the basis of:
1. the potential loss resulting from an exploit of the vulnerability
2. the trivialness of exploiting the vulnerability
3. the disclosure's detail
Award amounts will be easiest to assess on a case by case basis. Upon confirmation of a vulnerability, we agree to award responsible disclosures at minimum 100k sats and as high as the total potential loss that would result from exploiting the vulnerability.
### Rules
1. Disclosure is responsible and does not increase the likelihood of an exploit.
2. Disclosure includes steps to reproduce.
3. Disclosure includes a realistic attack scenario with prerequisites for an attack and expected gains after the exploitation. Disclosures without such scenario, with unrealistic assumptions or without meaningful outcomes will not be eligible for awards.
4. You must be the first person to responsibly disclose the issue to be eligible for awards.
<br>
## Development documentation awards
For significant changes to documentation, create an issue before making said changes. In such cases we will award documentation improvements in accordance with issue specification and PR awards.
For changes on the order of something like a typo, we'll award a nominal amount at our discretion.
<br>
## Helpfulness awards
Like issue specification awards, helping fellow contributors substantially in a well documented manner such that the helped fellow contributes a merged PR is eligible for a one-time relative reward.
| circumstances | award |
| -------------------------------------------------------------------------------------- | ----- |
| substantial and singular source of help | 10% |
| substantial but nonsingular source of help | 1-5% |
| source of relatively trivial help | 1% |
<br>
# Contribution extras
We want to make contributing to SN as rewarding as possible, so we offer a few extras to contributors.
## Dev chat
We self-host a private chat server for contributors to SN. If you'd like to join, please respond in this [discussion](https://github.com/stackernews/stacker.news/discussions/1059).
## Triage permissions
We offer triage permissions to contributors after they've made a few contributions. I'll usually add them as I notice people contributing, but if I missed you and you'd like to be added, let me know!
## Contributor badges on SN profiles
Contributors can get badges on their SN profiles by opening a pull request adding their SN nym to the [contributors.txt](/contributors.txt) file.
## What else you got
In the future we plan to offer more, like gratis github copilot subscriptions, reverse tunnels, codespaces, and merch.
If you'd like to see something added, please make a suggestion.
<br>
# Development Tips
<br>
## Linting
We use [JavaScript Standard Style](https://standardjs.com/) to enforce code style and correctness. You should run `sndev lint` before submitting a PR.
If you're using VSCode, you can install the [StandardJS VSCode Extension](https://marketplace.visualstudio.com/items?itemName=standard.vscode-standard) extension to get linting in your editor. We also recommend installing [StandardJS code snippets](https://marketplace.visualstudio.com/items?itemName=capaj.vscode-standardjs-snippets) and [StandardJS react code snippets](https://marketplace.visualstudio.com/items?itemName=TimonVS.ReactSnippetsStandard) for code snippets.
<br>
## Database migrations
We use [prisma](https://www.prisma.io/) for our database migrations. To create a new migration, modify `prisma/schema.prisma` according to [prisma schema reference](https://www.prisma.io/docs/orm/reference/prisma-schema-reference) and apply it with:
`./sndev prisma migrate dev`
If you want to create a migration without applying it, eg to create a trigger or modify the generated sql before applying, use the `--create-only` option:
`./sndev prisma migrate dev --create-only`
Generate the local copy of the prisma ORM client in `node_modules` after changes. This should only be needed to get Intellisense in your editor locally.
`./sndev prisma generate`
<br>
## Connecting to the local database
You can connect to the local database via `./sndev psql`. [psql](https://www.postgresql.org/docs/13/app-psql.html) is an interactive terminal for working with PostgreSQL.
<br>
## Running cli on local lightning nodes
You can run `lncli` on the local lnd nodes via `./sndev cli lnd` and `./sndev cli sn_lnd`. The node for your local SN instance is `sn_lnd` and the node serving as any external node, like a stacker's node or external wallet, is `lnd`.
You can run `lightning-cli` on the local cln node via `./sndev cli cln` which serves as an external node or wallet.
<br>
## Testing local auth
You can login to test features like posting, replying, tipping, etc with `./sndev login <nym>` which will provide a link to login as an existing nym or a new account for a nonexistent nym. But, it you want to test auth specifically you'll need to configure them in your `.env` file.
### Login with Email
#### MailHog
- The app is already prepared to send emails through [MailHog](https://github.com/mailhog/MailHog) so no extra configuration is needed
- Click "sign up" and enter any email address (remember, it's not going anywhere beyond your workstation)
- Access MailHog's web UI on http://localhost:8025
- Click the link (looks like this):
```
http://localhost:3000/api/auth/callback/email?email=satoshi%40gmail.com&token=110e30a954ce7ca643379d90eb511640733de405f34a31b38eeda8e254d48cd7
```
#### Sendgrid
- Create a Sendgrid account (or other smtp service)
```
LOGIN_EMAIL_SERVER=smtp://apikey:<sendgrid_api_key>@smtp.sendgrid.net:587
LOGIN_EMAIL_FROM=<sendgrid_email_from>
```
- Click "sign up" and enter your email address
- Check your email
- Click the link (looks like this):
```
http://localhost:3000/api/auth/callback/email?email=satoshi%40gmail.com&token=110e30a954ce7ca643379d90eb511640733de405f34a31b38eeda8e254d48cd7
```
### Login with Github
- [Create a new OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) in your Github account
- Set the callback URL to: `http://localhost:3000/api/auth/callback/github`
- Update your `.env` file
```
GITHUB_ID=<Client ID>
GITHUB_SECRET=<Client secret>
```
- Signup and login as above
### Login with Lightning
- Use [ngrok](https://ngrok.com/) to create a HTTPS tunnel to localhost:3000
- Update `LNAUTH_URL` in `.env` with the URL provided by `ngrok` and add /api/lnauth to it
<br>
## Enabling web push notifications
To enable Web Push locally, you will need to set the `VAPID_*` env vars. `VAPID_MAILTO` needs to be an email address using the `mailto:` scheme. For `NEXT_PUBLIC_VAPID_PUBKEY` and `VAPID_PRIVKEY`, you can run `npx web-push generate-vapid-keys`.
<br>
# Internals
<br>
## Stack
The site is written in javascript (not typescript 😱) using [Next.js](https://nextjs.org/), a [React](https://react.dev/) framework. The backend API is provided via [GraphQL](https://graphql.org/). The database is [PostgreSQL](https://www.postgresql.org/) modeled with [Prisma](https://www.prisma.io/). The [job queue](https://github.com/timgit/pg-boss) is also maintained in PostgreSQL. We use [lnd](https://github.com/lightningnetwork/lnd) for our lightning node. A customized [Bootstrap](https://react-bootstrap.netlify.app/) theme is used for styling.
<br>
## Services
Currently, SN runs and maintains two significant services and one microservice:
1. the nextjs web app, found in `./`
2. the worker service, found in `./worker`, which runs periodic jobs and jobs sent to it by the web app
3. a screenshot microservice, found in `./capture`, which takes screenshots of SN for social previews
In addition, we run other critical services the above services interact with like `lnd`, `postgres`, `opensearch`, `tor`, and `s3`.
<br>
## Wallet transaction safety
To ensure stackers balances are kept sane, some wallet updates are run in [serializable transactions](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-SERIALIZABLE) at the database level. Because early versions of prisma had relatively poor support for transactions most wallet touching code is written in [plpgsql](https://www.postgresql.org/docs/current/plpgsql.html) stored procedures and can be found in the `prisma/migrations` folder.
*UPDATE*: Most wallet updates are now run in [read committed](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED) transactions. See `api/paidAction/README.md` for more information.
<br>
# Need help?
Open a [discussion](http://github.com/stackernews/stacker.news/discussions) or [issue](http://github.com/stackernews/stacker.news/issues/new) or [email us](mailto:kk@stacker.news) or request joining the [dev chat](#dev-chat).
<br>
# Responsible disclosure
If you found a vulnerability, we would greatly appreciate it if you contact us via [security@stacker.news](mailto:security@stacker.news) or open a [security advisory](https://github.com/stackernews/stacker.news/security/advisories/new). Our PGP key can be found [here](https://stacker.news/pgp.txt) (EBAF 75DA 7279 CB48).
<br>
# License
[MIT](https://choosealicense.com/licenses/mit/)

View File

@ -1,13 +1,20 @@
import lndService from 'ln-service'
import { cachedFetcher } from '@/lib/fetch'
import { toPositiveNumber } from '@/lib/format'
import { authenticatedLndGrpc } from '@/lib/lnd'
import { getIdentity, getHeight, getWalletInfo, getNode, getPayment, parsePaymentRequest } from 'ln-service'
import { datePivot } from '@/lib/time'
import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
const { lnd } = lndService.authenticatedLndGrpc({
const lnd = global.lnd || authenticatedLndGrpc({
cert: process.env.LND_CERT,
macaroon: process.env.LND_MACAROON,
socket: process.env.LND_SOCKET
})
}).lnd
if (process.env.NODE_ENV === 'development') global.lnd = lnd
// Check LND GRPC connection
lndService.getWalletInfo({ lnd }, (err, result) => {
getWalletInfo({ lnd }, (err, result) => {
if (err) {
console.error('LND GRPC connection error')
return
@ -15,4 +22,181 @@ lndService.getWalletInfo({ lnd }, (err, result) => {
console.log('LND GRPC connection successful')
})
export async function estimateRouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) {
// if the payment request includes us as route hint, we needd to use the destination and amount
// otherwise, this will fail with a self-payment error
if (request) {
const inv = parsePaymentRequest({ request })
const ourPubkey = await getOurPubkey({ lnd })
if (Array.isArray(inv.routes)) {
for (const route of inv.routes) {
if (Array.isArray(route)) {
for (const hop of route) {
if (hop.public_key === ourPubkey) {
console.log('estimateRouteFee ignoring self-payment route')
request = false
break
}
}
}
}
}
}
return await new Promise((resolve, reject) => {
const params = {}
if (request) {
console.log('estimateRouteFee using payment request')
params.payment_request = request
} else {
console.log('estimateRouteFee using destination and amount')
params.dest = Buffer.from(destination, 'hex')
params.amt_sat = tokens ? toPositiveNumber(tokens) : toPositiveNumber(BigInt(mtokens) / BigInt(1e3))
}
lnd.router.estimateRouteFee({
...params,
timeout
}, (err, res) => {
if (err) {
if (res?.failure_reason) {
reject(new Error(`Unable to estimate route: ${res.failure_reason}`))
} else {
reject(err)
}
return
}
if (res.routing_fee_msat < 0 || res.time_lock_delay <= 0) {
reject(new Error('Unable to estimate route, excessive values: ' + JSON.stringify(res)))
return
}
resolve({
routingFeeMsat: toPositiveNumber(res.routing_fee_msat),
timeLockDelay: toPositiveNumber(res.time_lock_delay)
})
})
})
}
// created_height is the accepted_height, timeout is the expiry height
// ln-service remaps the `htlcs` field of lookupInvoice to `payments` and
// see: https://github.com/alexbosworth/lightning/blob/master/lnd_responses/htlc_as_payment.js
// and: https://lightning.engineering/api-docs/api/lnd/lightning/lookup-invoice/index.html#lnrpcinvoicehtlc
export function hodlInvoiceCltvDetails (inv) {
if (!inv.payments) {
throw new Error('No payments found')
}
if (!inv.is_held) {
throw new Error('Invoice is not held')
}
const acceptHeight = inv.payments.reduce((max, htlc) => {
const createdHeight = toPositiveNumber(htlc.created_height)
return createdHeight > max ? createdHeight : max
}, 0)
const expiryHeight = inv.payments.reduce((min, htlc) => {
const timeout = toPositiveNumber(htlc.timeout)
return timeout < min ? timeout : min
}, Number.MAX_SAFE_INTEGER)
return {
expiryHeight: toPositiveNumber(expiryHeight),
acceptHeight: toPositiveNumber(acceptHeight)
}
}
export function getPaymentFailureStatus (withdrawal) {
if (withdrawal && !withdrawal.is_failed) {
throw new Error('withdrawal is not failed')
}
if (withdrawal?.failed?.is_insufficient_balance) {
return {
status: 'INSUFFICIENT_BALANCE',
message: 'you didn\'t have enough sats'
}
} else if (withdrawal?.failed?.is_invalid_payment) {
return {
status: 'INVALID_PAYMENT',
message: 'invalid payment'
}
} else if (withdrawal?.failed?.is_pathfinding_timeout) {
return {
status: 'PATHFINDING_TIMEOUT',
message: 'no route found'
}
} else if (withdrawal?.failed?.is_route_not_found) {
return {
status: 'ROUTE_NOT_FOUND',
message: 'no route found'
}
}
return {
status: 'UNKNOWN_FAILURE',
message: 'unknown failure'
}
}
export const getBlockHeight = cachedFetcher(async function fetchBlockHeight ({ lnd, ...args }) {
try {
const { current_block_height: height } = await getHeight({ lnd, ...args })
return height
} catch (err) {
throw new Error(`Unable to fetch block height: ${err.message}`)
}
}, {
maxSize: 1,
cacheExpiry: 60 * 1000, // 1 minute
forceRefreshThreshold: 5 * 60 * 1000, // 5 minutes
keyGenerator: () => 'getHeight'
})
export const getOurPubkey = cachedFetcher(async function fetchOurPubkey ({ lnd, ...args }) {
try {
const identity = await getIdentity({ lnd, ...args })
return identity.public_key
} catch (err) {
throw new Error(`Unable to fetch identity: ${err.message}`)
}
}, {
maxSize: 1,
cacheExpiry: 0, // never expire
forceRefreshThreshold: 0, // never force refresh
keyGenerator: () => 'getOurPubkey'
})
export const getNodeSockets = cachedFetcher(async function fetchNodeSockets ({ lnd, ...args }) {
try {
return (await getNode({ lnd, is_omitting_channels: true, ...args }))?.sockets
} catch (err) {
throw new Error(`Unable to fetch node info: ${err.message}`)
}
}, {
maxSize: 100,
cacheExpiry: 1000 * 60 * 60 * 24, // 1 day
forceRefreshThreshold: 1000 * 60 * 60 * 24 * 7, // 1 week
keyGenerator: (args) => {
const { public_key: publicKey } = args
return publicKey
}
})
export async function getPaymentOrNotSent ({ id, lnd, createdAt }) {
try {
return await getPayment({ id, lnd })
} catch (err) {
if (err[1] === 'SentPaymentNotFound' &&
createdAt < datePivot(new Date(), { milliseconds: -LND_PATHFINDING_TIMEOUT_MS * 2 })) {
// if the payment is older than 2x timeout, but not found in LND, we can assume it errored before lnd stored it
return { notSent: true, is_failed: true }
} else {
throw err
}
}
}
export default lnd

View File

@ -1,18 +1,12 @@
import { PrismaClient } from '@prisma/client'
import createPrisma from '@/lib/create-prisma'
const prisma = global.prisma || (() => {
console.log('initing prisma')
const prisma = new PrismaClient({
log: [{ level: 'query', emit: 'event' }, 'warn', 'error']
})
prisma.$on('query', (e) => {
if (e.duration > 50) {
console.log('Query: ' + e.query)
console.log('Params: ' + e.params)
console.log('Duration: ' + e.duration + 'ms')
return createPrisma({
connectionParams: {
connection_limit: process.env.DB_APP_CONNECTION_LIMIT
}
})
return prisma
})()
if (process.env.NODE_ENV === 'development') global.prisma = prisma

View File

@ -1,3 +0,0 @@
{
"type": "module"
}

371
api/paidAction/README.md Normal file
View File

@ -0,0 +1,371 @@
# Paid Actions
Paid actions are actions that require payments to perform. Given that we support several payment flows, some of which require more than one round of communication either with LND or the client, and several paid actions, we have this plugin-like interface to easily add new paid actions.
<details>
<summary>internals</summary>
All paid action progress, regardless of flow, is managed using a state machine that's transitioned by the invoice progress and payment progress (in the case of p2p paid action). Below is the full state machine for paid actions:
```mermaid
stateDiagram-v2
[*] --> PENDING
PENDING --> PAID
PENDING --> CANCELING
PENDING --> FAILED
PAID --> [*]
CANCELING --> FAILED
FAILED --> RETRYING
FAILED --> [*]
RETRYING --> [*]
[*] --> PENDING_HELD
PENDING_HELD --> HELD
PENDING_HELD --> FORWARDING
PENDING_HELD --> CANCELING
PENDING_HELD --> FAILED
HELD --> PAID
HELD --> CANCELING
HELD --> FAILED
FORWARDING --> FORWARDED
FORWARDING --> FAILED_FORWARD
FORWARDED --> PAID
FAILED_FORWARD --> CANCELING
FAILED_FORWARD --> FAILED
```
</details>
## Payment Flows
There are three payment flows:
### Fee credits
The stacker has enough fee credits to pay for the action. This is the simplest flow and is similar to a normal request.
### Optimistic
The optimistic flow is useful for actions that require immediate feedback to the client, but don't require the action to be immediately visible to everyone else.
For paid actions that support it, if the stacker doesn't have enough fee credits, we store the action in a `PENDING` state on the server, which is visible only to the stacker, then return a payment request to the client. The client then pays the invoice however and whenever they wish, and the server monitors payment progress. If the payment succeeds, the action is executed fully becoming visible to everyone and is marked as `PAID`. Otherwise, the action is marked as `FAILED`, the client is notified the payment failed and the payment can be retried.
<details>
<summary>Internals</summary>
Internally, optimistic flows make use of a state machine that's transitioned by the invoice payment progress.
```mermaid
stateDiagram-v2
[*] --> PENDING
PENDING --> PAID
PENDING --> CANCELING
PENDING --> FAILED
PAID --> [*]
CANCELING --> FAILED
FAILED --> RETRYING
FAILED --> [*]
RETRYING --> [*]
```
</details>
### Pessimistic
For paid actions that don't support optimistic actions (or when the stacker is `@anon`), if the client doesn't have enough fee credits, we return a payment request to the client without performing the action and only storing the action's arguments. After the client pays the invoice, the server performs the action with original arguments. Pessimistic actions require the payment to complete before being visible to them and everyone else.
Internally, pessimistic flows use hold invoices. If the action doesn't succeed, the payment is cancelled and it's as if the payment never happened (ie it's a lightning native refund mechanism).
<details>
<summary>Internals</summary>
Internally, pessimistic flows make use of a state machine that's transitioned by the invoice payment progress much like optimistic flows, but with extra steps.
```mermaid
stateDiagram-v2
PAID --> [*]
CANCELING --> FAILED
FAILED --> [*]
[*] --> PENDING_HELD
PENDING_HELD --> HELD
PENDING_HELD --> CANCELING
PENDING_HELD --> FAILED
HELD --> PAID
HELD --> CANCELING
HELD --> FAILED
```
</details>
### Table of existing paid actions and their supported flows
| action | fee credits | optimistic | pessimistic | anonable | qr payable | p2p wrapped | side effects | reward sats | p2p direct |
| ----------------- | ----------- | ---------- | ----------- | -------- | ---------- | ----------- | ------------ | ----------- | ---------- |
| zaps | x | x | x | x | x | x | x | | |
| posts | x | x | x | x | x | | x | x | |
| comments | x | x | x | x | x | | x | x | |
| downzaps | x | x | | | x | | x | x | |
| poll votes | x | x | | | x | | | x | |
| territory actions | x | | x | | x | | | x | |
| donations | x | | x | x | x | | | x | |
| update posts | x | | x | | x | | x | x | |
| update comments | x | | x | | x | | x | x | |
| receive | | x | | | x | x | x | | x |
| buy fee credits | | | x | | x | | | x | |
| invite gift | x | | | | | | x | x | |
## Not-custodial zaps (ie p2p wrapped payments)
Zaps, and possibly other future actions, can be performed peer to peer and non-custodially. This means that the payment is made directly from the client to the recipient, without the server taking custody of the funds. Currently, in order to trigger this behavior, the recipient must have a receiving wallet attached and the sender must have insufficient funds in their custodial wallet to perform the requested zap.
This works by requesting an invoice from the recipient's wallet and reusing the payment hash in a hold invoice paid to SN (to collect the sybil fee) which we serve to the sender. When the sender pays this wrapped invoice, we forward our own money to the recipient, who then reveals the preimage to us, allowing us to settle the wrapped invoice and claim the sender's funds. This effectively does what a lightning node does when forwarding a payment but allows us to do it at the application layer.
<details>
<summary>Internals</summary>
Internally, p2p wrapped payments make use of the same paid action state machine but it's transitioned by both the incoming invoice payment progress *and* the outgoing invoice payment progress.
```mermaid
stateDiagram-v2
PAID --> [*]
CANCELING --> FAILED
FAILED --> RETRYING
FAILED --> [*]
RETRYING --> [*]
[*] --> PENDING_HELD
PENDING_HELD --> FORWARDING
PENDING_HELD --> CANCELING
PENDING_HELD --> FAILED
FORWARDING --> FORWARDED
FORWARDING --> FAILED_FORWARD
FORWARDED --> PAID
FAILED_FORWARD --> CANCELING
FAILED_FORWARD --> FAILED
```
</details>
## Paid Action Interface
Each paid action is implemented in its own file in the `paidAction` directory. Each file exports a module with the following properties:
### Boolean flags
- `anonable`: can be performed anonymously
### Payment methods
- `paymentMethods`: an array of payment methods that the action supports ordered from most preferred to least preferred
- P2P: a p2p payment made directly from the client to the recipient
- after wrapping the invoice, anonymous users will follow a PESSIMISTIC flow to pay the invoice and logged in users will follow an OPTIMISTIC flow
- FEE_CREDIT: a payment made from the user's fee credit balance
- OPTIMISTIC: an optimistic payment flow
- PESSIMISTIC: a pessimistic payment flow
### Functions
All functions have the following signature: `function(args: Object, context: Object): Promise`
- `getCost`: returns the cost of the action in msats as a `BigInt`
- `perform`: performs the action
- returns: an object with the result of the action as defined in the `graphql` schema
- if the action supports optimism and an `invoiceId` is provided, the action should be performed optimistically
- any action data that needs to be hidden while it's pending, should store in its rows a `PENDING` state along with its `invoiceId`
- it can optionally store in the invoice with the `invoiceId` the `actionId` to be able to link the action with the invoice regardless of retries
- `onPaid`: called when the action is paid
- if the action does not support optimism, this function is optional
- this function should be used to mark the rows created in `perform` as `PAID` and perform critical side effects of the action (like denormalizations)
- `nonCriticalSideEffects`: called after the action is paid to run any side effects whose failure does not affect the action's execution
- this function is always optional
- it's passed the result of the action (or the action's paid invoice) and the current context
- this is where things like push notifications should be handled
- `onFail`: called when the action fails
- if the action does not support optimism, this function is optional
- this function should be used to mark the rows created in `perform` as `FAILED`
- `retry`: called when the action is retried with any new invoice information
- return: an object with the result of the action as defined in the `graphql` schema (same as `perform`)
- this function is called when an optimistic action is retried
- it's passed the original `invoiceId` and the `newInvoiceId`
- this function should update the rows created in `perform` to contain the new `newInvoiceId` and remark the row as `PENDING`
- `getInvoiceablePeer`: returns the userId of the peer that's capable of generating an invoice so they can be paid for the action
- this is only used for p2p wrapped zaps currently
- `describe`: returns a description as a string of the action
- for actions that require generating an invoice, and for stackers that don't hide invoice descriptions, this is used in the invoice description
- `getSybilFeePercent` (required if `getInvoiceablePeer` is implemented): returns the action sybil fee percent as a `BigInt` (eg. 30n for 30%)
#### Function arguments
`args` contains the arguments for the action as defined in the `graphql` schema. If the action is optimistic or pessimistic, `args` will contain an `invoiceId` field which can be stored alongside the paid action's data. If this is a call to `retry`, `args` will contain the original `invoiceId` and `newInvoiceId` fields.
`context` contains the following fields:
- `me`: the user performing the action (undefined if anonymous)
- `cost`: the cost of the action in msats as a `BigInt`
- `sybilFeePercent`: the sybil fee percent as a `BigInt` (eg. 30n for 30%)
- `tx`: the current transaction (for anything that needs to be done atomically with the payment)
- `models`: the current prisma client (for anything that doesn't need to be done atomically with the payment)
- `lnd`: the current lnd client
## Recording Cowboy Credits
To avoid adding sats and credits together everywhere to show an aggregate sat value, in most cases we denormalize a `sats` field that carries the "sats value", the combined sats + credits of something, and a `credits` field that carries only the earned `credits`. For example, the `Item` table has an `msats` field that carries the sum of the `mcredits` and `msats` earned and a `mcredits` field that carries the value of the `mcredits` earned. So, the sats value an item earned is `item.msats` BUT the real sats earned is `item.msats - item.mcredits`.
The ONLY exception to this are for the `users` table where we store a stacker's rewards sats and credits balances separately.
## `IMPORTANT: transaction isolation`
We use a `read committed` isolation level for actions. This means paid actions need to be mindful of concurrency issues. Specifically, reading data from the database and then writing it back in `read committed` is a common source of consistency bugs (aka serialization anamolies).
### This is a big deal
1. If you read from the database and intend to use that data to write to the database, and it's possible that a concurrent transaction could change the data you've read (it usually is), you need to be prepared to handle that.
2. This applies to **ALL**, and I really mean **ALL**, read data regardless of how you read the data within the `read committed` transaction:
- independent statements
- `WITH` queries (CTEs) in the same statement
- subqueries in the same statement
### How to handle it
1. take row level locks on the rows you read, using something like a `SELECT ... FOR UPDATE` statement
- NOTE: this does not protect against missing concurrent inserts. It only prevents concurrent updates to the rows you've already read.
- read about row level locks available in postgres: https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS
2. check that the data you read is still valid before writing it back to the database i.e. optimistic concurrency control
- NOTE: this does not protect against missing concurrent inserts. It only prevents concurrent updates to the rows you've already read.
3. avoid having to read data from one row to modify the data of another row all together
### Example
Let's say you are aggregating total sats for an item from a table `zaps` and updating the total sats for that item in another table `item_zaps`. Two 100 sat zaps are requested for the same item at the same time in two concurrent transactions. The total sats for the item should be 200, but because of the way `read committed` works, the following statements lead to a total sats of 100:
*the statements here are listed in the order they are executed, but each transaction is happening concurrently*
#### Incorrect
```sql
-- transaction 1
BEGIN;
INSERT INTO zaps (item_id, sats) VALUES (1, 100);
SELECT sum(sats) INTO total_sats FROM zaps WHERE item_id = 1;
-- total_sats is 100
-- transaction 2
BEGIN;
INSERT INTO zaps (item_id, sats) VALUES (1, 100);
SELECT sum(sats) INTO total_sats FROM zaps WHERE item_id = 1;
-- total_sats is still 100, because transaction 1 hasn't committed yet
-- transaction 1
UPDATE item_zaps SET sats = total_sats WHERE item_id = 1;
-- sets sats to 100
-- transaction 2
UPDATE item_zaps SET sats = total_sats WHERE item_id = 1;
-- sets sats to 100
COMMIT;
-- transaction 1
COMMIT;
-- item_zaps.sats is 100, but we would expect it to be 200
```
Note that row level locks wouldn't help in this case, because we can't lock the rows that the transactions don't know to exist yet.
#### Subqueries are still incorrect
```sql
-- transaction 1
BEGIN;
INSERT INTO zaps (item_id, sats) VALUES (1, 100);
UPDATE item_zaps SET sats = (SELECT sum(sats) INTO total_sats FROM zaps WHERE item_id = 1) WHERE item_id = 1;
-- item_zaps.sats is 100
-- transaction 2
BEGIN;
INSERT INTO zaps (item_id, sats) VALUES (1, 100);
UPDATE item_zaps SET sats = (SELECT sum(sats) INTO total_sats FROM zaps WHERE item_id = 1) WHERE item_id = 1;
-- item_zaps.sats is still 100, because transaction 1 hasn't committed yet
-- transaction 1
COMMIT;
-- transaction 2
COMMIT;
-- item_zaps.sats is 100, but we would expect it to be 200
```
Note that while the `UPDATE` transaction 2's update statement will block until transaction 1 commits, the subquery is computed before it blocks and is not re-evaluated after the block.
#### Correct
```sql
-- transaction 1
BEGIN;
INSERT INTO zaps (item_id, sats) VALUES (1, 100);
-- transaction 2
BEGIN;
INSERT INTO zaps (item_id, sats) VALUES (1, 100);
-- transaction 1
UPDATE item_zaps SET sats = sats + 100 WHERE item_id = 1;
-- transaction 2
UPDATE item_zaps SET sats = sats + 100 WHERE item_id = 1;
COMMIT;
-- transaction 1
COMMIT;
-- item_zaps.sats is 200
```
The above works because `UPDATE` takes a lock on the rows it's updating, so transaction 2 will block until transaction 1 commits, and once transaction 2 is unblocked, it will re-evaluate the `sats` value of the row it's updating.
#### More resources
- https://stackoverflow.com/questions/61781595/postgres-read-commited-doesnt-re-read-updated-row?noredirect=1#comment109279507_61781595
- https://www.cybertec-postgresql.com/en/transaction-anomalies-with-select-for-update/
From the [postgres docs](https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED):
> UPDATE, DELETE, SELECT FOR UPDATE, and SELECT FOR SHARE commands behave the same as SELECT in terms of searching for target rows: they will only find target rows that were committed as of the command start time. However, such a target row might have already been updated (or deleted or locked) by another concurrent transaction by the time it is found. In this case, the would-be updater will wait for the first updating transaction to commit or roll back (if it is still in progress). If the first updater rolls back, then its effects are negated and the second updater can proceed with updating the originally found row. If the first updater commits, the second updater will ignore the row if the first updater deleted it, otherwise it will attempt to apply its operation to the updated version of the row. The search condition of the command (the WHERE clause) is re-evaluated to see if the updated version of the row still matches the search condition. If so, the second updater proceeds with its operation using the updated version of the row. In the case of SELECT FOR UPDATE and SELECT FOR SHARE, this means it is the updated version of the row that is locked and returned to the client.
From the [postgres source docs](https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/executor/README#l350):
> It is also possible that there are relations in the query that are not to be locked (they are neither the UPDATE/DELETE/MERGE target nor specified to be locked in SELECT FOR UPDATE/SHARE). When re-running the test query ***we want to use the same rows*** from these relations that were joined to the locked rows.
## `IMPORTANT: deadlocks`
Deadlocks can occur when two transactions are waiting for each other to release locks. This can happen when two transactions lock rows in different orders whether explicit or implicit.
If both transactions lock the rows in the same order, the deadlock is avoided.
### Incorrect
```sql
-- transaction 1
BEGIN;
UPDATE users set msats = msats + 1 WHERE id = 1;
-- transaction 2
BEGIN;
UPDATE users set msats = msats + 1 WHERE id = 2;
-- transaction 1 (blocks here until transaction 2 commits)
UPDATE users set msats = msats + 1 WHERE id = 2;
-- transaction 2 (blocks here until transaction 1 commits)
UPDATE users set msats = msats + 1 WHERE id = 1;
-- deadlock occurs because neither transaction can proceed to here
```
In practice, this most often occurs when selecting multiple rows for update in different orders. Recently, we had a deadlock when spliting zaps to multiple users. The solution was to select the rows for update in the same order.
### Incorrect
```sql
WITH forwardees AS (
SELECT "userId", (($1::BIGINT * pct) / 100)::BIGINT AS msats
FROM "ItemForward"
WHERE "itemId" = $2::INTEGER
),
UPDATE users
SET
msats = users.msats + forwardees.msats,
"stackedMsats" = users."stackedMsats" + forwardees.msats
FROM forwardees
WHERE users.id = forwardees."userId";
```
If forwardees are selected in a different order in two concurrent transactions, e.g. (1,2) in tx 1 and (2,1) in tx 2, a deadlock can occur. To avoid this, always select rows for update in the same order.
### Correct
We fixed the deadlock by selecting the forwardees in the same order in these transactions.
```sql
WITH forwardees AS (
SELECT "userId", (($1::BIGINT * pct) / 100)::BIGINT AS msats
FROM "ItemForward"
WHERE "itemId" = $2::INTEGER
ORDER BY "userId" ASC
),
UPDATE users
SET
msats = users.msats + forwardees.msats,
"stackedMsats" = users."stackedMsats" + forwardees.msats
FROM forwardees
WHERE users.id = forwardees."userId";
```
### More resources
- https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-DEADLOCKS

82
api/paidAction/boost.js Normal file
View File

@ -0,0 +1,82 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)
}
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) {
itemId = parseInt(itemId)
let invoiceData = {}
if (invoiceId) {
invoiceData = { invoiceId, invoiceActionState: 'PENDING' }
// store a reference to the item in the invoice
await tx.invoice.update({
where: { id: invoiceId },
data: { actionId: itemId }
})
}
const act = await tx.itemAct.create({ data: { msats: cost, itemId, userId: me.id, act: 'BOOST', ...invoiceData } })
const [{ path }] = await tx.$queryRaw`
SELECT ltree2text(path) as path FROM "Item" WHERE id = ${itemId}::INTEGER`
return { id: itemId, sats, act: 'BOOST', path, actId: act.id }
}
export async function retry ({ invoiceId, newInvoiceId }, { tx, cost }) {
await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
const [{ id, path }] = await tx.$queryRaw`
SELECT "Item".id, ltree2text(path) as path
FROM "Item"
JOIN "ItemAct" ON "Item".id = "ItemAct"."itemId"
WHERE "ItemAct"."invoiceId" = ${newInvoiceId}::INTEGER`
return { id, sats: msatsToSats(cost), act: 'BOOST', path }
}
export async function onPaid ({ invoice, actId }, { tx }) {
let itemAct
if (invoice) {
await tx.itemAct.updateMany({
where: { invoiceId: invoice.id },
data: {
invoiceActionState: 'PAID'
}
})
itemAct = await tx.itemAct.findFirst({ where: { invoiceId: invoice.id } })
} else if (actId) {
itemAct = await tx.itemAct.findFirst({ where: { id: actId } })
} else {
throw new Error('No invoice or actId')
}
// increment boost on item
await tx.item.update({
where: { id: itemAct.itemId },
data: {
boost: { increment: msatsToSats(itemAct.msats) }
}
})
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('expireBoost', jsonb_build_object('id', ${itemAct.itemId}::INTEGER), 21, true,
now() + interval '30 days', now() + interval '40 days')`
}
export async function onFail ({ invoice }, { tx }) {
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
}
export async function describe ({ id: itemId, sats }, { actionId, cost }) {
return `SN: boost ${sats ?? msatsToSats(cost)} sats to #${itemId ?? actionId}`
}

View File

@ -0,0 +1,32 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ credits }) {
return satsToMsats(credits)
}
export async function perform ({ credits }, { me, cost, tx }) {
await tx.user.update({
where: { id: me.id },
data: {
mcredits: {
increment: cost
}
}
})
return {
credits
}
}
export async function describe () {
return 'SN: buy fee credits'
}

29
api/paidAction/donate.js Normal file
View File

@ -0,0 +1,29 @@
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
export const anonable = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)
}
export async function perform ({ sats }, { me, tx }) {
await tx.donation.create({
data: {
sats,
userId: me?.id ?? USER_ID.anon
}
})
return { sats }
}
export async function describe (args, context) {
return 'SN: donate to rewards pool'
}

100
api/paidAction/downZap.js Normal file
View File

@ -0,0 +1,100 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
import { Prisma } from '@prisma/client'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)
}
export async function perform ({ invoiceId, sats, id: itemId }, { me, cost, tx }) {
itemId = parseInt(itemId)
let invoiceData = {}
if (invoiceId) {
invoiceData = { invoiceId, invoiceActionState: 'PENDING' }
// store a reference to the item in the invoice
await tx.invoice.update({
where: { id: invoiceId },
data: { actionId: itemId }
})
}
const itemAct = await tx.itemAct.create({
data: { msats: cost, itemId, userId: me.id, act: 'DONT_LIKE_THIS', ...invoiceData }
})
const [{ path }] = await tx.$queryRaw`SELECT ltree2text(path) as path FROM "Item" WHERE id = ${itemId}::INTEGER`
return { id: itemId, sats, act: 'DONT_LIKE_THIS', path, actId: itemAct.id }
}
export async function retry ({ invoiceId, newInvoiceId }, { tx, cost }) {
await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
const [{ id, path }] = await tx.$queryRaw`
SELECT "Item".id, ltree2text(path) as path
FROM "Item"
JOIN "ItemAct" ON "Item".id = "ItemAct"."itemId"
WHERE "ItemAct"."invoiceId" = ${newInvoiceId}::INTEGER`
return { id, sats: msatsToSats(cost), act: 'DONT_LIKE_THIS', path }
}
export async function onPaid ({ invoice, actId }, { tx }) {
let itemAct
if (invoice) {
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID' } })
itemAct = await tx.itemAct.findFirst({ where: { invoiceId: invoice.id }, include: { item: true } })
} else if (actId) {
itemAct = await tx.itemAct.findUnique({ where: { id: actId }, include: { item: true } })
} else {
throw new Error('No invoice or actId')
}
const msats = BigInt(itemAct.msats)
const sats = msatsToSats(msats)
// denormalize downzaps
await tx.$executeRaw`
WITH territory AS (
SELECT COALESCE(r."subName", i."subName", 'meta')::TEXT as "subName"
FROM "Item" i
LEFT JOIN "Item" r ON r.id = i."rootId"
WHERE i.id = ${itemAct.itemId}::INTEGER
), zapper AS (
SELECT
COALESCE(${itemAct.item.parentId
? Prisma.sql`"zapCommentTrust"`
: Prisma.sql`"zapPostTrust"`}, 0) as "zapTrust",
COALESCE(${itemAct.item.parentId
? Prisma.sql`"subZapCommentTrust"`
: Prisma.sql`"subZapPostTrust"`}, 0) as "subZapTrust"
FROM territory
LEFT JOIN "UserSubTrust" ust ON ust."subName" = territory."subName"
AND ust."userId" = ${itemAct.userId}::INTEGER
), zap AS (
INSERT INTO "ItemUserAgg" ("userId", "itemId", "downZapSats")
VALUES (${itemAct.userId}::INTEGER, ${itemAct.itemId}::INTEGER, ${sats}::INTEGER)
ON CONFLICT ("itemId", "userId") DO UPDATE
SET "downZapSats" = "ItemUserAgg"."downZapSats" + ${sats}::INTEGER, updated_at = now()
RETURNING LOG("downZapSats" / GREATEST("downZapSats" - ${sats}::INTEGER, 1)::FLOAT) AS log_sats
)
UPDATE "Item"
SET "weightedDownVotes" = "weightedDownVotes" + zapper."zapTrust" * zap.log_sats,
"subWeightedDownVotes" = "subWeightedDownVotes" + zapper."subZapTrust" * zap.log_sats
FROM zap, zapper
WHERE "Item".id = ${itemAct.itemId}::INTEGER`
}
export async function onFail ({ invoice }, { tx }) {
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
}
export async function describe ({ id: itemId, sats }, { cost, actionId }) {
return `SN: downzap of ${sats ?? msatsToSats(cost)} sats to #${itemId ?? actionId}`
}

496
api/paidAction/index.js Normal file
View File

@ -0,0 +1,496 @@
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
import { datePivot } from '@/lib/time'
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { createHmac } from '@/api/resolvers/wallet'
import { Prisma } from '@prisma/client'
import { createWrappedInvoice, createUserInvoice } from '@/wallets/server'
import { assertBelowMaxPendingInvoices, assertBelowMaxPendingDirectPayments } from './lib/assert'
import * as ITEM_CREATE from './itemCreate'
import * as ITEM_UPDATE from './itemUpdate'
import * as ZAP from './zap'
import * as DOWN_ZAP from './downZap'
import * as POLL_VOTE from './pollVote'
import * as TERRITORY_CREATE from './territoryCreate'
import * as TERRITORY_UPDATE from './territoryUpdate'
import * as TERRITORY_BILLING from './territoryBilling'
import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
import * as DONATE from './donate'
import * as BOOST from './boost'
import * as RECEIVE from './receive'
import * as BUY_CREDITS from './buyCredits'
import * as INVITE_GIFT from './inviteGift'
export const paidActions = {
ITEM_CREATE,
ITEM_UPDATE,
ZAP,
DOWN_ZAP,
BOOST,
POLL_VOTE,
TERRITORY_CREATE,
TERRITORY_UPDATE,
TERRITORY_BILLING,
TERRITORY_UNARCHIVE,
DONATE,
RECEIVE,
BUY_CREDITS,
INVITE_GIFT
}
export default async function performPaidAction (actionType, args, incomingContext) {
try {
const { me, models, forcePaymentMethod } = incomingContext
const paidAction = paidActions[actionType]
console.group('performPaidAction', actionType, args)
if (!paidAction) {
throw new Error(`Invalid action type ${actionType}`)
}
if (!me && !paidAction.anonable) {
throw new Error('You must be logged in to perform this action')
}
// treat context as immutable
const contextWithMe = {
...incomingContext,
me: me ? await models.user.findUnique({ where: { id: parseInt(me.id) } }) : undefined
}
const context = {
...contextWithMe,
cost: await paidAction.getCost(args, contextWithMe),
sybilFeePercent: await paidAction.getSybilFeePercent?.(args, contextWithMe)
}
// special case for zero cost actions
if (context.cost === 0n) {
console.log('performing zero cost action')
return await performNoInvoiceAction(actionType, args, { ...context, paymentMethod: 'ZERO_COST' })
}
for (const paymentMethod of paidAction.paymentMethods) {
console.log(`considering payment method ${paymentMethod}`)
const contextWithPaymentMethod = { ...context, paymentMethod }
if (forcePaymentMethod &&
paymentMethod !== forcePaymentMethod) {
console.log('skipping payment method', paymentMethod, 'because forcePaymentMethod is set to', forcePaymentMethod)
continue
}
// payment methods that anonymous users can use
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P) {
try {
return await performP2PAction(actionType, args, contextWithPaymentMethod)
} catch (e) {
if (e instanceof NonInvoiceablePeerError) {
console.log('peer cannot be invoiced, skipping')
continue
}
console.error(`${paymentMethod} action failed`, e)
throw e
}
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC) {
return await beginPessimisticAction(actionType, args, contextWithPaymentMethod)
}
// additional payment methods that logged in users can use
if (me) {
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT ||
paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
try {
return await performNoInvoiceAction(actionType, args, contextWithPaymentMethod)
} catch (e) {
// if we fail with fee credits or reward sats, but not because of insufficient funds, bail
console.error(`${paymentMethod} action failed`, e)
if (!e.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"') &&
!e.message.includes('\\"users\\" violates check constraint \\"mcredits_positive\\"')) {
throw e
}
}
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC) {
return await performOptimisticAction(actionType, args, contextWithPaymentMethod)
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT) {
try {
return await performDirectAction(actionType, args, contextWithPaymentMethod)
} catch (e) {
if (e instanceof NonInvoiceablePeerError) {
console.log('peer cannot be invoiced, skipping')
continue
}
console.error(`${paymentMethod} action failed`, e)
throw e
}
}
}
}
throw new Error('No working payment method found')
} catch (e) {
console.error('performPaidAction failed', e)
throw e
} finally {
console.groupEnd()
}
}
async function performNoInvoiceAction (actionType, args, incomingContext) {
const { me, models, cost, paymentMethod } = incomingContext
const action = paidActions[actionType]
const result = await models.$transaction(async tx => {
const context = { ...incomingContext, tx }
if (paymentMethod === 'FEE_CREDIT') {
await tx.user.update({
where: {
id: me?.id ?? USER_ID.anon
},
data: { mcredits: { decrement: cost } }
})
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
await tx.user.update({
where: {
id: me?.id ?? USER_ID.anon
},
data: { msats: { decrement: cost } }
})
}
const result = await action.perform(args, context)
await action.onPaid?.(result, context)
return {
result,
paymentMethod
}
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
// run non critical side effects in the background
// after the transaction has been committed
action.nonCriticalSideEffects?.(result.result, incomingContext).catch(console.error)
return result
}
async function performOptimisticAction (actionType, args, incomingContext) {
const { models, invoiceArgs: incomingInvoiceArgs } = incomingContext
const action = paidActions[actionType]
const optimisticContext = { ...incomingContext, optimistic: true }
const invoiceArgs = incomingInvoiceArgs ?? await createSNInvoice(actionType, args, optimisticContext)
return await models.$transaction(async tx => {
const context = { ...optimisticContext, tx, invoiceArgs }
const invoice = await createDbInvoice(actionType, args, context)
return {
invoice,
result: await action.perform?.({ invoiceId: invoice.id, ...args }, context),
paymentMethod: 'OPTIMISTIC'
}
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
}
async function beginPessimisticAction (actionType, args, context) {
const action = paidActions[actionType]
if (!action.paymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC)) {
throw new Error(`This action ${actionType} does not support pessimistic invoicing`)
}
// just create the invoice and complete action when it's paid
const invoiceArgs = context.invoiceArgs ?? await createSNInvoice(actionType, args, context)
return {
invoice: await createDbInvoice(actionType, args, { ...context, invoiceArgs }),
paymentMethod: 'PESSIMISTIC'
}
}
async function performP2PAction (actionType, args, incomingContext) {
// if the action has an invoiceable peer, we'll create a peer invoice
// wrap it, and return the wrapped invoice
const { cost, sybilFeePercent, models, lnd, me } = incomingContext
if (!sybilFeePercent) {
throw new Error('sybil fee percent is not set for an invoiceable peer action')
}
const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, incomingContext)
if (!userId) {
throw new NonInvoiceablePeerError()
}
let context
try {
await assertBelowMaxPendingInvoices(incomingContext)
const description = await paidActions[actionType].describe(args, incomingContext)
const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, {
msats: cost,
feePercent: sybilFeePercent,
description,
expiry: INVOICE_EXPIRE_SECS
}, { models, me, lnd })
context = {
...incomingContext,
invoiceArgs: {
bolt11: invoice,
wrappedBolt11: wrappedInvoice,
wallet,
maxFee
}
}
} catch (e) {
console.error('failed to create wrapped invoice', e)
throw new NonInvoiceablePeerError()
}
return me
? await performOptimisticAction(actionType, args, context)
: await beginPessimisticAction(actionType, args, context)
}
// we don't need to use the module for perform-ing outside actions
// because we can't track the state of outside invoices we aren't paid/paying
async function performDirectAction (actionType, args, incomingContext) {
const { models, lnd, cost } = incomingContext
const { comment, lud18Data, noteStr, description: actionDescription } = args
const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, incomingContext)
if (!userId) {
throw new NonInvoiceablePeerError()
}
try {
await assertBelowMaxPendingDirectPayments(userId, incomingContext)
const description = actionDescription ?? await paidActions[actionType].describe(args, incomingContext)
for await (const { invoice, logger, wallet } of createUserInvoice(userId, {
msats: cost,
description,
expiry: INVOICE_EXPIRE_SECS
}, { models, lnd })) {
let hash
try {
hash = parsePaymentRequest({ request: invoice }).id
} catch (e) {
console.error('failed to parse invoice', e)
logger?.error('failed to parse invoice: ' + e.message, { bolt11: invoice })
continue
}
try {
return {
invoice: await models.directPayment.create({
data: {
comment,
lud18Data,
desc: noteStr,
bolt11: invoice,
msats: cost,
hash,
walletId: wallet.id,
receiverId: userId
}
}),
paymentMethod: 'DIRECT'
}
} catch (e) {
console.error('failed to create direct payment', e)
logger?.error('failed to create direct payment: ' + e.message, { bolt11: invoice })
}
}
} catch (e) {
console.error('failed to create user invoice', e)
}
throw new NonInvoiceablePeerError()
}
export async function retryPaidAction (actionType, args, incomingContext) {
const { models, me } = incomingContext
const { invoice: failedInvoice } = args
console.log('retryPaidAction', actionType, args)
const action = paidActions[actionType]
if (!action) {
throw new Error(`retryPaidAction - invalid action type ${actionType}`)
}
if (!me) {
throw new Error(`retryPaidAction - must be logged in ${actionType}`)
}
if (!failedInvoice) {
throw new Error(`retryPaidAction - missing invoice ${actionType}`)
}
const { msatsRequested, actionId, actionArgs, actionOptimistic } = failedInvoice
const retryContext = {
...incomingContext,
optimistic: actionOptimistic,
me: await models.user.findUnique({ where: { id: parseInt(me.id) } }),
cost: BigInt(msatsRequested),
actionId,
predecessorId: failedInvoice.id
}
let invoiceArgs
const invoiceForward = await models.invoiceForward.findUnique({
where: {
invoiceId: failedInvoice.id
},
include: {
wallet: true
}
})
if (invoiceForward) {
// this is a wrapped invoice, we need to retry it with receiver fallbacks
try {
const { userId } = invoiceForward.wallet
// this will return an invoice from the first receiver wallet that didn't fail yet and throw if none is available
const { invoice: bolt11, wrappedInvoice: wrappedBolt11, wallet, maxFee } = await createWrappedInvoice(userId, {
msats: failedInvoice.msatsRequested,
feePercent: await action.getSybilFeePercent?.(actionArgs, retryContext),
description: await action.describe?.(actionArgs, retryContext),
expiry: INVOICE_EXPIRE_SECS
}, retryContext)
invoiceArgs = { bolt11, wrappedBolt11, wallet, maxFee }
} catch (err) {
console.log('failed to retry wrapped invoice, falling back to SN:', err)
}
}
invoiceArgs ??= await createSNInvoice(actionType, actionArgs, retryContext)
return await models.$transaction(async tx => {
const context = { ...retryContext, tx, invoiceArgs }
// update the old invoice to RETRYING, so that it's not confused with FAILED
await tx.invoice.update({
where: {
id: failedInvoice.id,
actionState: 'FAILED'
},
data: {
actionState: 'RETRYING'
}
})
// create a new invoice
const invoice = await createDbInvoice(actionType, actionArgs, context)
return {
result: await action.retry?.({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context),
invoice,
paymentMethod: actionOptimistic ? 'OPTIMISTIC' : 'PESSIMISTIC'
}
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
}
const INVOICE_EXPIRE_SECS = 600
export class NonInvoiceablePeerError extends Error {
constructor () {
super('non invoiceable peer')
this.name = 'NonInvoiceablePeerError'
}
}
// we seperate the invoice creation into two functions because
// because if lnd is slow, it'll timeout the interactive tx
async function createSNInvoice (actionType, args, context) {
const { me, lnd, cost, optimistic } = context
const action = paidActions[actionType]
const createLNDInvoice = optimistic ? createInvoice : createHodlInvoice
await assertBelowMaxPendingInvoices(context)
if (cost < 1000n) {
// sanity check
throw new Error('The cost of the action must be at least 1 sat')
}
const expiresAt = datePivot(new Date(), { seconds: INVOICE_EXPIRE_SECS })
const invoice = await createLNDInvoice({
description: me?.hideInvoiceDesc ? undefined : await action.describe(args, context),
lnd,
mtokens: String(cost),
expires_at: expiresAt
})
return { bolt11: invoice.request, preimage: invoice.secret }
}
async function createDbInvoice (actionType, args, context) {
const { me, models, tx, cost, optimistic, actionId, invoiceArgs, paymentAttempt, predecessorId } = context
const { bolt11, wrappedBolt11, preimage, wallet, maxFee } = invoiceArgs
const db = tx ?? models
if (cost < 1000n) {
// sanity check
throw new Error('The cost of the action must be at least 1 sat')
}
const servedBolt11 = wrappedBolt11 ?? bolt11
const servedInvoice = parsePaymentRequest({ request: servedBolt11 })
const expiresAt = new Date(servedInvoice.expires_at)
const invoiceData = {
hash: servedInvoice.id,
msatsRequested: BigInt(servedInvoice.mtokens),
preimage,
bolt11: servedBolt11,
userId: me?.id ?? USER_ID.anon,
actionType,
actionState: wrappedBolt11 ? 'PENDING_HELD' : optimistic ? 'PENDING' : 'PENDING_HELD',
actionOptimistic: optimistic,
actionArgs: args,
expiresAt,
actionId,
paymentAttempt,
predecessorId
}
let invoice
if (wrappedBolt11) {
invoice = (await db.invoiceForward.create({
include: { invoice: true },
data: {
bolt11,
maxFeeMsats: maxFee,
invoice: {
create: invoiceData
},
wallet: {
connect: {
id: wallet.id
}
}
}
})).invoice
} else {
invoice = await db.invoice.create({ data: invoiceData })
}
// insert a job to check the invoice after it's set to expire
await db.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil, priority)
VALUES ('checkInvoice',
jsonb_build_object('hash', ${invoice.hash}::TEXT), 21, true,
${expiresAt}::TIMESTAMP WITH TIME ZONE,
${expiresAt}::TIMESTAMP WITH TIME ZONE + interval '10m', 100)`
// the HMAC is only returned during invoice creation
// this makes sure that only the person who created this invoice
// has access to the HMAC
invoice.hmac = createHmac(invoice.hash)
return invoice
}

View File

@ -0,0 +1,60 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { notifyInvite } from '@/lib/webPush'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS
]
export async function getCost ({ id }, { models, me }) {
const invite = await models.invite.findUnique({ where: { id, userId: me.id, revoked: false } })
if (!invite) {
throw new Error('invite not found')
}
return satsToMsats(invite.gift)
}
export async function perform ({ id, userId }, { me, cost, tx }) {
const invite = await tx.invite.findUnique({
where: { id, userId: me.id, revoked: false }
})
if (invite.limit && invite.giftedCount >= invite.limit) {
throw new Error('invite limit reached')
}
// check that user was created in last hour
// check that user did not already redeem an invite
await tx.user.update({
where: {
id: userId,
inviteId: null,
createdAt: {
gt: new Date(Date.now() - 1000 * 60 * 60)
}
},
data: {
mcredits: {
increment: cost
},
inviteId: id,
referrerId: me.id
}
})
return await tx.invite.update({
where: { id, userId: me.id, revoked: false, ...(invite.limit ? { giftedCount: { lt: invite.limit } } : {}) },
data: {
giftedCount: {
increment: 1
}
}
})
}
export async function nonCriticalSideEffects (_, { me }) {
notifyInvite(me.id)
}

View File

@ -0,0 +1,309 @@
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySubscribers, notifyUserSubscribers, notifyThreadSubscribers } from '@/lib/webPush'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { msatsToSats, satsToMsats } from '@/lib/format'
import { GqlInputError } from '@/lib/error'
export const anonable = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export const DEFAULT_ITEM_COST = 1000n
export async function getBaseCost ({ models, bio, parentId, subName }) {
if (bio) return DEFAULT_ITEM_COST
if (parentId) {
// the subname is stored in the root item of the thread
const [sub] = await models.$queryRaw`
SELECT s."replyCost"
FROM "Item" i
LEFT JOIN "Item" r ON r.id = i."rootId"
LEFT JOIN "Sub" s ON s.name = COALESCE(r."subName", i."subName")
WHERE i.id = ${Number(parentId)}`
if (sub?.replyCost) return satsToMsats(sub.replyCost)
return DEFAULT_ITEM_COST
}
const sub = await models.sub.findUnique({ where: { name: subName } })
return satsToMsats(sub.baseCost)
}
export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio }, { models, me }) {
const baseCost = await getBaseCost({ models, bio, parentId, subName })
// cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + upload fees + boost
const [{ cost }] = await models.$queryRaw`
SELECT ${baseCost}::INTEGER
* POWER(10, item_spam(${parseInt(parentId)}::INTEGER, ${me?.id ?? USER_ID.anon}::INTEGER,
${me?.id && !bio ? ITEM_SPAM_INTERVAL : ANON_ITEM_SPAM_INTERVAL}::INTERVAL))
* ${me ? 1 : 100}::INTEGER
+ (SELECT "nUnpaid" * "uploadFeesMsats"
FROM upload_fees(${me?.id || USER_ID.anon}::INTEGER, ${uploadIds}::INTEGER[]))
+ ${satsToMsats(boost)}::INTEGER as cost`
// sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
// cost must be greater than user's balance, and user has not disabled freebies
const freebie = (parentId || bio) && cost <= baseCost && !!me &&
me?.msats < cost && !me?.disableFreebies && me?.mcredits < cost
return freebie ? BigInt(0) : BigInt(cost)
}
export async function perform (args, context) {
const { invoiceId, parentId, uploadIds = [], forwardUsers = [], options: pollOptions = [], boost = 0, ...data } = args
const { tx, me, cost } = context
const boostMsats = satsToMsats(boost)
const deletedUploads = []
for (const uploadId of uploadIds) {
if (!await tx.upload.findUnique({ where: { id: uploadId } })) {
deletedUploads.push(uploadId)
}
}
if (deletedUploads.length > 0) {
throw new Error(`upload(s) ${deletedUploads.join(', ')} are expired, consider reuploading.`)
}
let invoiceData = {}
if (invoiceId) {
invoiceData = { invoiceId, invoiceActionState: 'PENDING' }
await tx.upload.updateMany({
where: { id: { in: uploadIds } },
data: invoiceData
})
}
const itemActs = []
if (boostMsats > 0) {
itemActs.push({
msats: boostMsats, act: 'BOOST', userId: data.userId, ...invoiceData
})
}
if (cost > 0) {
itemActs.push({
msats: cost - boostMsats, act: 'FEE', userId: data.userId, ...invoiceData
})
data.cost = msatsToSats(cost - boostMsats)
}
const mentions = await getMentions(args, context)
const itemMentions = await getItemMentions(args, context)
// start with median vote
if (me) {
const [row] = await tx.$queryRaw`SELECT
COALESCE(percentile_cont(0.5) WITHIN GROUP(
ORDER BY "weightedVotes" - "weightedDownVotes"), 0)
AS median FROM "Item" WHERE "userId" = ${me.id}::INTEGER`
if (row?.median < 0) {
data.weightedDownVotes = -row.median
}
}
const itemData = {
parentId: parentId ? parseInt(parentId) : null,
...data,
...invoiceData,
boost,
threadSubscriptions: {
createMany: {
data: [
{ userId: data.userId },
...forwardUsers.map(({ userId }) => ({ userId }))
]
}
},
itemForwards: {
createMany: {
data: forwardUsers
}
},
pollOptions: {
createMany: {
data: pollOptions.map(option => ({ option }))
}
},
itemUploads: {
create: uploadIds.map(id => ({ uploadId: id }))
},
itemActs: {
createMany: {
data: itemActs
}
},
mentions: {
createMany: {
data: mentions
}
},
itemReferrers: {
create: itemMentions
}
}
let item
if (data.bio && me) {
item = (await tx.user.update({
where: { id: data.userId },
include: { bio: true },
data: {
bio: {
create: itemData
}
}
})).bio
} else {
try {
item = await tx.item.create({ data: itemData })
} catch (err) {
if (err.message.includes('violates exclusion constraint \\"Item_unique_time_constraint\\"')) {
const message = `you already submitted this ${itemData.title ? 'post' : 'comment'}`
throw new GqlInputError(message)
}
throw err
}
}
// store a reference to the item in the invoice
if (invoiceId) {
await tx.invoice.update({
where: { id: invoiceId },
data: { actionId: item.id }
})
}
await performBotBehavior(item, context)
// ltree is unsupported in Prisma, so we have to query it manually (FUCK!)
return (await tx.$queryRaw`
SELECT *, ltree2text(path) AS path, created_at AS "createdAt", updated_at AS "updatedAt"
FROM "Item" WHERE id = ${item.id}::INTEGER`
)[0]
}
export async function retry ({ invoiceId, newInvoiceId }, { tx }) {
await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
await tx.item.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
await tx.upload.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
return (await tx.$queryRaw`
SELECT *, ltree2text(path) AS path, created_at AS "createdAt", updated_at AS "updatedAt"
FROM "Item" WHERE "invoiceId" = ${newInvoiceId}::INTEGER`
)[0]
}
export async function onPaid ({ invoice, id }, context) {
const { tx } = context
let item
if (invoice) {
item = await tx.item.findFirst({
where: { invoiceId: invoice.id },
include: {
user: true
}
})
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID' } })
await tx.item.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID', invoicePaidAt: new Date() } })
await tx.upload.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID', paid: true } })
} else if (id) {
item = await tx.item.findUnique({
where: { id },
include: {
user: true,
itemUploads: { include: { upload: true } }
}
})
await tx.upload.updateMany({
where: { id: { in: item.itemUploads.map(({ uploadId }) => uploadId) } },
data: {
paid: true
}
})
} else {
throw new Error('No item found')
}
await tx.$executeRaw`INSERT INTO pgboss.job (name, data, startafter, priority)
VALUES ('timestampItem', jsonb_build_object('id', ${item.id}::INTEGER), now() + interval '10 minutes', -2)`
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
VALUES ('imgproxy', jsonb_build_object('id', ${item.id}::INTEGER), 21, true, now() + interval '5 seconds')`
if (item.boost > 0) {
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('expireBoost', jsonb_build_object('id', ${item.id}::INTEGER), 21, true,
now() + interval '30 days', now() + interval '40 days')`
}
if (item.parentId) {
// denormalize ncomments, lastCommentAt, and "weightedComments" for ancestors, and insert into reply table
await tx.$executeRaw`
WITH comment AS (
SELECT "Item".*, users.trust
FROM "Item"
JOIN users ON "Item"."userId" = users.id
WHERE "Item".id = ${item.id}::INTEGER
), ancestors AS (
SELECT "Item".*
FROM "Item", comment
WHERE "Item".path @> comment.path AND "Item".id <> comment.id
ORDER BY "Item".id
), updated_ancestors AS (
UPDATE "Item"
SET ncomments = "Item".ncomments + 1,
"lastCommentAt" = GREATEST("Item"."lastCommentAt", comment.created_at),
"nDirectComments" = "Item"."nDirectComments" +
CASE WHEN comment."parentId" = "Item".id THEN 1 ELSE 0 END
FROM comment, ancestors
WHERE "Item".id = ancestors.id
RETURNING "Item".*
)
INSERT INTO "Reply" (created_at, updated_at, "ancestorId", "ancestorUserId", "itemId", "userId", level)
SELECT comment.created_at, comment.updated_at, ancestors.id, ancestors."userId",
comment.id, comment."userId", nlevel(comment.path) - nlevel(ancestors.path)
FROM ancestors, comment`
}
}
export async function nonCriticalSideEffects ({ invoice, id }, { models }) {
const item = await models.item.findFirst({
where: invoice ? { invoiceId: invoice.id } : { id: parseInt(id) },
include: {
mentions: true,
itemReferrers: { include: { refereeItem: true } },
user: true
}
})
if (item.parentId) {
notifyItemParents({ item, models }).catch(console.error)
notifyThreadSubscribers({ models, item }).catch(console.error)
}
for (const { userId } of item.mentions) {
notifyMention({ models, item, userId }).catch(console.error)
}
for (const { refereeItem } of item.itemReferrers) {
notifyItemMention({ models, referrerItem: item, refereeItem }).catch(console.error)
}
notifyUserSubscribers({ models, item }).catch(console.error)
notifyTerritorySubscribers({ models, item }).catch(console.error)
}
export async function onFail ({ invoice }, { tx }) {
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
await tx.item.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
await tx.upload.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
}
export async function describe ({ parentId }, context) {
return `SN: create ${parentId ? `reply to #${parentId}` : 'item'}`
}

View File

@ -0,0 +1,182 @@
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { uploadFees } from '../resolvers/upload'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { notifyItemMention, notifyMention } from '@/lib/webPush'
import { satsToMsats } from '@/lib/format'
export const anonable = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ id, boost = 0, uploadIds, bio }, { me, models }) {
// the only reason updating items costs anything is when it has new uploads
// or more boost
const old = await models.item.findUnique({ where: { id: parseInt(id) } })
const { totalFeesMsats } = await uploadFees(uploadIds, { models, me })
const cost = BigInt(totalFeesMsats) + satsToMsats(boost - old.boost)
if (cost > 0 && old.invoiceActionState && old.invoiceActionState !== 'PAID') {
throw new Error('creation invoice not paid')
}
return cost
}
export async function perform (args, context) {
const { id, boost = 0, uploadIds = [], options: pollOptions = [], forwardUsers: itemForwards = [], ...data } = args
const { tx, me } = context
const old = await tx.item.findUnique({
where: { id: parseInt(id) },
include: {
threadSubscriptions: true,
mentions: true,
itemForwards: true,
itemReferrers: true,
itemUploads: true
}
})
const newBoost = boost - old.boost
const itemActs = []
if (newBoost > 0) {
const boostMsats = satsToMsats(newBoost)
itemActs.push({
msats: boostMsats, act: 'BOOST', userId: me?.id || USER_ID.anon
})
}
// createMany is the set difference of the new - old
// deleteMany is the set difference of the old - new
// updateMany is the intersection of the old and new
const difference = (a = [], b = [], key = 'userId') => a.filter(x => !b.find(y => y[key] === x[key]))
const intersectionMerge = (a = [], b = [], key) => a.filter(x => b.find(y => y.userId === x.userId))
.map(x => ({ [key]: x[key], ...b.find(y => y.userId === x.userId) }))
const mentions = await getMentions(args, context)
const itemMentions = await getItemMentions(args, context)
const itemUploads = uploadIds.map(id => ({ uploadId: id }))
await tx.upload.updateMany({
where: { id: { in: uploadIds } },
data: { paid: true }
})
// we put boost in the where clause because we don't want to update the boost
// if it has changed concurrently
await tx.item.update({
where: { id: parseInt(id), boost: old.boost },
data: {
...data,
boost: {
increment: newBoost
},
pollOptions: {
createMany: {
data: pollOptions?.map(option => ({ option }))
}
},
itemUploads: {
create: difference(itemUploads, old.itemUploads, 'uploadId').map(({ uploadId }) => ({ uploadId })),
deleteMany: {
uploadId: {
in: difference(old.itemUploads, itemUploads, 'uploadId').map(({ uploadId }) => uploadId)
}
}
},
itemActs: {
createMany: {
data: itemActs
}
},
itemForwards: {
deleteMany: {
userId: {
in: difference(old.itemForwards, itemForwards).map(({ userId }) => userId)
}
},
createMany: {
data: difference(itemForwards, old.itemForwards)
},
update: intersectionMerge(old.itemForwards, itemForwards, 'id').map(({ id, ...data }) => ({
where: { id },
data
}))
},
threadSubscriptions: {
deleteMany: {
userId: {
in: difference(old.itemForwards, itemForwards).map(({ userId }) => userId)
}
},
createMany: {
data: difference(itemForwards, old.itemForwards).map(({ userId }) => ({ userId }))
}
},
mentions: {
deleteMany: {
userId: {
in: difference(old.mentions, mentions).map(({ userId }) => userId)
}
},
createMany: {
data: difference(mentions, old.mentions)
}
},
itemReferrers: {
deleteMany: {
refereeId: {
in: difference(old.itemReferrers, itemMentions, 'refereeId').map(({ refereeId }) => refereeId)
}
},
create: difference(itemMentions, old.itemReferrers, 'refereeId')
}
}
})
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('imgproxy', jsonb_build_object('id', ${id}::INTEGER), 21, true,
now() + interval '5 seconds', now() + interval '1 day')`
if (newBoost > 0) {
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('expireBoost', jsonb_build_object('id', ${id}::INTEGER), 21, true,
now() + interval '30 days', now() + interval '40 days')`
}
await performBotBehavior(args, context)
// ltree is unsupported in Prisma, so we have to query it manually (FUCK!)
return (await tx.$queryRaw`
SELECT *, ltree2text(path) AS path, created_at AS "createdAt", updated_at AS "updatedAt"
FROM "Item" WHERE id = ${parseInt(id)}::INTEGER`
)[0]
}
export async function nonCriticalSideEffects ({ invoice, id }, { models }) {
const item = await models.item.findFirst({
where: invoice ? { invoiceId: invoice.id } : { id: parseInt(id) },
include: {
mentions: true,
itemReferrers: { include: { refereeItem: true } }
}
})
// compare timestamps to only notify if mention or item referral was just created to avoid duplicates on edits
for (const { userId, createdAt } of item.mentions) {
if (item.updatedAt.getTime() !== createdAt.getTime()) continue
notifyMention({ models, item, userId }).catch(console.error)
}
for (const { refereeItem, createdAt } of item.itemReferrers) {
if (item.updatedAt.getTime() !== createdAt.getTime()) continue
notifyItemMention({ models, referrerItem: item, refereeItem }).catch(console.error)
}
}
export async function describe ({ id, parentId }, context) {
return `SN: update ${parentId ? `reply to #${parentId}` : 'post'}`
}

View File

@ -0,0 +1,56 @@
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
import { datePivot } from '@/lib/time'
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
const MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES = 10
const MAX_PENDING_DIRECT_INVOICES_PER_USER = 100
export async function assertBelowMaxPendingInvoices (context) {
const { models, me } = context
const pendingInvoices = await models.invoice.count({
where: {
userId: me?.id ?? USER_ID.anon,
actionState: {
notIn: PAID_ACTION_TERMINAL_STATES
}
}
})
if (pendingInvoices >= MAX_PENDING_PAID_ACTIONS_PER_USER) {
throw new Error('You have too many pending paid actions, cancel some or wait for them to expire')
}
}
export async function assertBelowMaxPendingDirectPayments (userId, context) {
const { models, me } = context
if (me?.id !== userId) {
const pendingSenderInvoices = await models.directPayment.count({
where: {
senderId: me?.id ?? USER_ID.anon,
createdAt: {
gt: datePivot(new Date(), { minutes: -MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES })
}
}
})
if (pendingSenderInvoices >= MAX_PENDING_DIRECT_INVOICES_PER_USER) {
throw new Error('You\'ve sent too many direct payments')
}
}
if (!userId) return
const pendingReceiverInvoices = await models.directPayment.count({
where: {
receiverId: userId,
createdAt: {
gt: datePivot(new Date(), { minutes: -MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES })
}
}
})
if (pendingReceiverInvoices >= MAX_PENDING_DIRECT_INVOICES_PER_USER) {
throw new Error('Receiver has too many direct payments')
}
}

View File

@ -0,0 +1,89 @@
import { USER_ID } from '@/lib/constants'
import { deleteReminders, getDeleteAt, getRemindAt } from '@/lib/item'
import { parseInternalLinks } from '@/lib/url'
export async function getMentions ({ text }, { me, tx }) {
const mentionPattern = /\B@[\w_]+/gi
const names = text.match(mentionPattern)?.map(m => m.slice(1))
if (names?.length > 0) {
const users = await tx.user.findMany({
where: {
name: {
in: names
},
id: {
not: me?.id || USER_ID.anon
}
}
})
return users.map(user => ({ userId: user.id }))
}
return []
}
export const getItemMentions = async ({ text }, { me, tx }) => {
const linkPattern = new RegExp(`${process.env.NEXT_PUBLIC_URL}/items/\\d+[a-zA-Z0-9/?=]*`, 'gi')
const refs = text.match(linkPattern)?.map(m => {
try {
const { itemId, commentId } = parseInternalLinks(m)
return Number(commentId || itemId)
} catch (err) {
return null
}
}).filter(r => !!r)
if (refs?.length > 0) {
const referee = await tx.item.findMany({
where: {
id: { in: refs },
userId: { not: me?.id || USER_ID.anon }
}
})
return referee.map(r => ({ refereeId: r.id }))
}
return []
}
export async function performBotBehavior ({ text, id }, { me, tx }) {
// delete any existing deleteItem or reminder jobs for this item
const userId = me?.id || USER_ID.anon
id = Number(id)
await tx.$queryRaw`
DELETE FROM pgboss.job
WHERE name = 'deleteItem'
AND data->>'id' = ${id}::TEXT
AND state <> 'completed'`
await deleteReminders({ id, userId, models: tx })
if (text) {
const deleteAt = getDeleteAt(text)
if (deleteAt) {
await tx.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
VALUES (
'deleteItem',
jsonb_build_object('id', ${id}::INTEGER),
${deleteAt}::TIMESTAMP WITH TIME ZONE,
${deleteAt}::TIMESTAMP WITH TIME ZONE + interval '1 minute')`
}
const remindAt = getRemindAt(text)
if (remindAt) {
await tx.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
VALUES (
'reminder',
jsonb_build_object('itemId', ${id}::INTEGER, 'userId', ${userId}::INTEGER),
${remindAt}::TIMESTAMP WITH TIME ZONE,
${remindAt}::TIMESTAMP WITH TIME ZONE + interval '1 minute')`
await tx.reminder.create({
data: {
userId,
itemId: Number(id),
remindAt
}
})
}
}
}

View File

@ -0,0 +1,27 @@
import { USER_ID } from '@/lib/constants'
export const GLOBAL_SEEDS = [USER_ID.k00b, USER_ID.ek]
export function initialTrust ({ name, userId }) {
const results = GLOBAL_SEEDS.map(id => ({
subName: name,
userId: id,
zapPostTrust: 1,
subZapPostTrust: 1,
zapCommentTrust: 1,
subZapCommentTrust: 1
}))
if (!GLOBAL_SEEDS.includes(userId)) {
results.push({
subName: name,
userId,
zapPostTrust: 0,
subZapPostTrust: 1,
zapCommentTrust: 0,
subZapCommentTrust: 1
})
}
return results
}

View File

@ -0,0 +1,70 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export async function getCost ({ id }, { me, models }) {
const pollOption = await models.pollOption.findUnique({
where: { id: parseInt(id) },
include: { item: true }
})
return satsToMsats(pollOption.item.pollCost)
}
export async function perform ({ invoiceId, id }, { me, cost, tx }) {
const pollOption = await tx.pollOption.findUnique({
where: { id: parseInt(id) }
})
const itemId = parseInt(pollOption.itemId)
let invoiceData = {}
if (invoiceId) {
invoiceData = { invoiceId, invoiceActionState: 'PENDING' }
// store a reference to the item in the invoice
await tx.invoice.update({
where: { id: invoiceId },
data: { actionId: itemId }
})
}
// the unique index on userId, itemId will prevent double voting
await tx.itemAct.create({ data: { msats: cost, itemId, userId: me.id, act: 'POLL', ...invoiceData } })
await tx.pollBlindVote.create({ data: { userId: me.id, itemId, ...invoiceData } })
await tx.pollVote.create({ data: { pollOptionId: pollOption.id, itemId, ...invoiceData } })
return { id }
}
export async function retry ({ invoiceId, newInvoiceId }, { tx }) {
await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
await tx.pollBlindVote.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
await tx.pollVote.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
const { pollOptionId } = await tx.pollVote.findFirst({ where: { invoiceId: newInvoiceId } })
return { id: pollOptionId }
}
export async function onPaid ({ invoice }, { tx }) {
if (!invoice) return
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID' } })
await tx.pollBlindVote.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID' } })
// anonymize the vote
await tx.pollVote.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceId: null, invoiceActionState: null } })
}
export async function onFail ({ invoice }, { tx }) {
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
await tx.pollBlindVote.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
await tx.pollVote.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
}
export async function describe ({ id }, { actionId }) {
return `SN: vote on poll #${id ?? actionId}`
}

84
api/paidAction/receive.js Normal file
View File

@ -0,0 +1,84 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { toPositiveBigInt, numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
import { notifyDeposit } from '@/lib/webPush'
import { getInvoiceableWallets } from '@/wallets/server'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.P2P,
PAID_ACTION_PAYMENT_METHODS.DIRECT
]
export async function getCost ({ msats }) {
return toPositiveBigInt(msats)
}
export async function getInvoiceablePeer (_, { me, models, cost, paymentMethod }) {
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P && !me?.proxyReceive) return null
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT && !me?.directReceive) return null
const wallets = await getInvoiceableWallets(me.id, { models })
if (wallets.length === 0) {
return null
}
if (cost < satsToMsats(me.receiveCreditsBelowSats)) {
return null
}
return me.id
}
export async function getSybilFeePercent () {
return 10n
}
export async function perform ({
invoiceId,
comment,
lud18Data,
noteStr
}, { me, tx }) {
return await tx.invoice.update({
where: { id: invoiceId },
data: {
comment,
lud18Data,
...(noteStr ? { desc: noteStr } : {})
},
include: { invoiceForward: true }
})
}
export async function describe ({ description }, { me, cost, paymentMethod, sybilFeePercent }) {
const fee = paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P
? cost * BigInt(sybilFeePercent) / 100n
: 0n
return description ?? `SN: ${me?.name ?? ''} receives ${numWithUnits(msatsToSats(cost - fee))}`
}
export async function onPaid ({ invoice }, { tx }) {
if (!invoice) {
throw new Error('invoice is required')
}
// P2P lnurlp does not need to update the user's balance
if (invoice?.invoiceForward) return
await tx.user.update({
where: { id: invoice.userId },
data: {
mcredits: {
increment: invoice.msatsReceived
}
}
})
}
export async function nonCriticalSideEffects ({ invoice }, { models }) {
await notifyDeposit(invoice.userId, invoice)
await models.$executeRaw`
INSERT INTO pgboss.job (name, data)
VALUES ('nip57', jsonb_build_object('hash', ${invoice.hash}))`
}

View File

@ -0,0 +1,73 @@
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ name }, { models }) {
const sub = await models.sub.findUnique({
where: {
name
}
})
return satsToMsats(TERRITORY_PERIOD_COST(sub.billingType))
}
export async function perform ({ name }, { cost, tx }) {
const sub = await tx.sub.findUnique({
where: {
name
}
})
if (sub.billingType === 'ONCE') {
throw new Error('Cannot bill a ONCE territory')
}
let billedLastAt = sub.billPaidUntil
let billingCost = sub.billingCost
// if the sub is archived, they are paying to reactivate it
if (sub.status === 'STOPPED') {
// get non-grandfathered cost and reset their billing to start now
billedLastAt = new Date()
billingCost = TERRITORY_PERIOD_COST(sub.billingType)
}
const billPaidUntil = nextBilling(billedLastAt, sub.billingType)
return await tx.sub.update({
// optimistic concurrency control
// make sure the sub hasn't changed since we fetched it
where: {
...sub,
postTypes: {
equals: sub.postTypes
}
},
data: {
billedLastAt,
billPaidUntil,
billingCost,
status: 'ACTIVE',
SubAct: {
create: {
msats: cost,
type: 'BILLING',
userId: sub.userId
}
}
}
})
}
export async function describe ({ name }) {
return `SN: billing for territory ${name}`
}

View File

@ -0,0 +1,56 @@
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory'
import { initialTrust } from './lib/territory'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ billingType }) {
return satsToMsats(TERRITORY_PERIOD_COST(billingType))
}
export async function perform ({ invoiceId, ...data }, { me, cost, tx }) {
const { billingType } = data
const billingCost = TERRITORY_PERIOD_COST(billingType)
const billedLastAt = new Date()
const billPaidUntil = nextBilling(billedLastAt, billingType)
const sub = await tx.sub.create({
data: {
...data,
billedLastAt,
billPaidUntil,
billingCost,
rankingType: 'WOT',
userId: me.id,
SubAct: {
create: {
msats: cost,
type: 'BILLING',
userId: me.id
}
},
SubSubscription: {
create: {
userId: me.id
}
}
}
})
await tx.userSubTrust.createMany({
data: initialTrust({ name: sub.name, userId: sub.userId })
})
return sub
}
export async function describe ({ name }) {
return `SN: create territory ${name}`
}

View File

@ -0,0 +1,90 @@
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory'
import { initialTrust } from './lib/territory'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ billingType }) {
return satsToMsats(TERRITORY_PERIOD_COST(billingType))
}
export async function perform ({ name, invoiceId, ...data }, { me, cost, tx }) {
const sub = await tx.sub.findUnique({
where: {
name
}
})
data.billingCost = TERRITORY_PERIOD_COST(data.billingType)
// we never want to bill them again if they are changing to ONCE
if (data.billingType === 'ONCE') {
data.billPaidUntil = null
data.billingAutoRenew = false
}
data.billedLastAt = new Date()
data.billPaidUntil = nextBilling(data.billedLastAt, data.billingType)
data.status = 'ACTIVE'
data.userId = me.id
if (sub.userId !== me.id) {
await tx.territoryTransfer.create({ data: { subName: name, oldUserId: sub.userId, newUserId: me.id } })
await tx.subSubscription.delete({ where: { userId_subName: { userId: sub.userId, subName: name } } })
}
await tx.subAct.create({
data: {
userId: me.id,
subName: name,
msats: cost,
type: 'BILLING'
}
})
await tx.subSubscription.upsert({
where: {
userId_subName: {
userId: me.id,
subName: name
}
},
update: {
userId: me.id,
subName: name
},
create: {
userId: me.id,
subName: name
}
})
const updatedSub = await tx.sub.update({
data,
// optimistic concurrency control
// make sure none of the relevant fields have changed since we fetched the sub
where: {
...sub,
postTypes: {
equals: sub.postTypes
}
}
})
await tx.userSubTrust.createMany({
data: initialTrust({ name: updatedSub.name, userId: updatedSub.userId })
})
return updatedSub
}
export async function describe ({ name }, context) {
return `SN: unarchive territory ${name}`
}

View File

@ -0,0 +1,83 @@
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { proratedBillingCost } from '@/lib/territory'
import { datePivot } from '@/lib/time'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ oldName, billingType }, { models }) {
const oldSub = await models.sub.findUnique({
where: {
name: oldName
}
})
const cost = proratedBillingCost(oldSub, billingType)
if (!cost) {
return 0n
}
return satsToMsats(cost)
}
export async function perform ({ oldName, invoiceId, ...data }, { me, cost, tx }) {
const oldSub = await tx.sub.findUnique({
where: {
name: oldName
}
})
data.billingCost = TERRITORY_PERIOD_COST(data.billingType)
// we never want to bill them again if they are changing to ONCE
if (data.billingType === 'ONCE') {
data.billPaidUntil = null
data.billingAutoRenew = false
}
// if they are changing to YEARLY, bill them in a year
// if they are changing to MONTHLY from YEARLY, do nothing
if (oldSub.billingType === 'MONTHLY' && data.billingType === 'YEARLY') {
data.billPaidUntil = datePivot(new Date(oldSub.billedLastAt), { years: 1 })
}
// if this billing change makes their bill paid up, set them to active
if (data.billPaidUntil === null || data.billPaidUntil >= new Date()) {
data.status = 'ACTIVE'
}
if (cost > 0n) {
await tx.subAct.create({
data: {
userId: me.id,
subName: oldName,
msats: cost,
type: 'BILLING'
}
})
}
return await tx.sub.update({
data,
where: {
// optimistic concurrency control
// make sure none of the relevant fields have changed since we fetched the sub
...oldSub,
postTypes: {
equals: oldSub.postTypes
},
name: oldName,
userId: me.id
}
})
}
export async function describe ({ name }, context) {
return `SN: update territory billing ${name}`
}

245
api/paidAction/zap.js Normal file
View File

@ -0,0 +1,245 @@
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
import { notifyZapped } from '@/lib/webPush'
import { getInvoiceableWallets } from '@/wallets/server'
import { Prisma } from '@prisma/client'
export const anonable = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.P2P,
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)
}
export async function getInvoiceablePeer ({ id, sats, hasSendWallet }, { models, me, cost }) {
// if the zap is dust, or if me doesn't have a send wallet but has enough sats/credits to pay for it
// then we don't invoice the peer
if (sats < me?.sendCreditsBelowSats ||
(me && !hasSendWallet && (me.mcredits >= cost || me.msats >= cost))) {
return null
}
const item = await models.item.findUnique({
where: { id: parseInt(id) },
include: {
itemForwards: true,
user: true
}
})
// bios don't get sats
if (item.bio) {
return null
}
const wallets = await getInvoiceableWallets(item.userId, { models })
// request peer invoice if they have an attached wallet and have not forwarded the item
// and the receiver doesn't want to receive credits
if (wallets.length > 0 &&
item.itemForwards.length === 0 &&
sats >= item.user.receiveCreditsBelowSats) {
return item.userId
}
return null
}
export async function getSybilFeePercent () {
return 30n
}
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, sybilFeePercent, tx }) {
const feeMsats = cost * sybilFeePercent / 100n
const zapMsats = cost - feeMsats
itemId = parseInt(itemId)
let invoiceData = {}
if (invoiceId) {
invoiceData = { invoiceId, invoiceActionState: 'PENDING' }
// store a reference to the item in the invoice
await tx.invoice.update({
where: { id: invoiceId },
data: { actionId: itemId }
})
}
const acts = await tx.itemAct.createManyAndReturn({
data: [
{ msats: feeMsats, itemId, userId: me?.id ?? USER_ID.anon, act: 'FEE', ...invoiceData },
{ msats: zapMsats, itemId, userId: me?.id ?? USER_ID.anon, act: 'TIP', ...invoiceData }
]
})
const [{ path }] = await tx.$queryRaw`
SELECT ltree2text(path) as path FROM "Item" WHERE id = ${itemId}::INTEGER`
return { id: itemId, sats, act: 'TIP', path, actIds: acts.map(act => act.id) }
}
export async function retry ({ invoiceId, newInvoiceId }, { tx, cost }) {
await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
const [{ id, path }] = await tx.$queryRaw`
SELECT "Item".id, ltree2text(path) as path
FROM "Item"
JOIN "ItemAct" ON "Item".id = "ItemAct"."itemId"
WHERE "ItemAct"."invoiceId" = ${newInvoiceId}::INTEGER`
return { id, sats: msatsToSats(cost), act: 'TIP', path }
}
export async function onPaid ({ invoice, actIds }, { tx }) {
let acts
if (invoice) {
await tx.itemAct.updateMany({
where: { invoiceId: invoice.id },
data: {
invoiceActionState: 'PAID'
}
})
acts = await tx.itemAct.findMany({ where: { invoiceId: invoice.id }, include: { item: true } })
actIds = acts.map(act => act.id)
} else if (actIds) {
acts = await tx.itemAct.findMany({ where: { id: { in: actIds } }, include: { item: true } })
} else {
throw new Error('No invoice or actIds')
}
const msats = acts.reduce((a, b) => a + BigInt(b.msats), BigInt(0))
const sats = msatsToSats(msats)
const itemAct = acts.find(act => act.act === 'TIP')
if (invoice?.invoiceForward) {
// only the op got sats and we need to add it to their stackedMsats
// because the sats were p2p
await tx.user.update({
where: { id: itemAct.item.userId },
data: { stackedMsats: { increment: itemAct.msats } }
})
} else {
// splits only use mcredits
await tx.$executeRaw`
WITH forwardees AS (
SELECT "userId", ((${itemAct.msats}::BIGINT * pct) / 100)::BIGINT AS mcredits
FROM "ItemForward"
WHERE "itemId" = ${itemAct.itemId}::INTEGER
), total_forwarded AS (
SELECT COALESCE(SUM(mcredits), 0) as mcredits
FROM forwardees
), recipients AS (
SELECT "userId", mcredits FROM forwardees
UNION
SELECT ${itemAct.item.userId}::INTEGER as "userId",
${itemAct.msats}::BIGINT - (SELECT mcredits FROM total_forwarded)::BIGINT as mcredits
ORDER BY "userId" ASC -- order to prevent deadlocks
)
UPDATE users
SET
mcredits = users.mcredits + recipients.mcredits,
"stackedMsats" = users."stackedMsats" + recipients.mcredits,
"stackedMcredits" = users."stackedMcredits" + recipients.mcredits
FROM recipients
WHERE users.id = recipients."userId"`
}
// perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt
// NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking
await tx.$queryRaw`
WITH territory AS (
SELECT COALESCE(r."subName", i."subName", 'meta')::TEXT as "subName"
FROM "Item" i
LEFT JOIN "Item" r ON r.id = i."rootId"
WHERE i.id = ${itemAct.itemId}::INTEGER
), zapper AS (
SELECT
COALESCE(${itemAct.item.parentId
? Prisma.sql`"zapCommentTrust"`
: Prisma.sql`"zapPostTrust"`}, 0) as "zapTrust",
COALESCE(${itemAct.item.parentId
? Prisma.sql`"subZapCommentTrust"`
: Prisma.sql`"subZapPostTrust"`}, 0) as "subZapTrust"
FROM territory
LEFT JOIN "UserSubTrust" ust ON ust."subName" = territory."subName"
AND ust."userId" = ${itemAct.userId}::INTEGER
), zap AS (
INSERT INTO "ItemUserAgg" ("userId", "itemId", "zapSats")
VALUES (${itemAct.userId}::INTEGER, ${itemAct.itemId}::INTEGER, ${sats}::INTEGER)
ON CONFLICT ("itemId", "userId") DO UPDATE
SET "zapSats" = "ItemUserAgg"."zapSats" + ${sats}::INTEGER, updated_at = now()
RETURNING ("zapSats" = ${sats}::INTEGER)::INTEGER as first_vote,
LOG("zapSats" / GREATEST("zapSats" - ${sats}::INTEGER, 1)::FLOAT) AS log_sats
), item_zapped AS (
UPDATE "Item"
SET
"weightedVotes" = "weightedVotes" + zapper."zapTrust" * zap.log_sats,
"subWeightedVotes" = "subWeightedVotes" + zapper."subZapTrust" * zap.log_sats,
upvotes = upvotes + zap.first_vote,
msats = "Item".msats + ${msats}::BIGINT,
mcredits = "Item".mcredits + ${invoice?.invoiceForward ? 0n : msats}::BIGINT,
"lastZapAt" = now()
FROM zap, zapper
WHERE "Item".id = ${itemAct.itemId}::INTEGER
RETURNING "Item".*, zapper."zapTrust" * zap.log_sats as "weightedVote"
), ancestors AS (
SELECT "Item".*
FROM "Item", item_zapped
WHERE "Item".path @> item_zapped.path AND "Item".id <> item_zapped.id
ORDER BY "Item".id
)
UPDATE "Item"
SET "weightedComments" = "Item"."weightedComments" + item_zapped."weightedVote",
"commentMsats" = "Item"."commentMsats" + ${msats}::BIGINT,
"commentMcredits" = "Item"."commentMcredits" + ${invoice?.invoiceForward ? 0n : msats}::BIGINT
FROM item_zapped, ancestors
WHERE "Item".id = ancestors.id`
// record potential bounty payment
// NOTE: we are at least guaranteed that we see the update "ItemUserAgg" from our tx so we can trust
// we won't miss a zap that aggregates into a bounty payment, regardless of the order of updates
await tx.$executeRaw`
WITH bounty AS (
SELECT root.id, "ItemUserAgg"."zapSats" >= root.bounty AS paid, "ItemUserAgg"."itemId" AS target
FROM "ItemUserAgg"
JOIN "Item" ON "Item".id = "ItemUserAgg"."itemId"
LEFT JOIN "Item" root ON root.id = "Item"."rootId"
WHERE "ItemUserAgg"."userId" = ${itemAct.userId}::INTEGER
AND "ItemUserAgg"."itemId" = ${itemAct.itemId}::INTEGER
AND root."userId" = ${itemAct.userId}::INTEGER
AND root.bounty IS NOT NULL
)
UPDATE "Item"
SET "bountyPaidTo" = array_remove(array_append(array_remove("bountyPaidTo", bounty.target), bounty.target), NULL)
FROM bounty
WHERE "Item".id = bounty.id AND bounty.paid`
}
export async function nonCriticalSideEffects ({ invoice, actIds }, { models }) {
const itemAct = await models.itemAct.findFirst({
where: invoice ? { invoiceId: invoice.id } : { id: { in: actIds } },
include: { item: true }
})
// avoid duplicate notifications with the same zap amount
// by checking if there are any other pending acts on the item
const pendingActs = await models.itemAct.count({
where: {
itemId: itemAct.itemId,
createdAt: {
gt: itemAct.createdAt
}
}
})
if (pendingActs === 0) notifyZapped({ models, item: itemAct.item }).catch(console.error)
}
export async function onFail ({ invoice }, { tx }) {
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
}
export async function describe ({ id: itemId, sats }, { actionId, cost }) {
return `SN: zap ${sats ?? msatsToSats(cost)} sats to #${itemId ?? actionId}`
}

64
api/payingAction/index.js Normal file
View File

@ -0,0 +1,64 @@
import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format'
import { Prisma } from '@prisma/client'
import { parsePaymentRequest, payViaPaymentRequest } from 'ln-service'
// paying actions are completely distinct from paid actions
// and there's only one paying action: send
// ... still we want the api to at least be similar
export default async function performPayingAction ({ bolt11, maxFee, walletId }, { me, models, lnd }) {
try {
console.group('performPayingAction', `${bolt11.slice(0, 10)}...`, maxFee, walletId)
if (!me) {
throw new Error('You must be logged in to perform this action')
}
const decoded = await parsePaymentRequest({ request: bolt11 })
const cost = toPositiveBigInt(toPositiveBigInt(decoded.mtokens) + satsToMsats(maxFee))
console.log('cost', cost)
const withdrawal = await models.$transaction(async tx => {
await tx.user.update({
where: {
id: me.id
},
data: { msats: { decrement: cost } }
})
return await tx.withdrawl.create({
data: {
hash: decoded.id,
bolt11,
msatsPaying: toPositiveBigInt(decoded.mtokens),
msatsFeePaying: satsToMsats(maxFee),
userId: me.id,
walletId,
autoWithdraw: !!walletId
}
})
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
payViaPaymentRequest({
lnd,
request: withdrawal.bolt11,
max_fee: msatsToSats(withdrawal.msatsFeePaying),
pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS,
confidence: LND_PATHFINDING_TIME_PREF_PPM
}).catch(console.error)
return withdrawal
} catch (e) {
if (e.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
throw new Error('insufficient funds')
}
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
throw new Error('you cannot withdraw to the same invoice twice')
}
console.error('performPayingAction failed', e)
throw e
} finally {
console.groupEnd()
}
}

View File

@ -1,13 +1,15 @@
import { SN_ADMIN_IDS } from '@/lib/constants'
export default {
Query: {
snl: async (parent, _, { models }) => {
const { live } = await models.snl.findFirst()
return live
const snl = await models.snl.findFirst()
return !!snl?.live
}
},
Mutation: {
onAirToggle: async (parent, _, { models, me }) => {
if (me.id !== 616) {
if (!me || !SN_ADMIN_IDS.includes(me.id)) {
throw new Error('not an admin')
}
const { id, live } = await models.snl.findFirst()

7
api/resolvers/apiKey.js Normal file
View File

@ -0,0 +1,7 @@
import { GqlAuthorizationError } from '@/lib/error'
export default function assertApiKeyNotPermitted ({ me }) {
if (me?.apiKey === true) {
throw new GqlAuthorizationError('this operation is not allowed to be performed via API Key')
}
}

View File

@ -1,37 +1,27 @@
import lndService from 'ln-service'
import lnd from '../lnd'
import { isServiceEnabled } from '@/lib/sndev'
import { cachedFetcher } from '@/lib/fetch'
import { getHeight } from 'ln-service'
const cache = new Map()
const expiresIn = 1000 * 30 // 30 seconds in milliseconds
async function fetchBlockHeight () {
let blockHeight = 0
const getBlockHeight = cachedFetcher(async function fetchBlockHeight ({ lnd }) {
try {
const height = await lndService.getHeight({ lnd })
blockHeight = height.current_block_height
const { current_block_height: height } = await getHeight({ lnd })
return height
} catch (err) {
console.error('fetchBlockHeight', err)
console.error('getBlockHeight', err)
return 0
}
cache.set('block', { height: blockHeight, createdAt: Date.now() })
return blockHeight
}
async function getBlockHeight () {
if (cache.has('block')) {
const { height, createdAt } = cache.get('block')
const expired = createdAt + expiresIn < Date.now()
if (expired) fetchBlockHeight().catch(console.error) // update cache
return height // serve stale block height (this on the SSR critical path)
} else {
fetchBlockHeight().catch(console.error)
}
return 0
}
}, {
maxSize: 1,
cacheExpiry: 60 * 1000, // 1 minute
forceRefreshThreshold: 0,
keyGenerator: () => 'getBlockHeight'
})
export default {
Query: {
blockHeight: async (parent, opts, ctx) => {
return await getBlockHeight()
blockHeight: async (parent, opts, { lnd }) => {
if (!isServiceEnabled('payments')) return 0
return await getBlockHeight({ lnd }) || 0
}
}
}

View File

@ -1,37 +1,26 @@
import lndService from 'ln-service'
import lnd from '../lnd'
import { cachedFetcher } from '@/lib/fetch'
const cache = new Map()
const expiresIn = 1000 * 30 // 30 seconds in milliseconds
async function fetchChainFeeRate () {
let chainFee = 0
const getChainFeeRate = cachedFetcher(async function fetchChainFeeRate () {
const url = 'https://mempool.space/api/v1/fees/recommended'
try {
const fee = await lndService.getChainFeeRate({ lnd })
chainFee = fee.tokens_per_vbyte
const res = await fetch(url)
const body = await res.json()
return body.hourFee
} catch (err) {
console.error('fetchChainFee', err)
return 0
}
cache.set('fee', { fee: chainFee, createdAt: Date.now() })
return chainFee
}
async function getChainFeeRate () {
if (cache.has('fee')) {
const { fee, createdAt } = cache.get('fee')
const expired = createdAt + expiresIn < Date.now()
if (expired) fetchChainFeeRate().catch(console.error) // update cache
return fee
} else {
fetchChainFeeRate().catch(console.error)
}
return 0
}
}, {
maxSize: 1,
cacheExpiry: 60 * 1000, // 1 minute
forceRefreshThreshold: 0, // never force refresh
keyGenerator: () => 'getChainFeeRate'
})
export default {
Query: {
chainFee: async (parent, opts, ctx) => {
return await getChainFeeRate()
return await getChainFeeRate() || 0
}
}
}

View File

@ -1,45 +1,29 @@
import { timeUnitForRange, whenRange } from '../../lib/time'
const PLACEHOLDERS_NUM = 616
export function interval (when) {
switch (when) {
case 'week':
return '1 week'
case 'month':
return '1 month'
case 'year':
return '1 year'
case 'forever':
return null
default:
return '1 day'
}
}
export function withClause (range) {
const unit = timeUnitForRange(range)
return `
WITH range_values AS (
SELECT date_trunc('${unit}', $1) as minval,
date_trunc('${unit}', $2) as maxval
),
times AS (
SELECT generate_series(minval, maxval, interval '1 ${unit}') as time
FROM range_values
)
`
}
export function intervalClause (range, table) {
const unit = timeUnitForRange(range)
return `date_trunc('${unit}', "${table}".created_at) >= date_trunc('${unit}', $1) AND date_trunc('${unit}', "${table}".created_at) <= date_trunc('${unit}', $2) `
}
import { timeUnitForRange, whenRange } from '@/lib/time'
export function viewIntervalClause (range, view) {
return `"${view}".day >= date_trunc('day', timezone('America/Chicago', $1)) AND "${view}".day <= date_trunc('day', timezone('America/Chicago', $2)) `
const unit = timeUnitForRange(range)
return `"${view}".t >= date_trunc('${unit}', timezone('America/Chicago', $1)) AND date_trunc('${unit}', "${view}".t) <= date_trunc('${unit}', timezone('America/Chicago', $2)) `
}
export function viewGroup (range, view) {
const unit = timeUnitForRange(range)
return `(
(SELECT *
FROM ${view}_days
WHERE ${viewIntervalClause(range, `${view}_days`)})
UNION ALL
(SELECT *
FROM ${view}_hours
WHERE ${viewIntervalClause(range, `${view}_hours`)}
${unit === 'hour' ? '' : `AND "${view}_hours".t >= date_trunc('day', timezone('America/Chicago', now()))`})
UNION ALL
(SELECT * FROM
${view}(
date_trunc('hour', timezone('America/Chicago', now())),
date_trunc('hour', timezone('America/Chicago', now())), '1 hour'::INTERVAL, 'hour')
WHERE "${view}".t >= date_trunc('hour', timezone('America/Chicago', $1))
AND "${view}".t <= date_trunc('hour', timezone('America/Chicago', $2)))
) u`
}
export default {
@ -47,253 +31,129 @@ export default {
registrationGrowth: async (parent, { when, from, to }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'referrals', 'value', sum(referrals)),
json_build_object('name', 'organic', 'value', sum(organic))
) AS data
FROM reg_growth_days
WHERE ${viewIntervalClause(range, 'reg_growth_days')}
GROUP BY time
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'referrals', 'value', count("referrerId")),
json_build_object('name', 'organic', 'value', count(users.id) FILTER(WHERE id > ${PLACEHOLDERS_NUM}) - count("referrerId"))
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'referrals', 'value', sum(referrals)),
json_build_object('name', 'organic', 'value', sum(organic))
) AS data
FROM times
LEFT JOIN users ON ${intervalClause(range, 'users')} AND time = date_trunc('${timeUnitForRange(range)}', created_at)
FROM ${viewGroup(range, 'reg_growth')}
GROUP BY time
ORDER BY time ASC`, ...range)
},
spenderGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'any', 'value', floor(avg("any"))),
json_build_object('name', 'jobs', 'value', floor(avg(jobs))),
json_build_object('name', 'boost', 'value', floor(avg(boost))),
json_build_object('name', 'fees', 'value', floor(avg(fees))),
json_build_object('name', 'zaps', 'value', floor(avg(tips))),
json_build_object('name', 'donation', 'value', floor(avg(donations))),
json_build_object('name', 'territories', 'value', floor(avg(territories)))
) AS data
FROM spender_growth_days
WHERE ${viewIntervalClause(range, 'spender_growth_days')}
GROUP BY time
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'any', 'value', count(DISTINCT "userId")),
json_build_object('name', 'jobs', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'STREAM')),
json_build_object('name', 'boost', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'BOOST')),
json_build_object('name', 'fees', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'FEE')),
json_build_object('name', 'zaps', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'TIP')),
json_build_object('name', 'donation', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'DONATION')),
json_build_object('name', 'territories', 'value', count(DISTINCT "userId") FILTER (WHERE act = 'TERRITORY'))
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'any', 'value', COUNT(DISTINCT "userId")),
json_build_object('name', 'jobs', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'STREAM')),
json_build_object('name', 'boost', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'BOOST')),
json_build_object('name', 'fees', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'FEE')),
json_build_object('name', 'poll', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'POLL')),
json_build_object('name', 'downzaps', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'DONT_LIKE_THIS')),
json_build_object('name', 'zaps', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'TIP')),
json_build_object('name', 'donation', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'DONATION')),
json_build_object('name', 'territories', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'TERRITORY'))
) AS data
FROM times
LEFT JOIN
((SELECT "ItemAct".created_at, "userId", act::text as act
FROM "ItemAct"
WHERE ${intervalClause(range, 'ItemAct')})
UNION ALL
(SELECT created_at, "userId", 'DONATION' as act
FROM "Donation"
WHERE ${intervalClause(range, 'Donation')})
UNION ALL
(SELECT created_at, "userId", 'TERRITORY' as act
FROM "SubAct"
WHERE type = 'BILLING' AND ${intervalClause(range, 'SubAct')})
) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
FROM ${viewGroup(range, 'spender_growth')}
GROUP BY time
ORDER BY time ASC`, ...range)
},
itemGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'posts', 'value', sum(posts)),
json_build_object('name', 'comments', 'value', sum(comments)),
json_build_object('name', 'jobs', 'value', sum(jobs)),
json_build_object('name', 'comments/posts', 'value', ROUND(sum(comments)/GREATEST(sum(posts), 1), 2))
) AS data
FROM item_growth_days
WHERE ${viewIntervalClause(range, 'item_growth_days')}
GROUP BY time
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'comments', 'value', count("parentId")),
json_build_object('name', 'jobs', 'value', count("subName") FILTER (WHERE "subName" = 'jobs')),
json_build_object('name', 'posts', 'value', count("Item".id)-count("parentId")-(count("subName") FILTER (WHERE "subName" = 'jobs'))),
json_build_object('name', 'comments/posts', 'value', ROUND(count("parentId")/GREATEST(count("Item".id)-count("parentId"), 1), 2))
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'posts', 'value', sum(posts)),
json_build_object('name', 'comments', 'value', sum(comments)),
json_build_object('name', 'jobs', 'value', sum(jobs)),
json_build_object('name', 'zaps', 'value', sum(zaps)),
json_build_object('name', 'territories', 'value', sum(territories)),
json_build_object('name', 'comments/posts', 'value', ROUND(sum(comments)/GREATEST(sum(posts), 1), 2))
) AS data
FROM times
LEFT JOIN "Item" ON ${intervalClause(range, 'Item')} AND time = date_trunc('${timeUnitForRange(range)}', created_at)
FROM ${viewGroup(range, 'item_growth')}
GROUP BY time
ORDER BY time ASC`, ...range)
},
spendingGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'jobs', 'value', sum(jobs)),
json_build_object('name', 'boost', 'value', sum(boost)),
json_build_object('name', 'fees', 'value', sum(fees)),
json_build_object('name', 'zaps', 'value', sum(tips)),
json_build_object('name', 'donations', 'value', sum(donations)),
json_build_object('name', 'territories', 'value', sum(territories))
) AS data
FROM spending_growth_days
WHERE ${viewIntervalClause(range, 'spending_growth_days')}
GROUP BY time
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'jobs', 'value', coalesce(floor(sum(CASE WHEN act = 'STREAM' THEN msats ELSE 0 END)/1000),0)),
json_build_object('name', 'boost', 'value', coalesce(floor(sum(CASE WHEN act = 'BOOST' THEN msats ELSE 0 END)/1000),0)),
json_build_object('name', 'fees', 'value', coalesce(floor(sum(CASE WHEN act NOT IN ('BOOST', 'TIP', 'STREAM', 'DONATION', 'REVENUE') THEN msats ELSE 0 END)/1000),0)),
json_build_object('name', 'zaps', 'value', coalesce(floor(sum(CASE WHEN act = 'TIP' THEN msats ELSE 0 END)/1000),0)),
json_build_object('name', 'donations', 'value', coalesce(floor(sum(CASE WHEN act = 'DONATION' THEN msats ELSE 0 END)/1000),0)),
json_build_object('name', 'territories', 'value', coalesce(floor(sum(CASE WHEN act = 'REVENUE' THEN msats ELSE 0 END)/1000),0))
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'jobs', 'value', sum(jobs)),
json_build_object('name', 'boost', 'value', sum(boost)),
json_build_object('name', 'fees', 'value', sum(fees)),
json_build_object('name', 'zaps', 'value', sum(tips)),
json_build_object('name', 'donations', 'value', sum(donations)),
json_build_object('name', 'territories', 'value', sum(territories))
) AS data
FROM times
LEFT JOIN
((SELECT "ItemAct".created_at, msats, act::text as act
FROM "ItemAct"
WHERE ${intervalClause(range, 'ItemAct')})
UNION ALL
(SELECT created_at, sats * 1000 as msats, 'DONATION' as act
FROM "Donation"
WHERE ${intervalClause(range, 'Donation')})
UNION ALL
(SELECT created_at, msats, 'REVENUE' as act
FROM "SubAct"
WHERE type = 'BILLING' AND ${intervalClause(range, 'SubAct')})
) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
FROM ${viewGroup(range, 'spending_growth')}
GROUP BY time
ORDER BY time ASC`, ...range)
},
stackerGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'any', 'value', floor(avg("any"))),
json_build_object('name', 'posts', 'value', floor(avg(posts))),
json_build_object('name', 'comments', 'value', floor(floor(avg(comments)))),
json_build_object('name', 'rewards', 'value', floor(avg(rewards))),
json_build_object('name', 'referrals', 'value', floor(avg(referrals))),
json_build_object('name', 'territories', 'value', floor(avg(territories)))
) AS data
FROM stackers_growth_days
WHERE ${viewIntervalClause(range, 'stackers_growth_days')}
GROUP BY time
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'any', 'value', count(distinct user_id)),
json_build_object('name', 'posts', 'value', count(distinct user_id) FILTER (WHERE type = 'POST')),
json_build_object('name', 'comments', 'value', count(distinct user_id) FILTER (WHERE type = 'COMMENT')),
json_build_object('name', 'rewards', 'value', count(distinct user_id) FILTER (WHERE type = 'EARN')),
json_build_object('name', 'referrals', 'value', count(distinct user_id) FILTER (WHERE type = 'REFERRAL')),
json_build_object('name', 'territories', 'value', count(distinct user_id) FILTER (WHERE type = 'REVENUE'))
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'any', 'value', COUNT(DISTINCT "userId")),
json_build_object('name', 'posts', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'POST')),
json_build_object('name', 'comments', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'COMMENT')),
json_build_object('name', 'rewards', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'EARN')),
json_build_object('name', 'referrals', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'REFERRAL')),
json_build_object('name', 'territories', 'value', COUNT(DISTINCT "userId") FILTER (WHERE type = 'REVENUE'))
) AS data
FROM times
LEFT JOIN
((SELECT "ItemAct".created_at, "Item"."userId" as user_id, CASE WHEN "Item"."parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE ${intervalClause(range, 'ItemAct')} AND "ItemAct".act = 'TIP')
UNION ALL
(SELECT created_at, "userId" as user_id, 'EARN' as type
FROM "Earn"
WHERE ${intervalClause(range, 'Earn')})
UNION ALL
(SELECT created_at, "userId" as user_id, 'REVENUE' as type
FROM "SubAct"
WHERE type = 'REVENUE' AND ${intervalClause(range, 'SubAct')})
UNION ALL
(SELECT created_at, "referrerId" as user_id, 'REFERRAL' as type
FROM "ReferralAct"
WHERE ${intervalClause(range, 'ReferralAct')})) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
FROM ${viewGroup(range, 'stackers_growth')}
GROUP BY time
ORDER BY time ASC`, ...range)
},
stackingGrowth: async (parent, { when, to, from }, { models }) => {
const range = whenRange(when, from, to)
if (when !== 'day') {
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', day) as time, json_build_array(
json_build_object('name', 'rewards', 'value', sum(rewards)),
json_build_object('name', 'posts', 'value', sum(posts)),
json_build_object('name', 'comments', 'value', sum(comments)),
json_build_object('name', 'referrals', 'value', sum(referrals)),
json_build_object('name', 'territories', 'value', sum(territories))
) AS data
FROM stacking_growth_days
WHERE ${viewIntervalClause(range, 'stacking_growth_days')}
GROUP BY time
ORDER BY time ASC`, ...range)
}
return await models.$queryRawUnsafe(
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'rewards', 'value', coalesce(floor(sum(airdrop)/1000),0)),
json_build_object('name', 'posts', 'value', coalesce(floor(sum(post)/1000),0)),
json_build_object('name', 'comments', 'value', coalesce(floor(sum(comment)/1000),0)),
json_build_object('name', 'referrals', 'value', coalesce(floor(sum(referral)/1000),0)),
json_build_object('name', 'territories', 'value', coalesce(floor(sum(revenue)/1000),0))
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'rewards', 'value', sum(rewards)),
json_build_object('name', 'posts', 'value', sum(posts)),
json_build_object('name', 'comments', 'value', sum(comments)),
json_build_object('name', 'referrals', 'value', sum(referrals)),
json_build_object('name', 'territories', 'value', sum(territories))
) AS data
FROM times
LEFT JOIN
((SELECT "ItemAct".created_at, 0 as airdrop,
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post,
0 as referral, 0 as revenue
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE ${intervalClause(range, 'ItemAct')} AND "ItemAct".act = 'TIP')
UNION ALL
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral, 0 as revenue
FROM "ReferralAct"
WHERE ${intervalClause(range, 'ReferralAct')})
UNION ALL
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, 0 as referral, msats as revenue
FROM "SubAct"
WHERE type = 'REVENUE' AND ${intervalClause(range, 'SubAct')})
UNION ALL
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment, 0 as referral, 0 as revenue
FROM "Earn"
WHERE ${intervalClause(range, 'Earn')})) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
FROM ${viewGroup(range, 'stacking_growth')}
GROUP BY time
ORDER BY time ASC`, ...range)
},
itemGrowthSubs: async (parent, { when, to, from, sub }, { models }) => {
const range = whenRange(when, from, to)
const subExists = await models.sub.findUnique({ where: { name: sub } })
if (!subExists) throw new Error('Sub not found')
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'posts', 'value', coalesce(sum(posts),0)),
json_build_object('name', 'comments', 'value', coalesce(sum(comments),0))
) AS data
FROM ${viewGroup(range, 'sub_stats')}
WHERE sub_name = $3
GROUP BY time
ORDER BY time ASC`, ...range, sub)
},
revenueGrowthSubs: async (parent, { when, to, from, sub }, { models }) => {
const range = whenRange(when, from, to)
const subExists = await models.sub.findUnique({ where: { name: sub } })
if (!subExists) throw new Error('Sub not found')
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time, json_build_array(
json_build_object('name', 'revenue', 'value', coalesce(sum(msats_revenue/1000),0)),
json_build_object('name', 'stacking', 'value', coalesce(sum(msats_stacked/1000),0)),
json_build_object('name', 'spending', 'value', coalesce(sum(msats_spent/1000),0))
) AS data
FROM ${viewGroup(range, 'sub_stats')}
WHERE sub_name = $3
GROUP BY time
ORDER BY time ASC`, ...range, sub)
}
}
}

View File

@ -1,23 +0,0 @@
import { ANON_USER_ID, AWS_S3_URL_REGEXP } from '../../lib/constants'
import { msatsToSats } from '../../lib/format'
export default {
Query: {
imageFeesInfo: async (parent, { s3Keys }, { models, me }) => {
return imageFeesInfo(s3Keys, { models, me })
}
}
}
export function uploadIdsFromText (text, { models }) {
if (!text) return null
return [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1]))
}
export async function imageFeesInfo (s3Keys, { models, me }) {
const [info] = await models.$queryRawUnsafe('SELECT * FROM image_fees_info($1::INTEGER, $2::INTEGER[])', me ? me.id : ANON_USER_ID, s3Keys)
const imageFee = msatsToSats(info.imageFeeMsats)
const totalFeesMsats = info.nUnpaid * Number(info.imageFeeMsats)
const totalFees = msatsToSats(totalFeesMsats)
return { ...info, imageFee, totalFees, totalFeesMsats }
}

View File

@ -16,9 +16,10 @@ import { GraphQLJSONObject as JSONObject } from 'graphql-type-json'
import admin from './admin'
import blockHeight from './blockHeight'
import chainFee from './chainFee'
import image from './image'
import { GraphQLScalarType, Kind } from 'graphql'
import { createIntScalar } from 'graphql-scalar'
import paidAction from './paidAction'
import vault from './vault'
const date = new GraphQLScalarType({
name: 'Date',
@ -54,4 +55,5 @@ const limit = createIntScalar({
})
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
upload, search, growth, rewards, referrals, price, admin, blockHeight, chainFee, image, { JSONObject }, { Date: date }, { Limit: limit }]
upload, search, growth, rewards, referrals, price, admin, blockHeight, chainFee,
{ JSONObject }, { Date: date }, { Limit: limit }, paidAction, vault]

View File

@ -1,14 +1,15 @@
import { GraphQLError } from 'graphql'
import { inviteSchema, ssValidate } from '../../lib/validate'
import { msatsToSats } from '../../lib/format'
import { inviteSchema, validateSchema } from '@/lib/validate'
import { msatsToSats } from '@/lib/format'
import assertApiKeyNotPermitted from './apiKey'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { Prisma } from '@prisma/client'
export default {
Query: {
invites: async (parent, args, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.invite.findMany({
where: {
userId: me.id
@ -28,26 +29,48 @@ export default {
},
Mutation: {
createInvite: async (parent, { gift, limit }, { me, models }) => {
createInvite: async (parent, { id, gift, limit, description }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })
await ssValidate(inviteSchema, { gift, limit })
return await models.invite.create({
data: { gift, limit, userId: me.id }
})
await validateSchema(inviteSchema, { id, gift, limit, description })
try {
return await models.invite.create({
data: {
id,
gift,
limit,
userId: me.id,
description
}
})
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002' && error.meta.target.includes('id')) {
throw new GqlInputError('an invite with this code already exists')
}
}
throw error
}
},
revokeInvite: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.invite.update({
where: { id },
data: { revoked: true }
})
try {
return await models.invite.update({
where: { id, userId: me.id },
data: { revoked: true }
})
} catch (err) {
if (err.code === 'P2025') {
throw new GqlInputError('invite not found')
}
throw err
}
}
},
@ -60,7 +83,10 @@ export default {
},
poor: async (invite, args, { me, models }) => {
const user = await models.user.findUnique({ where: { id: invite.userId } })
return msatsToSats(user.msats) < invite.gift
return msatsToSats(user.msats) < invite.gift && msatsToSats(user.mcredits) < invite.gift
},
description: (invite, args, { me }) => {
return invite.userId === me?.id ? invite.description : undefined
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
import { randomBytes } from 'crypto'
import { bech32 } from 'bech32'
import { GraphQLError } from 'graphql'
import assertGofacYourself from './ofac'
import assertApiKeyNotPermitted from './apiKey'
import { GqlAuthenticationError } from '@/lib/error'
function encodedUrl (iurl, tag, k1) {
const url = new URL(iurl)
@ -26,16 +27,19 @@ export default {
}
},
Mutation: {
createAuth: async (parent, args, { models }) => {
createAuth: async (parent, args, { models, me }) => {
assertApiKeyNotPermitted({ me })
return await models.lnAuth.create({ data: { k1: k1() } })
},
createWith: async (parent, args, { me, models, headers }) => {
await assertGofacYourself({ models, headers })
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })
return await models.lnWith.create({ data: { k1: k1(), userId: me.id } })
}
},

View File

@ -1,4 +1,4 @@
import { GraphQLError } from 'graphql'
import { GqlInputError } from '@/lib/error'
export default {
Query: {
@ -11,7 +11,7 @@ export default {
Mutation: {
createMessage: async (parent, { text }, { me, models }) => {
if (!text) {
throw new GraphQLError('Must have text', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('must have text')
}
return await models.message.create({

View File

@ -1,17 +1,18 @@
import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { getItem, filterClause, whereClause, muteClause } from './item'
import { getInvoice } from './wallet'
import { pushSubscriptionSchema, ssValidate } from '../../lib/validate'
import { replyToSubscription } from '../webPush'
import { decodeCursor, LIMIT, nextNoteCursorEncoded } from '@/lib/cursor'
import { getItem, filterClause, whereClause, muteClause, activeOrMine } from './item'
import { getInvoice, getWithdrawl } from './wallet'
import { pushSubscriptionSchema, validateSchema } from '@/lib/validate'
import { replyToSubscription } from '@/lib/webPush'
import { getSub } from './sub'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { WALLET_MAX_RETRIES, WALLET_RETRY_BEFORE_MS } from '@/lib/constants'
export default {
Query: {
notifications: async (parent, { cursor, inc }, { me, models }) => {
const decodedCursor = decodeCursor(cursor)
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const meFull = await models.user.findUnique({ where: { id: me.id } })
@ -78,16 +79,17 @@ export default {
itemDrivenQueries.push(
`SELECT "Item".*, "Item".created_at AS "sortTime", 'Reply' AS type
FROM "ThreadSubscription"
JOIN "Item" p ON "ThreadSubscription"."itemId" = p.id
JOIN "Item" ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
JOIN "Reply" r ON "ThreadSubscription"."itemId" = r."ancestorId"
JOIN "Item" ON r."itemId" = "Item".id
${whereClause(
'"ThreadSubscription"."userId" = $1',
'"Item"."userId" <> $1',
'"Item".created_at >= "ThreadSubscription".created_at',
'"Item"."parentId" IS NOT NULL'
'r.created_at >= "ThreadSubscription".created_at',
'r.created_at < $2',
'r."userId" <> $1',
...(meFull.noteAllDescendants ? [] : ['r.level = 1'])
)}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
LIMIT ${LIMIT}`
)
// User subscriptions
@ -97,6 +99,7 @@ export default {
FROM "Item"
JOIN "UserSubscription" ON "Item"."userId" = "UserSubscription"."followeeId"
${whereClause(
'"Item".created_at < $2',
'"UserSubscription"."followerId" = $1',
`(
("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
@ -104,7 +107,23 @@ export default {
)`
)}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
LIMIT ${LIMIT}`
)
// Territory subscriptions
itemDrivenQueries.push(
`SELECT "Item".*, "Item".created_at AS "sortTime", 'TerritoryPost' AS type
FROM "Item"
JOIN "SubSubscription" ON "Item"."subName" = "SubSubscription"."subName"
${whereClause(
'"Item".created_at < $2',
'"SubSubscription"."userId" = $1',
'"Item"."userId" <> $1',
'"Item"."parentId" IS NULL',
'"Item".created_at >= "SubSubscription".created_at'
)}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}`
)
// mentions
@ -114,11 +133,28 @@ export default {
FROM "Mention"
JOIN "Item" ON "Mention"."itemId" = "Item".id
${whereClause(
'"Item".created_at < $2',
'"Mention"."userId" = $1',
'"Item"."userId" <> $1'
)}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3`
LIMIT ${LIMIT}`
)
}
// item mentions
if (meFull.noteItemMentions) {
itemDrivenQueries.push(
`SELECT "Referrer".*, "ItemMention".created_at AS "sortTime", 'ItemMention' AS type
FROM "ItemMention"
JOIN "Item" "Referee" ON "ItemMention"."refereeId" = "Referee".id
JOIN "Item" "Referrer" ON "ItemMention"."referrerId" = "Referrer".id
${whereClause(
'"ItemMention".created_at < $2',
'"Referrer"."userId" <> $1',
'"Referee"."userId" = $1'
)}
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}`
)
}
// Inner union to de-dupe item-driven notifications
@ -130,72 +166,92 @@ export default {
${itemDrivenQueries.map(q => `(${q})`).join(' UNION ALL ')}
) as "Item"
${whereClause(
'"Item".created_at <= $2',
'"Item".created_at < $2',
await filterClause(me, models),
muteClause(me))}
muteClause(me),
activeOrMine(me))}
ORDER BY id ASC, CASE
WHEN type = 'Mention' THEN 1
WHEN type = 'Reply' THEN 2
WHEN type = 'FollowActivity' THEN 3
WHEN type = 'TerritoryPost' THEN 4
WHEN type = 'ItemMention' THEN 5
END ASC
)`
)
// territory transfers
queries.push(
`(SELECT "Item".id::text, "Item"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
'JobChanged' AS type
FROM "Item"
WHERE "Item"."userId" = $1
AND "maxBid" IS NOT NULL
AND "statusUpdatedAt" <= $2 AND "statusUpdatedAt" <> created_at
`(SELECT "TerritoryTransfer".id::text, "TerritoryTransfer"."created_at" AS "sortTime", NULL as "earnedSats",
'TerritoryTransfer' AS type
FROM "TerritoryTransfer"
WHERE "TerritoryTransfer"."newUserId" = $1
AND "TerritoryTransfer"."created_at" <= $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
LIMIT ${LIMIT})`
)
if (meFull.noteItemSats) {
queries.push(
`(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
MAX("Item".msats/1000) as "earnedSats", 'Votification' AS type
`(SELECT "Item".id::TEXT, "Item"."lastZapAt" AS "sortTime",
"Item".msats/1000 as "earnedSats", 'Votification' AS type
FROM "Item"
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
WHERE "ItemAct"."userId" <> $1
AND "ItemAct".created_at <= $2
AND "ItemAct".act IN ('TIP', 'FEE')
AND "Item"."userId" = $1
GROUP BY "Item".id
WHERE "Item"."userId" = $1
AND "Item"."lastZapAt" < $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
LIMIT ${LIMIT})`
)
}
if (meFull.noteForwardedSats) {
queries.push(
`(SELECT "Item".id::TEXT, MAX("ItemAct".created_at) AS "sortTime",
MAX("Item".msats / 1000 * "ItemForward".pct / 100) as "earnedSats", 'ForwardedVotification' AS type
`(SELECT "Item".id::TEXT, "Item"."lastZapAt" AS "sortTime",
("Item".msats / 1000 * "ItemForward".pct / 100) as "earnedSats", 'ForwardedVotification' AS type
FROM "Item"
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
JOIN "ItemForward" ON "ItemForward"."itemId" = "Item".id AND "ItemForward"."userId" = $1
WHERE "ItemAct"."userId" <> $1
AND "Item"."userId" <> $1
AND "ItemAct".created_at <= $2
AND "ItemAct".act IN ('TIP')
GROUP BY "Item".id
WHERE "Item"."userId" <> $1
AND "Item"."lastZapAt" < $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
LIMIT ${LIMIT})`
)
}
if (meFull.noteDeposits) {
queries.push(
`(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
`(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime",
FLOOR("Invoice"."msatsReceived" / 1000) as "earnedSats",
'InvoicePaid' AS type
FROM "Invoice"
WHERE "Invoice"."userId" = $1
AND "confirmedAt" IS NOT NULL
AND "isHeld" IS NULL
AND created_at <= $2
AND "Invoice"."confirmedAt" IS NOT NULL
AND "Invoice"."created_at" < $2
AND (
("Invoice"."isHeld" IS NULL AND "Invoice"."actionType" IS NULL)
OR (
"Invoice"."actionType" = 'RECEIVE'
AND "Invoice"."actionState" = 'PAID'
)
)
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
LIMIT ${LIMIT})`
)
}
if (meFull.noteWithdrawals) {
queries.push(
`(SELECT "Withdrawl".id::text, MAX(COALESCE("Invoice"."confirmedAt", "Withdrawl".created_at)) AS "sortTime",
FLOOR(MAX("Withdrawl"."msatsPaid" / 1000)) as "earnedSats",
'WithdrawlPaid' AS type
FROM "Withdrawl"
LEFT JOIN "InvoiceForward" ON "InvoiceForward"."withdrawlId" = "Withdrawl".id
LEFT JOIN "Invoice" ON "InvoiceForward"."invoiceId" = "Invoice".id
WHERE "Withdrawl"."userId" = $1
AND "Withdrawl".status = 'CONFIRMED'
AND "Withdrawl".created_at < $2
AND "InvoiceForward"."id" IS NULL
GROUP BY "Withdrawl".id
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
}
@ -205,10 +261,10 @@ export default {
'Invitification' AS type
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
WHERE "Invite"."userId" = $1
AND users.created_at <= $2
AND users.created_at < $2
GROUP BY "Invite".id
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
LIMIT ${LIMIT})`
)
queries.push(
`(SELECT users.id::text, users.created_at AS "sortTime", NULL as "earnedSats",
@ -216,37 +272,56 @@ export default {
FROM users
WHERE "users"."referrerId" = $1
AND "inviteId" IS NULL
AND users.created_at <= $2
LIMIT ${LIMIT}+$3)`
AND users.created_at < $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
}
if (meFull.noteEarning) {
queries.push(
`SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
`(SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
'Earn' AS type
FROM "Earn"
WHERE "userId" = $1
AND created_at <= $2
GROUP BY "userId", created_at`
AND created_at < $2
AND (type IS NULL OR type NOT IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL'))
GROUP BY "userId", created_at
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
queries.push(
`SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
`(SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
'Revenue' AS type
FROM "SubAct"
WHERE "userId" = $1
AND type = 'REVENUE'
AND created_at <= $2
GROUP BY "userId", "subName", created_at`
AND created_at < $2
GROUP BY "userId", "subName", created_at
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
queries.push(
`(SELECT min(id)::text, created_at AS "sortTime", FLOOR(sum(msats) / 1000) as "earnedSats",
'ReferralReward' AS type
FROM "Earn"
WHERE "userId" = $1
AND created_at < $2
AND type IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL')
GROUP BY "userId", created_at
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
}
if (meFull.noteCowboyHat) {
queries.push(
`SELECT id::text, updated_at AS "sortTime", 0 as "earnedSats", 'Streak' AS type
`(SELECT id::text, updated_at AS "sortTime", 0 as "earnedSats", 'Streak' AS type
FROM "Streak"
WHERE "userId" = $1
AND updated_at <= $2`
AND updated_at < $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
}
@ -256,31 +331,58 @@ export default {
FROM "Sub"
WHERE "Sub"."userId" = $1
AND "status" <> 'ACTIVE'
AND "statusUpdatedAt" <= $2
AND "statusUpdatedAt" < $2
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}+$3)`
LIMIT ${LIMIT})`
)
// we do all this crazy subquery stuff to make 'reward' islands
const notifications = await models.$queryRawUnsafe(
`SELECT MAX(id) AS id, MAX("sortTime") AS "sortTime", sum("earnedSats") AS "earnedSats", type,
MIN("sortTime") AS "minSortTime"
FROM
(SELECT *,
CASE
WHEN type = 'Earn' THEN
ROW_NUMBER() OVER(ORDER BY "sortTime" DESC) -
ROW_NUMBER() OVER(PARTITION BY type = 'Earn' ORDER BY "sortTime" DESC)
ELSE
ROW_NUMBER() OVER(ORDER BY "sortTime" DESC)
END as island
FROM
(${queries.join(' UNION ALL ')}) u
) sub
GROUP BY type, island
queries.push(
`(SELECT "Reminder".id::text, "Reminder"."remindAt" AS "sortTime", NULL as "earnedSats", 'Reminder' AS type
FROM "Reminder"
WHERE "Reminder"."userId" = $1
AND "Reminder"."remindAt" < $2
ORDER BY "sortTime" DESC
OFFSET $3
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)
LIMIT ${LIMIT})`
)
queries.push(
`(SELECT "Invoice".id::text,
CASE
WHEN
"Invoice"."paymentAttempt" < ${WALLET_MAX_RETRIES}
AND "Invoice"."userCancel" = false
AND "Invoice"."cancelledAt" <= now() - interval '${`${WALLET_RETRY_BEFORE_MS} milliseconds`}'
THEN "Invoice"."cancelledAt" + interval '${`${WALLET_RETRY_BEFORE_MS} milliseconds`}'
ELSE "Invoice"."updated_at"
END AS "sortTime", NULL as "earnedSats", 'Invoicification' AS type
FROM "Invoice"
WHERE "Invoice"."userId" = $1
AND "Invoice"."updated_at" < $2
AND "Invoice"."actionState" = 'FAILED'
AND (
-- this is the inverse of the filter for automated retries
"Invoice"."paymentAttempt" >= ${WALLET_MAX_RETRIES}
OR "Invoice"."userCancel" = true
OR "Invoice"."cancelledAt" <= now() - interval '${`${WALLET_RETRY_BEFORE_MS} milliseconds`}'
)
AND (
"Invoice"."actionType" = 'ITEM_CREATE' OR
"Invoice"."actionType" = 'ZAP' OR
"Invoice"."actionType" = 'DOWN_ZAP' OR
"Invoice"."actionType" = 'POLL_VOTE' OR
"Invoice"."actionType" = 'BOOST'
)
ORDER BY "sortTime" DESC
LIMIT ${LIMIT})`
)
const notifications = await models.$queryRawUnsafe(
`SELECT id, "sortTime", "earnedSats", type,
"sortTime" AS "minSortTime"
FROM
(${queries.join(' UNION ALL ')}) u
ORDER BY "sortTime" DESC
LIMIT ${LIMIT}`, me.id, decodedCursor.time)
if (decodedCursor.offset === 0) {
await models.user.update({ where: { id: me.id }, data: { checkedNotesAt: new Date() } })
@ -288,7 +390,7 @@ export default {
return {
lastChecked: meFull.checkedNotesAt,
cursor: notifications.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
cursor: notifications.length === LIMIT ? nextNoteCursorEncoded(decodedCursor, notifications) : null,
notifications
}
}
@ -296,10 +398,10 @@ export default {
Mutation: {
savePushSubscription: async (parent, { endpoint, p256dh, auth, oldEndpoint }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await ssValidate(pushSubscriptionSchema, { endpoint, p256dh, auth })
await validateSchema(pushSubscriptionSchema, { endpoint, p256dh, auth })
let dbPushSubscription
if (oldEndpoint) {
@ -320,12 +422,12 @@ export default {
},
deletePushSubscription: async (parent, { endpoint }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const subscription = await models.pushSubscription.findFirst({ where: { endpoint, userId: Number(me.id) } })
if (!subscription) {
throw new GraphQLError('endpoint not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('endpoint not found')
}
const deletedSubscription = await models.pushSubscription.delete({ where: { id: subscription.id } })
console.log(`[webPush] deleted subscription ${deletedSubscription.id} of user ${deletedSubscription.userId} due to client request`)
@ -348,6 +450,21 @@ export default {
FollowActivity: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
TerritoryPost: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
Reminder: {
item: async (n, args, { models, me }) => {
const { itemId } = await models.reminder.findUnique({ where: { id: Number(n.id) } })
return await getItem(n, { id: itemId }, { models, me })
}
},
TerritoryTransfer: {
sub: async (n, args, { models, me }) => {
const transfer = await models.territoryTransfer.findUnique({ where: { id: Number(n.id) }, include: { sub: true } })
return transfer.sub
}
},
JobChanged: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
@ -365,6 +482,24 @@ export default {
return subAct.subName
}
},
ReferralSource: {
__resolveType: async (n, args, { models }) => n.type
},
Referral: {
source: async (n, args, { models, me }) => {
// retrieve the referee landing record
const referral = await models.oneDayReferral.findFirst({ where: { refereeId: Number(n.id), landing: true } })
if (!referral) return null // if no landing record, it will return a generic referral
switch (referral.type) {
case 'POST':
case 'COMMENT': return { ...await getItem(n, { id: referral.typeId }, { models, me }), type: 'Item' }
case 'TERRITORY': return { ...await getSub(n, { name: referral.typeId }, { models, me }), type: 'Sub' }
case 'PROFILE': return { ...await models.user.findUnique({ where: { id: Number(referral.typeId) }, select: { name: true } }), type: 'User' }
default: return null
}
}
},
Streak: {
days: async (n, args, { models }) => {
const res = await models.$queryRaw`
@ -374,6 +509,14 @@ export default {
`
return res.length ? res[0].days : null
},
type: async (n, args, { models }) => {
const res = await models.$queryRaw`
SELECT "type"
FROM "Streak"
WHERE id = ${Number(n.id)}
`
return res.length ? res[0].type : null
}
},
Earn: {
@ -398,13 +541,38 @@ export default {
return null
}
},
ReferralReward: {
sources: async (n, args, { me, models }) => {
const [sources] = await models.$queryRawUnsafe(`
SELECT
COALESCE(FLOOR(sum(msats) FILTER(WHERE type = 'FOREVER_REFERRAL') / 1000), 0) AS forever,
COALESCE(FLOOR(sum(msats) FILTER(WHERE type = 'ONE_DAY_REFERRAL') / 1000), 0) AS "oneDay"
FROM "Earn"
WHERE "userId" = $1 AND created_at = $2
`, Number(me.id), new Date(n.sortTime))
if (sources.forever + sources.oneDay > 0) {
return sources
}
return null
}
},
Mention: {
mention: async (n, args, { models }) => true,
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
ItemMention: {
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
},
InvoicePaid: {
invoice: async (n, args, { me, models }) => getInvoice(n, { id: n.id }, { me, models })
},
Invoicification: {
invoice: async (n, args, { me, models }) => getInvoice(n, { id: n.id }, { me, models })
},
WithdrawlPaid: {
withdrawl: async (n, args, { me, models }) => getWithdrawl(n, { id: n.id }, { me, models })
},
Invitification: {
invite: async (n, args, { models }) => {
return await models.invite.findUnique({

View File

@ -1,13 +1,11 @@
import { GraphQLError } from 'graphql'
import { GqlAuthorizationError } from '@/lib/error'
// this function makes america more secure apparently
export default async function assertGofacYourself ({ models, headers, ip }) {
const country = await gOFACYourself({ models, headers, ip })
if (!country) return
throw new GraphQLError(
`Your IP address is in ${country}. We cannot provide financial services to residents of ${country}.`,
{ extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthorizationError(`Your IP address is in ${country}. We cannot provide financial services to residents of ${country}.`)
}
export async function gOFACYourself ({ models, headers = {}, ip }) {

View File

@ -0,0 +1,89 @@
import { retryPaidAction } from '../paidAction'
import { USER_ID, WALLET_MAX_RETRIES, WALLET_RETRY_TIMEOUT_MS } from '@/lib/constants'
function paidActionType (actionType) {
switch (actionType) {
case 'ITEM_CREATE':
case 'ITEM_UPDATE':
return 'ItemPaidAction'
case 'ZAP':
case 'DOWN_ZAP':
case 'BOOST':
return 'ItemActPaidAction'
case 'TERRITORY_CREATE':
case 'TERRITORY_UPDATE':
case 'TERRITORY_BILLING':
case 'TERRITORY_UNARCHIVE':
return 'SubPaidAction'
case 'DONATE':
return 'DonatePaidAction'
case 'POLL_VOTE':
return 'PollVotePaidAction'
case 'RECEIVE':
return 'ReceivePaidAction'
case 'BUY_CREDITS':
return 'BuyCreditsPaidAction'
default:
throw new Error('Unknown action type')
}
}
export default {
Query: {
paidAction: async (parent, { invoiceId }, { models, me }) => {
const invoice = await models.invoice.findUnique({
where: {
id: invoiceId,
userId: me?.id ?? USER_ID.anon
}
})
if (!invoice) {
throw new Error('Invoice not found')
}
return {
type: paidActionType(invoice.actionType),
invoice,
result: invoice.actionResult,
paymentMethod: invoice.actionOptimistic ? 'OPTIMISTIC' : 'PESSIMISTIC'
}
}
},
Mutation: {
retryPaidAction: async (parent, { invoiceId, newAttempt }, { models, me, lnd }) => {
if (!me) {
throw new Error('You must be logged in')
}
// make sure only one client at a time can retry by acquiring a lock that expires
const [invoice] = await models.$queryRaw`
UPDATE "Invoice"
SET "retryPendingSince" = now()
WHERE
id = ${invoiceId} AND
"userId" = ${me.id} AND
"actionState" = 'FAILED' AND
("retryPendingSince" IS NULL OR "retryPendingSince" < now() - ${`${WALLET_RETRY_TIMEOUT_MS} milliseconds`}::interval)
RETURNING *`
if (!invoice) {
throw new Error('Invoice not found or retry pending')
}
// do we want to retry a payment from the beginning with all sender and receiver wallets?
const paymentAttempt = newAttempt ? invoice.paymentAttempt + 1 : invoice.paymentAttempt
if (paymentAttempt > WALLET_MAX_RETRIES) {
throw new Error('Payment has been retried too many times')
}
const result = await retryPaidAction(invoice.actionType, { invoice }, { paymentAttempt, models, me, lnd })
return {
...result,
type: paidActionType(invoice.actionType)
}
}
},
PaidAction: {
__resolveType: obj => obj.type
}
}

View File

@ -1,36 +1,27 @@
const cache = new Map()
const expiresIn = 30000 // in milliseconds
import { SUPPORTED_CURRENCIES } from '@/lib/currency'
import { cachedFetcher } from '@/lib/fetch'
async function fetchPrice (fiat) {
const getPrice = cachedFetcher(async function fetchPrice (fiat = 'USD') {
const url = `https://api.coinbase.com/v2/prices/BTC-${fiat}/spot`
const price = await fetch(url)
.then((res) => res.json())
.then((body) => parseFloat(body.data.amount))
.catch((err) => {
console.error(err)
return -1
})
cache.set(fiat, { price, createdAt: Date.now() })
return price
}
async function getPrice (fiat) {
fiat ??= 'USD'
if (cache.has(fiat)) {
const { price, createdAt } = cache.get(fiat)
const expired = createdAt + expiresIn < Date.now()
if (expired) fetchPrice(fiat).catch(console.error) // update cache
return price // serve stale price (this on the SSR critical path)
} else {
fetchPrice(fiat).catch(console.error)
try {
const res = await fetch(url)
const body = await res.json()
return parseFloat(body.data.amount)
} catch (err) {
console.error(err)
return -1
}
return null
}
}, {
maxSize: SUPPORTED_CURRENCIES.length,
cacheExpiry: 60 * 1000, // 1 minute
forceRefreshThreshold: 0, // never force refresh
keyGenerator: (fiat = 'USD') => fiat
})
export default {
Query: {
price: async (parent, { fiatCurrency }, ctx) => {
return await getPrice(fiatCurrency)
return await getPrice(fiatCurrency) || -1
}
}
}

View File

@ -1,56 +1,28 @@
import { GraphQLError } from 'graphql'
import { withClause, intervalClause } from './growth'
import { timeUnitForRange, whenRange } from '../../lib/time'
import { timeUnitForRange, whenRange } from '@/lib/time'
import { viewGroup } from './growth'
import { GqlAuthenticationError } from '@/lib/error'
export default {
Query: {
referrals: async (parent, { when, from, to }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const range = whenRange(when, from, to)
const [{ totalSats }] = await models.$queryRawUnsafe(`
SELECT COALESCE(FLOOR(sum(msats) / 1000), 0) as "totalSats"
FROM "ReferralAct"
WHERE ${intervalClause(range, 'ReferralAct')}
AND "ReferralAct"."referrerId" = $3
`, ...range, Number(me.id))
const [{ totalReferrals }] = await models.$queryRawUnsafe(`
SELECT count(*)::INTEGER as "totalReferrals"
FROM users
WHERE ${intervalClause(range, 'users')}
AND "referrerId" = $3
`, ...range, Number(me.id))
const stats = await models.$queryRawUnsafe(
`${withClause(range)}
SELECT time, json_build_array(
json_build_object('name', 'referrals', 'value', count(*) FILTER (WHERE act = 'REFERREE')),
json_build_object('name', 'sats', 'value', FLOOR(COALESCE(sum(msats) FILTER (WHERE act IN ('BOOST', 'STREAM', 'FEE')), 0)))
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
json_build_array(
json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0)),
json_build_object('name', 'one day referrals', 'value', COALESCE(SUM(one_day_referrals), 0)),
json_build_object('name', 'referral sats', 'value', FLOOR(COALESCE(SUM(msats_referrals), 0) / 1000.0)),
json_build_object('name', 'one day referral sats', 'value', FLOOR(COALESCE(SUM(msats_one_day_referrals), 0) / 1000.0))
) AS data
FROM times
LEFT JOIN
((SELECT "ReferralAct".created_at, "ReferralAct".msats / 1000.0 as msats, "ItemAct".act::text as act
FROM "ReferralAct"
JOIN "ItemAct" ON "ItemAct".id = "ReferralAct"."itemActId"
WHERE ${intervalClause(range, 'ReferralAct')}
AND "ReferralAct"."referrerId" = $3)
UNION ALL
(SELECT created_at, 0.0 as sats, 'REFERREE' as act
FROM users
WHERE ${intervalClause(range, 'users')}
AND "referrerId" = $3)) u ON time = date_trunc('${timeUnitForRange(range)}', u.created_at)
GROUP BY time
ORDER BY time ASC`, ...range, Number(me.id))
return {
totalSats,
totalReferrals,
stats
}
FROM ${viewGroup(range, 'user_stats')}
WHERE id = ${me.id}
GROUP BY time
ORDER BY time ASC`, ...range)
}
}
}

View File

@ -1,39 +1,86 @@
import { GraphQLError } from 'graphql'
import { amountSchema, ssValidate } from '../../lib/validate'
import { serializeInvoicable } from './serial'
import { ANON_USER_ID } from '../../lib/constants'
import { getItem } from './item'
import { amountSchema, validateSchema } from '@/lib/validate'
import { getAd, getItem } from './item'
import { topUsers } from './user'
import performPaidAction from '../paidAction'
import { GqlInputError } from '@/lib/error'
const rewardCache = new Map()
let rewardCache
async function updateCachedRewards (when, models) {
const rewards = await getRewards(when, models)
rewardCache.set(when, { rewards, createdAt: Date.now() })
async function updateCachedRewards (models) {
const rewards = await getActiveRewards(models)
rewardCache = { rewards, createdAt: Date.now() }
return rewards
}
async function getCachedRewards (staleIn, when, models) {
if (rewardCache.has(when)) {
const { rewards, createdAt } = rewardCache.get(when)
async function getCachedActiveRewards (staleIn, models) {
if (rewardCache) {
const { rewards, createdAt } = rewardCache
const expired = createdAt + staleIn < Date.now()
if (expired) updateCachedRewards(when, models).catch(console.error)
if (expired) updateCachedRewards(models).catch(console.error)
return rewards // serve stale rewards
}
return await updateCachedRewards(when, models)
return await updateCachedRewards(models)
}
async function getActiveRewards (models) {
return await models.$queryRaw`
SELECT
(sum(total) / 1000)::INT as total,
date_trunc('day', (now() AT TIME ZONE 'America/Chicago') + interval '1 day') AT TIME ZONE 'America/Chicago' as time,
json_build_array(
json_build_object('name', 'donations', 'value', (sum(donations) / 1000)::INT),
json_build_object('name', 'fees', 'value', (sum(fees) / 1000)::INT),
json_build_object('name', 'boost', 'value', (sum(boost) / 1000)::INT),
json_build_object('name', 'jobs', 'value', (sum(jobs) / 1000)::INT),
json_build_object('name', 'anon''s stack', 'value', (sum(anons_stack) / 1000)::INT)
) AS sources
FROM (
(SELECT * FROM rewards_today)
UNION ALL
(SELECT * FROM
rewards(
date_trunc('hour', timezone('America/Chicago', now())),
date_trunc('hour', timezone('America/Chicago', now())), '1 hour'::INTERVAL, 'hour'))
) u`
}
async function getMonthlyRewards (when, models) {
return await models.$queryRaw`
SELECT
(sum(total) / 1000)::INT as total,
date_trunc('month', ${when?.[0]}::text::timestamp) AT TIME ZONE 'America/Chicago' as time,
json_build_array(
json_build_object('name', 'donations', 'value', (sum(donations) / 1000)::INT),
json_build_object('name', 'fees', 'value', (sum(fees) / 1000)::INT),
json_build_object('name', 'boost', 'value', (sum(boost) / 1000)::INT),
json_build_object('name', 'jobs', 'value', (sum(jobs) / 1000)::INT),
json_build_object('name', 'anon''s stack', 'value', (sum(anons_stack) / 1000)::INT)
) AS sources
FROM rewards_days
WHERE date_trunc('month', rewards_days.t) = date_trunc('month', ${when?.[0]}::text::timestamp - interval '1 month')`
}
async function getRewards (when, models) {
if (when) {
if (when.length > 2) {
throw new GraphQLError('too many dates', { extensions: { code: 'BAD_USER_INPUT' } })
if (when.length > 1) {
throw new GqlInputError('too many dates')
}
when.forEach(w => {
if (isNaN(new Date(w))) {
throw new GraphQLError('invalid date', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('invalid date')
}
})
if (new Date(when[0]) > new Date(when[when.length - 1])) {
throw new GraphQLError('bad date range', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('bad date range')
}
if (new Date(when[0]).getTime() > new Date('2024-03-01').getTime() && new Date(when[0]).getTime() < new Date('2024-05-02').getTime()) {
// after 3/1/2024 and until 5/1/2024, we reward monthly on the 1st
if (new Date(when[0]).getUTCDate() !== 1) {
throw new GqlInputError('bad reward date')
}
return await getMonthlyRewards(when, models)
}
}
@ -45,37 +92,18 @@ async function getRewards (when, models) {
COALESCE(${when?.[when.length - 1]}::text::timestamp - interval '1 day', now() AT TIME ZONE 'America/Chicago'),
interval '1 day') AS t
)
SELECT coalesce(FLOOR(sum(sats)), 0) as total,
SELECT (total / 1000)::INT as total,
days_cte.day + interval '1 day' as time,
json_build_array(
json_build_object('name', 'donations', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'DONATION')), 0)),
json_build_object('name', 'fees', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM', 'DONATION', 'ANON'))), 0)),
json_build_object('name', 'boost', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'BOOST')), 0)),
json_build_object('name', 'jobs', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'STREAM')), 0)),
json_build_object('name', 'anon''s stack', 'value', coalesce(FLOOR(sum(sats) FILTER(WHERE type = 'ANON')), 0))
json_build_object('name', 'donations', 'value', donations / 1000),
json_build_object('name', 'fees', 'value', fees / 1000),
json_build_object('name', 'boost', 'value', boost / 1000),
json_build_object('name', 'jobs', 'value', jobs / 1000),
json_build_object('name', 'anon''s stack', 'value', anons_stack / 1000)
) AS sources
FROM days_cte
CROSS JOIN LATERAL (
(SELECT ("ItemAct".msats - COALESCE("ReferralAct".msats, 0)) / 1000.0 as sats, act::text as type
FROM "ItemAct"
LEFT JOIN "ReferralAct" ON "ReferralAct"."itemActId" = "ItemAct".id
WHERE date_trunc('day', "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = days_cte.day AND "ItemAct".act <> 'TIP')
UNION ALL
(SELECT sats::FLOAT, 'DONATION' as type
FROM "Donation"
WHERE date_trunc('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = days_cte.day)
UNION ALL
-- any earnings from anon's stack that are not forwarded to other users
(SELECT "ItemAct".msats / 1000.0 as sats, 'ANON' as type
FROM "Item"
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
LEFT JOIN "ItemForward" ON "ItemForward"."itemId" = "Item".id
WHERE "Item"."userId" = ${ANON_USER_ID} AND "ItemAct".act = 'TIP'
AND date_trunc('day', "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = days_cte.day
GROUP BY "ItemAct".id, "ItemAct".msats
HAVING COUNT("ItemForward".id) = 0)
) subquery
GROUP BY days_cte.day
JOIN rewards_days ON rewards_days.t = days_cte.day
GROUP BY days_cte.day, total, donations, fees, boost, jobs, anons_stack
ORDER BY days_cte.day ASC`
return results.length ? results : [{ total: 0, time: '0', sources: [] }]
@ -84,18 +112,18 @@ async function getRewards (when, models) {
export default {
Query: {
rewards: async (parent, { when }, { models }) =>
when ? await getRewards(when, models) : await getCachedRewards(5000, when, models),
when ? await getRewards(when, models) : await getCachedActiveRewards(5000, models),
meRewards: async (parent, { when }, { me, models }) => {
if (!me) {
return null
}
if (!when || when.length > 2) {
throw new GraphQLError('invalid date range', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('bad date range')
}
for (const w of when) {
if (isNaN(new Date(w))) {
throw new GraphQLError('invalid date', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('invalid date')
}
}
@ -113,6 +141,7 @@ export default {
(SELECT FLOOR("Earn".msats / 1000.0) as sats, type, rank, "typeId"
FROM "Earn"
WHERE "Earn"."userId" = ${me.id}
AND (type IS NULL OR type NOT IN ('FOREVER_REFERRAL', 'ONE_DAY_REFERRAL'))
AND date_trunc('day', "Earn".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = days_cte.day
ORDER BY "Earn".msats DESC)
) "Earn"
@ -122,16 +151,29 @@ export default {
return results
}
},
Rewards: {
leaderboard: async (parent, args, { models, ...context }) => {
// get to and from using postgres because it's easier to do there
const [{ to, from }] = await models.$queryRaw`
SELECT date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago' as from,
(date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') + interval '1 day - 1 second' as to`
return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 500 }, { models, ...context })
},
total: async (parent, args, { models }) => {
if (!parent.total) {
return 0
}
return parent.total
},
ad: async (parent, args, { me, models }) => {
return await getAd(parent, { }, { me, models })
}
},
Mutation: {
donateToRewards: async (parent, { sats, hash, hmac }, { me, models, lnd }) => {
await ssValidate(amountSchema, { amount: sats })
donateToRewards: async (parent, { sats }, { me, models, lnd }) => {
await validateSchema(amountSchema, { amount: sats })
await serializeInvoicable(
models.$queryRaw`SELECT donate(${sats}::INTEGER, ${me?.id || ANON_USER_ID}::INTEGER)`,
{ models, lnd, hash, hmac, me, enforceFee: sats }
)
return sats
return await performPaidAction('DONATE', { sats }, { me, models, lnd })
}
},
Reward: {

View File

@ -1,24 +1,29 @@
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { whenToFrom } from '../../lib/time'
import { getItem } from './item'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { whenToFrom } from '@/lib/time'
import { getItem, itemQueryWithMeta, SELECT } from './item'
const STOP_WORDS = ['a', 'an', 'and', 'are', 'as', 'at', 'be', 'but',
'by', 'for', 'if', 'in', 'into', 'is', 'it', 'no', 'not',
'of', 'on', 'or', 'such', 'that', 'the', 'their', 'then',
'there', 'these', 'they', 'this', 'to', 'was', 'will',
'with', 'bitcoin', 'page', 'adds', 'how', 'why', 'what',
'works', 'now', 'available', 'breaking', 'app', 'powered',
'just', 'dev', 'using', 'crypto', 'has', 'my', 'i', 'apps',
'really', 'new', 'era', 'application', 'best', 'year',
'latest', 'still', 'few', 'crypto', 'keep', 'public', 'current',
'levels', 'from', 'cryptocurrencies', 'confirmed', 'news', 'network',
'about', 'sources', 'vote', 'considerations', 'hope',
'keep', 'keeps', 'including', 'we', 'brings', "don't", 'do',
'interesting', 'us', 'welcome', 'thoughts', 'results']
function queryParts (q) {
const regex = /"([^"]*)"/gm
const queryArr = q.replace(regex, '').trim().split(/\s+/)
const url = queryArr.find(word => word.startsWith('url:'))
const nym = queryArr.find(word => word.startsWith('@'))
const territory = queryArr.find(word => word.startsWith('~'))
const exclude = [url, nym, territory]
const query = queryArr.filter(word => !exclude.includes(word)).join(' ')
return {
quotes: [...q.matchAll(regex)].map(m => m[1]),
nym,
url,
territory,
query
}
}
export default {
Query: {
related: async (parent, { title, id, cursor, limit, minMatch }, { me, models, search }) => {
related: async (parent, { title, id, cursor, limit = LIMIT, minMatch }, { me, models, search }) => {
const decodedCursor = decodeCursor(cursor)
if (!id && (!title || title.trim().split(/\s+/).length < 1)) {
@ -31,7 +36,7 @@ export default {
const like = []
if (id) {
like.push({
_index: 'item',
_index: process.env.OPENSEARCH_INDEX,
_id: id
})
}
@ -40,95 +45,125 @@ export default {
like.push(title)
}
const mustNot = []
const mustNot = [{ exists: { field: 'parentId' } }]
if (id) {
mustNot.push({ term: { id } })
}
let items = await search.search({
index: 'item',
size: limit || LIMIT,
let should = [
{
more_like_this: {
fields: ['title', 'text'],
like,
min_term_freq: 1,
min_doc_freq: 1,
max_doc_freq: 5,
min_word_length: 2,
max_query_terms: 25,
minimum_should_match: minMatch || '10%',
boost_terms: 100
}
}
]
if (process.env.OPENSEARCH_MODEL_ID) {
let qtitle = title
let qtext = title
if (id) {
const item = await getItem(parent, { id }, { me, models })
qtitle = item.title || item.text
qtext = item.text || item.title
}
should = [
{
neural: {
title_embedding: {
query_text: qtext,
model_id: process.env.OPENSEARCH_MODEL_ID,
k: decodedCursor.offset + LIMIT
}
}
},
{
neural: {
text_embedding: {
query_text: qtitle,
model_id: process.env.OPENSEARCH_MODEL_ID,
k: decodedCursor.offset + LIMIT
}
}
}
]
}
const results = await search.search({
index: process.env.OPENSEARCH_INDEX,
size: limit,
from: decodedCursor.offset,
_source: {
excludes: [
'text',
'text_embedding',
'title_embedding'
]
},
body: {
query: {
function_score: {
query: {
bool: {
should: [
should,
filter: [
{
more_like_this: {
fields: ['title'],
like,
min_term_freq: 1,
min_doc_freq: 1,
min_word_length: 2,
max_query_terms: 12,
minimum_should_match: minMatch || '80%',
stop_words: STOP_WORDS,
boost: 10000
bool: {
should: [
{ match: { status: 'ACTIVE' } },
{ match: { status: 'NOSATS' } }
],
must_not: mustNot
}
},
{
more_like_this: {
fields: ['title'],
like,
min_term_freq: 1,
min_doc_freq: 1,
min_word_length: 2,
max_query_terms: 12,
minimum_should_match: minMatch || '60%',
stop_words: STOP_WORDS,
boost: 1000
}
},
{
more_like_this: {
fields: ['title'],
like,
min_term_freq: 1,
min_doc_freq: 1,
min_word_length: 2,
max_query_terms: 12,
minimum_should_match: minMatch || '30%',
stop_words: STOP_WORDS,
boost: 100
}
},
{
more_like_this: {
fields: ['text'],
like,
min_term_freq: 1,
min_doc_freq: 1,
min_word_length: 2,
max_query_terms: 25,
minimum_should_match: minMatch || '30%',
stop_words: STOP_WORDS,
boost: 10
}
range: { wvotes: { gte: minMatch ? 0 : 0.2 } }
}
],
must_not: [{ exists: { field: 'parentId' } }, ...mustNot],
filter: {
range: { wvotes: { gte: minMatch ? 0 : 0.2 } }
}
]
}
},
field_value_factor: {
field: 'wvotes',
modifier: 'log1p',
factor: 1.2,
missing: 0
},
functions: [{
field_value_factor: {
field: 'wvotes',
modifier: 'none',
factor: 1,
missing: 0
}
}],
boost_mode: 'multiply'
}
}
}
})
items = items.body.hits.hits.map(async e => {
// this is super inefficient but will suffice until we do something more generic
return await getItem(parent, { id: e._source.id }, { me, models })
const values = results.body.hits.hits.map((e, i) => {
return `(${e._source.id}, ${i})`
}).join(',')
if (values.length === 0) {
return {
cursor: null,
items: []
}
}
const items = await itemQueryWithMeta({
me,
models,
query: `
WITH r(id, rank) AS (VALUES ${values})
${SELECT}, rank
FROM "Item"
JOIN r ON "Item".id = r.id`,
orderBy: 'ORDER BY rank ASC'
})
return {
@ -136,189 +171,289 @@ export default {
items
}
},
search: async (parent, { q: query, sub, cursor, sort, what, when, from: whenFrom, to: whenTo }, { me, models, search }) => {
search: async (parent, { q, cursor, sort, what, when, from: whenFrom, to: whenTo }, { me, models, search }) => {
const decodedCursor = decodeCursor(cursor)
let sitems
let sitems = null
if (!query) {
// short circuit: return empty result if either:
// 1. no query provided, or
// 2. searching bookmarks without being authed
if (!q || (what === 'bookmarks' && !me)) {
return {
items: [],
cursor: null
}
}
const whatArr = []
// build query in parts:
// filters: determine the universe of potential search candidates
// termQueries: queries related to the actual search terms
// functions: rank modifiers to boost by recency or popularity
const filters = []
const termQueries = []
const functions = []
// filters for item types
switch (what) {
case 'posts':
whatArr.push({ bool: { must_not: { exists: { field: 'parentId' } } } })
case 'posts': // posts only
filters.push({ bool: { must_not: { exists: { field: 'parentId' } } } })
break
case 'comments':
whatArr.push({ bool: { must: { exists: { field: 'parentId' } } } })
case 'comments': // comments only
filters.push({ bool: { must: { exists: { field: 'parentId' } } } })
break
case 'bookmarks':
if (me?.id) {
filters.push({ match: { bookmarkedBy: me?.id } })
}
break
default:
break
}
const queryArr = query.trim().split(/\s+/)
const url = queryArr.find(word => word.startsWith('url:'))
const nym = queryArr.find(word => word.startsWith('nym:'))
const exclude = [url, nym]
query = queryArr.filter(word => !exclude.includes(word)).join(' ')
if (url) {
whatArr.push({ match_phrase_prefix: { url: `${url.slice(4).toLowerCase()}` } })
}
if (nym) {
whatArr.push({ wildcard: { 'user.name': `*${nym.slice(4).toLowerCase()}*` } })
}
if (sub) {
whatArr.push({ match: { 'sub.name': sub } })
}
const should = [
{
// all terms are matched in fields
multi_match: {
query,
type: 'most_fields',
fields: ['title^1000', 'text'],
minimum_should_match: '100%',
boost: 10000
}
},
{
// all terms are matched in fields fuzzily
multi_match: {
query,
type: 'most_fields',
fields: ['title^1000', 'text'],
minimum_should_match: '60%',
boost: 1000
}
}
]
let boostMode = 'multiply'
let sortField
let sortMod = 'log1p'
switch (sort) {
case 'comments':
sortField = 'ncomments'
sortMod = 'square'
break
case 'sats':
sortField = 'sats'
break
case 'recent':
sortField = 'createdAt'
sortMod = 'square'
boostMode = 'replace'
break
default:
sortField = 'wvotes'
sortMod = 'none'
break
}
const functions = [
{
field_value_factor: {
field: sortField,
modifier: sortMod,
factor: 1.2
}
}
]
// allow fuzzy matching for single terms
if (sort !== 'recent') {
should.push({
// only some terms must match unless we're sorting
multi_match: {
query,
type: 'most_fields',
fields: ['title^1000', 'text'],
fuzziness: 'AUTO',
prefix_length: 3,
minimum_should_match: '60%'
}
})
// small bias toward posts with comments
functions.push({
field_value_factor: {
field: 'ncomments',
modifier: 'ln1p',
factor: 1
}
})
}
if (query.length) {
whatArr.push({
bool: {
should
}
})
}
// filter for active posts
filters.push(
me
? {
bool: {
should: [
{ match: { status: 'ACTIVE' } },
{ match: { status: 'NOSATS' } },
{ match: { userId: me.id } }
]
}
}
: {
bool: {
should: [
{ match: { status: 'ACTIVE' } },
{ match: { status: 'NOSATS' } }
]
}
}
)
// filter for time range
const whenRange = when === 'custom'
? {
gte: whenFrom,
lte: new Date(Math.min(new Date(whenTo), decodedCursor.time))
lte: new Date(Math.min(new Date(Number(whenTo)), decodedCursor.time))
}
: {
lte: decodedCursor.time,
gte: whenToFrom(when)
}
filters.push({ range: { createdAt: whenRange } })
// filter for non negative wvotes
filters.push({ range: { wvotes: { gte: 0 } } })
// decompose the search terms
const { query: _query, quotes, nym, url, territory } = queryParts(q)
const query = _query
// if search contains a url term, modify the query text
if (url) {
const uri = url.slice(4)
let uriObj
try {
uriObj = new URL(uri)
} catch {
try {
uriObj = new URL(`https://${uri}`)
} catch {}
}
if (uriObj) {
termQueries.push({
wildcard: { url: `*${uriObj?.hostname ?? uri}${uriObj?.pathname ?? ''}*` }
})
termQueries.push({
match: { text: `${uriObj?.hostname ?? uri}${uriObj?.pathname ?? ''}` }
})
}
}
// if nym, items must contain nym
if (nym) {
filters.push({ wildcard: { 'user.name': `*${nym.slice(1).toLowerCase()}*` } })
// push same requirement to termQueries to avoid empty should clause
termQueries.push({ wildcard: { 'user.name': `*${nym.slice(1).toLowerCase()}*` } })
}
// if territory, item must be from territory
if (territory) {
filters.push({ match: { 'sub.name': territory.slice(1) } })
// push same requirement to termQueries to avoid empty should clause
termQueries.push({ match: { 'sub.name': territory.slice(1) } })
}
// if quoted phrases, items must contain entire phrase
for (const quote of quotes) {
termQueries.push({
multi_match: {
query: quote,
type: 'phrase',
fields: ['title', 'text']
}
})
// force the search to include the quoted phrase
filters.push({
multi_match: {
query: quote,
type: 'phrase',
fields: ['title', 'text']
}
})
}
// functions for boosting search rank by recency or popularity
switch (sort) {
case 'comments':
functions.push({
field_value_factor: {
field: 'ncomments',
modifier: 'log1p'
}
})
break
case 'sats':
functions.push({
field_value_factor: {
field: 'sats',
modifier: 'log1p'
}
})
break
case 'recent':
functions.push({
gauss: {
createdAt: {
origin: 'now',
scale: '7d',
decay: 0.5
}
}
})
break
case 'zaprank':
functions.push({
field_value_factor: {
field: 'wvotes',
modifier: 'log1p'
}
})
break
default:
break
}
let osQuery = {
function_score: {
query: {
bool: {
filter: filters,
should: termQueries,
minimum_should_match: termQueries.length > 0 ? 1 : 0
}
},
functions,
score_mode: 'multiply',
boost_mode: 'multiply'
}
}
// query for search terms
if (query.length) {
// keyword based subquery, to be used on its own or in conjunction with a neural
// search
const subquery = [
{
multi_match: {
query,
type: 'best_fields',
fields: ['title^10', 'text'],
fuzziness: 'AUTO',
minimum_should_match: 1
}
},
// all match matches higher
{
multi_match: {
query,
type: 'best_fields',
fields: ['title^10', 'text'],
minimum_should_match: '100%',
boost: 100
}
},
// phrase match matches higher
{
multi_match: {
query,
type: 'phrase',
fields: ['title^10', 'text'],
boost: 1000
}
}
]
osQuery.function_score.query.bool.should = [...termQueries, ...subquery]
osQuery.function_score.query.bool.minimum_should_match = 1
// use hybrid neural search if model id is available, otherwise use only
// keyword search
if (process.env.OPENSEARCH_MODEL_ID) {
osQuery = {
hybrid: {
queries: [
{
bool: {
should: [
{
neural: {
title_embedding: {
query_text: query,
model_id: process.env.OPENSEARCH_MODEL_ID,
k: decodedCursor.offset + LIMIT
}
}
},
{
neural: {
text_embedding: {
query_text: query,
model_id: process.env.OPENSEARCH_MODEL_ID,
k: decodedCursor.offset + LIMIT
}
}
}
],
filter: filters,
minimum_should_match: 1
}
},
osQuery
]
}
}
}
}
try {
sitems = await search.search({
index: 'item',
index: process.env.OPENSEARCH_INDEX,
size: LIMIT,
_source: {
excludes: [
'text',
'text_embedding',
'title_embedding'
]
},
from: decodedCursor.offset,
body: {
query: {
function_score: {
query: {
bool: {
must: [
...whatArr,
me
? {
bool: {
should: [
{ match: { status: 'ACTIVE' } },
{ match: { status: 'NOSATS' } },
{ match: { userId: me.id } }
]
}
}
: {
bool: {
should: [
{ match: { status: 'ACTIVE' } },
{ match: { status: 'NOSATS' } }
]
}
}
],
filter: [
{
range:
{
createdAt: whenRange
}
},
{ range: { wvotes: { gte: 0 } } }
]
}
},
functions,
boost_mode: boostMode
}
},
query: osQuery,
highlight: {
fields: {
title: { number_of_fragments: 0, pre_tags: ['***'], post_tags: ['***'] },
@ -335,14 +470,30 @@ export default {
}
}
// return highlights
const items = sitems.body.hits.hits.map(async e => {
// this is super inefficient but will suffice until we do something more generic
const item = await getItem(parent, { id: e._source.id }, { me, models })
const values = sitems.body.hits.hits.map((e, i) => {
return `(${e._source.id}, ${i})`
}).join(',')
if (values.length === 0) {
return {
cursor: null,
items: []
}
}
const items = (await itemQueryWithMeta({
me,
models,
query: `
WITH r(id, rank) AS (VALUES ${values})
${SELECT}, rank
FROM "Item"
JOIN r ON "Item".id = r.id`,
orderBy: 'ORDER BY rank ASC, msats DESC'
})).map((item, i) => {
const e = sitems.body.hits.hits[i]
item.searchTitle = (e.highlight?.title && e.highlight.title[0]) || item.title
item.searchText = (e.highlight?.text && e.highlight.text.join(' ... ')) || undefined
return item
})

View File

@ -1,141 +0,0 @@
import { GraphQLError } from 'graphql'
import retry from 'async-retry'
import Prisma from '@prisma/client'
import { settleHodlInvoice } from 'ln-service'
import { createHmac } from './wallet'
import { msatsToSats, numWithUnits } from '../../lib/format'
import { BALANCE_LIMIT_MSATS } from '../../lib/constants'
export default async function serialize (models, ...calls) {
return await retry(async bail => {
try {
const [, ...result] = await models.$transaction(
[models.$executeRaw`SELECT ASSERT_SERIALIZED()`, ...calls],
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable })
return calls.length > 1 ? result : result[0]
} catch (error) {
console.log(error)
// two cases where we get insufficient funds:
// 1. plpgsql function raises
// 2. constraint violation via a prisma call
// XXX prisma does not provide a way to distinguish these cases so we
// have to check the error message
if (error.message.includes('SN_INSUFFICIENT_FUNDS') ||
error.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
bail(new GraphQLError('insufficient funds', { extensions: { code: 'BAD_INPUT' } }))
}
if (error.message.includes('SN_NOT_SERIALIZABLE')) {
bail(new Error('wallet balance transaction is not serializable'))
}
if (error.message.includes('SN_CONFIRMED_WITHDRAWL_EXISTS')) {
bail(new Error('withdrawal invoice already confirmed (to withdraw again create a new invoice)'))
}
if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) {
bail(new Error('withdrawal invoice exists and is pending'))
}
if (error.message.includes('SN_INELIGIBLE')) {
bail(new Error('user ineligible for gift'))
}
if (error.message.includes('SN_UNSUPPORTED')) {
bail(new Error('unsupported action'))
}
if (error.message.includes('SN_DUPLICATE')) {
bail(new Error('duplicate not allowed'))
}
if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
bail(new Error('faucet has been revoked or is exhausted'))
}
if (error.message.includes('SN_INV_PENDING_LIMIT')) {
bail(new Error('too many pending invoices'))
}
if (error.message.includes('SN_INV_EXCEED_BALANCE')) {
bail(new Error(`pending invoices must not cause balance to exceed ${numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))}`))
}
if (error.message.includes('40001') || error.code === 'P2034') {
throw new Error('wallet balance serialization failure - try again')
}
if (error.message.includes('23514') || ['P2002', 'P2003', 'P2004'].includes(error.code)) {
bail(new Error('constraint failure'))
}
bail(error)
}
}, {
minTimeout: 100,
factor: 1.1,
retries: 5
})
}
export async function serializeInvoicable (query, { models, lnd, hash, hmac, me, enforceFee }) {
if (!me && !hash) {
throw new Error('you must be logged in or pay')
}
let trx = Array.isArray(query) ? query : [query]
let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac, enforceFee)
trx = [
models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`,
...trx,
models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } })
]
}
let results = await serialize(models, ...trx)
if (hash) {
if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd })
results = results.slice(1, -1)
}
// if there is only one result, return it directly, else the array
results = results.flat(2)
return results.length > 1 ? results : results[0]
}
export async function checkInvoice (models, hash, hmac, fee) {
if (!hash) {
throw new GraphQLError('hash required', { extensions: { code: 'BAD_INPUT' } })
}
if (!hmac) {
throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
}
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' } })
}
const expired = new Date(invoice.expiresAt) <= new Date()
if (expired) {
throw new GraphQLError('invoice expired', { extensions: { code: 'BAD_INPUT' } })
}
if (invoice.confirmedAt) {
throw new GraphQLError('invoice already used', { extensions: { code: 'BAD_INPUT' } })
}
if (invoice.cancelled) {
throw new GraphQLError('invoice was canceled', { extensions: { code: 'BAD_INPUT' } })
}
if (!invoice.msatsReceived) {
throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
}
if (fee && msatsToSats(invoice.msatsReceived) < fee) {
throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
}
return invoice
}

View File

@ -1,62 +1,10 @@
import { GraphQLError } from 'graphql'
import serialize, { serializeInvoicable } from './serial'
import { TERRITORY_COST_MONTHLY, TERRITORY_COST_ONCE, TERRITORY_COST_YEARLY } from '../../lib/constants'
import { datePivot } from '../../lib/time'
import { ssValidate, territorySchema } from '../../lib/validate'
import { nextBilling, nextNextBilling } from '../../lib/territory'
export function paySubQueries (sub, models) {
if (sub.billingType === 'ONCE') {
return []
}
const billingAt = nextBilling(sub)
const billAt = nextNextBilling(sub)
const cost = BigInt(sub.billingCost) * BigInt(1000)
return [
models.user.update({
where: {
id: sub.userId
},
data: {
msats: {
decrement: cost
}
}
}),
// update 'em
models.sub.update({
where: {
name: sub.name
},
data: {
billedLastAt: billingAt,
status: 'ACTIVE'
}
}),
// record 'em
models.subAct.create({
data: {
userId: sub.userId,
subName: sub.name,
msats: cost,
type: 'BILLING'
}
}),
models.$executeRaw`
DELETE FROM pgboss.job
WHERE name = 'territoryBilling'
AND data->>'subName' = ${sub.name}
AND completedon IS NULL`,
// schedule 'em
models.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, keepuntil) VALUES ('territoryBilling',
${JSON.stringify({
subName: sub.name
})}::JSONB, ${billAt}, ${datePivot(billAt, { days: 1 })})`
]
}
import { whenRange } from '@/lib/time'
import { validateSchema, territorySchema } from '@/lib/validate'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { viewGroup } from './growth'
import { notifyTerritoryTransfer } from '@/lib/webPush'
import performPaidAction from '../paidAction'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
export async function getSub (parent, { name }, { models, me }) {
if (!name) return null
@ -72,6 +20,11 @@ export async function getSub (parent, { name }, { models, me }) {
where: {
userId: Number(me?.id)
}
},
SubSubscription: {
where: {
userId: Number(me?.id)
}
}
}
}
@ -84,21 +37,25 @@ export default {
sub: getSub,
subs: async (parent, args, { models, me }) => {
if (me) {
return await models.$queryRaw`
SELECT "Sub".*, COALESCE(json_agg("MuteSub".*) FILTER (WHERE "MuteSub"."userId" IS NOT NULL), '[]') AS "MuteSub"
const currentUser = await models.user.findUnique({ where: { id: me.id } })
const showNsfw = currentUser ? currentUser.nsfwMode : false
return await models.$queryRawUnsafe(`
SELECT "Sub".*, "Sub".created_at as "createdAt", COALESCE(json_agg("MuteSub".*) FILTER (WHERE "MuteSub"."userId" IS NOT NULL), '[]') AS "MuteSub"
FROM "Sub"
LEFT JOIN "MuteSub" ON "Sub".name = "MuteSub"."subName" AND "MuteSub"."userId" = ${me.id}::INTEGER
WHERE status <> 'STOPPED'
WHERE status <> 'STOPPED' ${showNsfw ? '' : 'AND "Sub"."nsfw" = FALSE'}
GROUP BY "Sub".name, "MuteSub"."userId"
ORDER BY "Sub".name ASC
`
`)
}
return await models.sub.findMany({
where: {
status: {
not: 'STOPPED'
}
},
nsfw: false
},
orderBy: {
name: 'asc'
@ -116,31 +73,99 @@ export default {
})
return latest?.createdAt
},
topSubs: async (parent, { cursor, when, by, from, to, limit = LIMIT }, { models, me }) => {
const decodedCursor = decodeCursor(cursor)
const range = whenRange(when, from, to || decodeCursor.time)
let column
switch (by) {
case 'revenue': column = 'revenue'; break
case 'spent': column = 'spent'; break
case 'posts': column = 'nposts'; break
case 'comments': column = 'ncomments'; break
default: column = 'stacked'; break
}
const subs = await models.$queryRawUnsafe(`
SELECT "Sub".*,
COALESCE(floor(sum(msats_revenue)/1000), 0) as revenue,
COALESCE(floor(sum(msats_stacked)/1000), 0) as stacked,
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
COALESCE(sum(posts), 0) as nposts,
COALESCE(sum(comments), 0) as ncomments
FROM ${viewGroup(range, 'sub_stats')}
JOIN "Sub" on "Sub".name = u.sub_name
GROUP BY "Sub".name
ORDER BY ${column} DESC NULLS LAST, "Sub".created_at ASC
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
return {
cursor: subs.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
subs
}
},
userSubs: async (_parent, { name, cursor, when, by, from, to, limit = LIMIT }, { models }) => {
if (!name) {
throw new GqlInputError('must supply user name')
}
const user = await models.user.findUnique({ where: { name } })
if (!user) {
throw new GqlInputError('no user has that name')
}
const decodedCursor = decodeCursor(cursor)
const range = whenRange(when, from, to || decodeCursor.time)
let column
switch (by) {
case 'revenue': column = 'revenue'; break
case 'spent': column = 'spent'; break
case 'posts': column = 'nposts'; break
case 'comments': column = 'ncomments'; break
default: column = 'stacked'; break
}
const subs = await models.$queryRawUnsafe(`
SELECT "Sub".*,
"Sub".created_at as "createdAt",
COALESCE(floor(sum(msats_revenue)/1000), 0) as revenue,
COALESCE(floor(sum(msats_stacked)/1000), 0) as stacked,
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
COALESCE(sum(posts), 0) as nposts,
COALESCE(sum(comments), 0) as ncomments
FROM ${viewGroup(range, 'sub_stats')}
JOIN "Sub" on "Sub".name = u.sub_name
WHERE "Sub"."userId" = $3
AND "Sub".status = 'ACTIVE'
GROUP BY "Sub".name
ORDER BY ${column} DESC NULLS LAST, "Sub".created_at ASC
OFFSET $4
LIMIT $5`, ...range, user.id, decodedCursor.offset, limit)
return {
cursor: subs.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
subs
}
}
},
Mutation: {
upsertSub: async (parent, { hash, hmac, ...data }, { me, models, lnd }) => {
upsertSub: async (parent, { ...data }, { me, models, lnd }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
// XXX this is because we did the wrong thing and used the subName as a primary key
const old = await models.sub.findUnique({
where: {
name: data.name,
userId: me.id
}
})
await validateSchema(territorySchema, data, { models, me, sub: { name: data.oldName } })
await ssValidate(territorySchema, data, { models, me })
if (old) {
return await updateSub(parent, data, { me, models, lnd, hash, hmac })
if (data.oldName) {
return await updateSub(parent, data, { me, models, lnd })
} else {
return await createSub(parent, data, { me, models, lnd, hash, hmac })
return await createSub(parent, data, { me, models, lnd })
}
},
paySub: async (parent, { name, hash, hmac }, { me, models, lnd }) => {
paySub: async (parent, { name }, { me, models, lnd }) => {
// check that they own the sub
const sub = await models.sub.findUnique({
where: {
@ -149,30 +174,22 @@ export default {
})
if (!sub) {
throw new GraphQLError('sub not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('sub not found')
}
if (sub.userId !== me.id) {
throw new GraphQLError('you do not own this sub', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('you do not own this sub')
}
if (sub.status === 'ACTIVE') {
return sub
}
const queries = paySubQueries(sub, models)
if (queries.length === 0) {
return sub
}
const results = await serializeInvoicable(
queries,
{ models, lnd, hash, hmac, me, enforceFee: sub.billingCost })
return results[1]
return await performPaidAction('TERRITORY_BILLING', { name }, { me, models, lnd })
},
toggleMuteSub: async (parent, { name }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const lookupData = { userId: Number(me.id), subName: name }
@ -185,9 +202,85 @@ export default {
await models.muteSub.create({ data: { ...lookupData } })
return true
}
},
toggleSubSubscription: async (sub, { name }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
const lookupData = { userId: me.id, subName: name }
const where = { userId_subName: lookupData }
const existing = await models.subSubscription.findUnique({ where })
if (existing) {
await models.subSubscription.delete({ where })
return false
} else {
await models.subSubscription.create({ data: lookupData })
return true
}
},
transferTerritory: async (parent, { subName, userName }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
const sub = await models.sub.findUnique({
where: {
name: subName
}
})
if (!sub) {
throw new GqlInputError('sub not found')
}
if (sub.userId !== me.id) {
throw new GqlInputError('you do not own this sub')
}
const user = await models.user.findFirst({ where: { name: userName } })
if (!user) {
throw new GqlInputError('user not found')
}
if (user.id === me.id) {
throw new GqlInputError('cannot transfer territory to yourself')
}
const [, updatedSub] = await models.$transaction([
models.territoryTransfer.create({ data: { subName, oldUserId: me.id, newUserId: user.id } }),
models.sub.update({ where: { name: subName }, data: { userId: user.id, billingAutoRenew: false } })
])
notifyTerritoryTransfer({ models, sub, to: user })
return updatedSub
},
unarchiveTerritory: async (parent, { ...data }, { me, models, lnd }) => {
if (!me) {
throw new GqlAuthenticationError()
}
const { name } = data
await validateSchema(territorySchema, data, { models, me })
const oldSub = await models.sub.findUnique({ where: { name } })
if (!oldSub) {
throw new GqlInputError('sub not found')
}
if (oldSub.status !== 'STOPPED') {
throw new GqlInputError('sub is not archived')
}
if (oldSub.billingType === 'ONCE') {
// sanity check. this should never happen but leaving this comment here
// to stop error propagation just in case and document that this should never happen.
// #defensivecode
throw new GqlInputError('sub should not be archived')
}
return await performPaidAction('TERRITORY_UNARCHIVE', data, { me, models, lnd })
}
},
Sub: {
optional: sub => sub,
user: async (sub, args, { models }) => {
if (sub.user) {
return sub.user
@ -195,94 +288,65 @@ export default {
return await models.user.findUnique({ where: { id: sub.userId } })
},
meMuteSub: async (sub, args, { models }) => {
return sub.meMuteSub || sub.MuteSub?.length > 0
}
if (sub.meMuteSub !== undefined) {
return sub.meMuteSub
}
return sub.MuteSub?.length > 0
},
nposts: async (sub, { when, from, to }, { models }) => {
if (typeof sub.nposts !== 'undefined') {
return sub.nposts
}
},
ncomments: async (sub, { when, from, to }, { models }) => {
if (typeof sub.ncomments !== 'undefined') {
return sub.ncomments
}
},
meSubscription: async (sub, args, { me, models }) => {
if (sub.meSubscription !== undefined) {
return sub.meSubscription
}
return sub.SubSubscription?.length > 0
},
createdAt: sub => sub.createdAt || sub.created_at
}
}
async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
const { billingType } = data
let billingCost = TERRITORY_COST_MONTHLY
let billAt = datePivot(new Date(), { months: 1 })
if (billingType === 'ONCE') {
billingCost = TERRITORY_COST_ONCE
billAt = null
} else if (billingType === 'YEARLY') {
billingCost = TERRITORY_COST_YEARLY
billAt = datePivot(new Date(), { years: 1 })
}
const cost = BigInt(1000) * BigInt(billingCost)
async function createSub (parent, data, { me, models, lnd }) {
try {
const results = await serializeInvoicable([
// bill 'em
models.user.update({
where: {
id: me.id
},
data: {
msats: {
decrement: cost
}
}
}),
// create 'em
models.sub.create({
data: {
...data,
billingCost,
rankingType: 'WOT',
userId: me.id
}
}),
// record 'em
models.subAct.create({
data: {
userId: me.id,
subName: data.name,
msats: cost,
type: 'BILLING'
}
}),
// schedule 'em
...(billAt
? [models.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, keepuntil) VALUES ('territoryBilling',
${JSON.stringify({
subName: data.name
})}::JSONB, ${billAt}, ${datePivot(billAt, { days: 1 })})`]
: [])
], { models, lnd, hash, hmac, me, enforceFee: billingCost })
return results[1]
return await performPaidAction('TERRITORY_CREATE', data, { me, models, lnd })
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('name taken', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('name taken')
}
throw error
}
}
async function updateSub (parent, { name, ...data }, { me, models, lnd, hash, hmac }) {
// prevent modification of billingType
delete data.billingType
async function updateSub (parent, { oldName, ...data }, { me, models, lnd }) {
const oldSub = await models.sub.findUnique({
where: {
name: oldName,
userId: me.id,
// this function's logic is only valid if the sub is not stopped
// so prevent updates to stopped subs
status: {
not: 'STOPPED'
}
}
})
if (!oldSub) {
throw new GqlInputError('sub not found')
}
try {
const results = await serialize(models,
// update 'em
models.sub.update({
data,
where: {
name
}
}))
return results[0]
return await performPaidAction('TERRITORY_UPDATE', { oldName, ...data }, { me, models, lnd })
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('name taken', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('name taken')
}
throw error
}

View File

@ -1,42 +1,70 @@
import { GraphQLError } from 'graphql'
import { ANON_USER_ID, IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_AVATAR, UPLOAD_TYPES_ALLOW } from '../../lib/constants'
import { createPresignedPost } from '../s3'
import { USER_ID, IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_AVATAR, UPLOAD_TYPES_ALLOW, AWS_S3_URL_REGEXP, AVATAR_TYPES_ALLOW } from '@/lib/constants'
import { createPresignedPost } from '@/api/s3'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { msatsToSats } from '@/lib/format'
export default {
Query: {
uploadFees: async (parent, { s3Keys }, { models, me }) => {
return uploadFees(s3Keys, { models, me })
}
},
Mutation: {
getSignedPOST: async (parent, { type, size, width, height, avatar }, { models, me }) => {
if (UPLOAD_TYPES_ALLOW.indexOf(type) === -1) {
throw new GraphQLError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(`upload must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace(/^(image|video)\//, '')).join(', ')}`)
}
if (size > UPLOAD_SIZE_MAX) {
throw new GraphQLError(`image must be less than ${UPLOAD_SIZE_MAX / (1024 ** 2)} megabytes`, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(`upload must be less than ${UPLOAD_SIZE_MAX / (1024 ** 2)} megabytes`)
}
if (avatar && size > UPLOAD_SIZE_MAX_AVATAR) {
throw new GraphQLError(`image must be less than ${UPLOAD_SIZE_MAX_AVATAR / (1024 ** 2)} megabytes`, { extensions: { code: 'BAD_INPUT' } })
if (avatar) {
if (AVATAR_TYPES_ALLOW.indexOf(type) === -1) {
throw new GqlInputError(`avatar must be ${AVATAR_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
}
if (size > UPLOAD_SIZE_MAX_AVATAR) {
throw new GqlInputError(`avatar must be less than ${UPLOAD_SIZE_MAX_AVATAR / (1024 ** 2)} megabytes`)
}
}
// width and height is 0 for videos
if (width * height > IMAGE_PIXELS_MAX) {
throw new GraphQLError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`)
}
const imgParams = {
const fileParams = {
type,
size,
width,
height,
userId: me?.id || ANON_USER_ID,
userId: me?.id || USER_ID.anon,
paid: false
}
if (avatar) {
if (!me) throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
imgParams.paid = undefined
if (!me) throw new GqlAuthenticationError()
fileParams.paid = undefined
}
const upload = await models.upload.create({ data: { ...imgParams } })
const upload = await models.upload.create({ data: { ...fileParams } })
return createPresignedPost({ key: String(upload.id), type, size })
}
}
}
export function uploadIdsFromText (text, { models }) {
if (!text) return []
return [...new Set([...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])))]
}
export async function uploadFees (s3Keys, { models, me }) {
// returns info object in this format:
// { bytes24h: int, bytesUnpaid: int, nUnpaid: int, uploadFeesMsats: BigInt }
const [info] = await models.$queryRawUnsafe('SELECT * FROM upload_fees($1::INTEGER, $2::INTEGER[])', me ? me.id : USER_ID.anon, s3Keys)
const uploadFees = msatsToSats(info.uploadFeesMsats)
const totalFeesMsats = info.nUnpaid * Number(info.uploadFeesMsats)
const totalFees = msatsToSats(totalFeesMsats)
return { ...info, uploadFees, totalFees, totalFeesMsats }
}

File diff suppressed because it is too large Load Diff

75
api/resolvers/vault.js Normal file
View File

@ -0,0 +1,75 @@
import { E_VAULT_KEY_EXISTS, GqlAuthenticationError, GqlInputError } from '@/lib/error'
export default {
Query: {
getVaultEntry: async (parent, { key }, { me, models }, info) => {
if (!me) throw new GqlAuthenticationError()
if (!key) throw new GqlInputError('must have key')
const k = await models.vault.findUnique({
where: {
key,
userId: me.id
}
})
return k
},
getVaultEntries: async (parent, { keysFilter }, { me, models }, info) => {
if (!me) throw new GqlAuthenticationError()
const entries = await models.vaultEntry.findMany({
where: {
userId: me.id,
key: keysFilter?.length
? {
in: keysFilter
}
: undefined
}
})
return entries
}
},
Mutation: {
// atomic vault migration
updateVaultKey: async (parent, { entries, hash }, { me, models }) => {
if (!me) throw new GqlAuthenticationError()
if (!hash) throw new GqlInputError('hash required')
const txs = []
const { vaultKeyHash: oldKeyHash } = await models.user.findUnique({ where: { id: me.id } })
if (oldKeyHash) {
if (oldKeyHash !== hash) {
throw new GqlInputError('vault key already set', E_VAULT_KEY_EXISTS)
} else {
return true
}
} else {
txs.push(models.user.update({
where: { id: me.id },
data: { vaultKeyHash: hash }
}))
}
for (const entry of entries) {
txs.push(models.vaultEntry.update({
where: { userId_key: { userId: me.id, key: entry.key } },
data: { value: entry.value, iv: entry.iv }
}))
}
await models.$transaction(txs)
return true
},
clearVault: async (parent, args, { me, models }) => {
if (!me) throw new GqlAuthenticationError()
const txs = []
txs.push(models.user.update({
where: { id: me.id },
data: { vaultKeyHash: '' }
}))
txs.push(models.vaultEntry.deleteMany({ where: { userId: me.id } }))
await models.$transaction(txs)
return true
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import AWS from 'aws-sdk'
import { MEDIA_URL } from '@/lib/constants'
const bucketRegion = 'us-east-1'
const Bucket = process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET
@ -7,8 +8,19 @@ AWS.config.update({
region: bucketRegion
})
const config = {
apiVersion: '2006-03-01',
s3ForcePathStyle: process.env.NODE_ENV === 'development'
}
export function createPresignedPost ({ key, type, size }) {
const s3 = new AWS.S3({ apiVersion: '2006-03-01' })
// for local development, we use the NEXT_PUBLIC_MEDIA_URL which
// is reachable from the host machine
if (process.env.NODE_ENV === 'development') {
config.endpoint = process.env.NEXT_PUBLIC_MEDIA_URL
}
const s3 = new AWS.S3(config)
return new Promise((resolve, reject) => {
s3.createPresignedPost({
Bucket,
@ -24,14 +36,35 @@ export function createPresignedPost ({ key, type, size }) {
})
}
export function deleteObjects (keys) {
const s3 = new AWS.S3({ apiVersion: '2006-03-01' })
return new Promise((resolve, reject) => {
s3.deleteObjects({
Bucket,
Delete: {
Objects: keys.map(key => ({ Key: String(key) }))
export async function deleteObjects (keys) {
// for local development, we use the MEDIA_URL which
// is reachable from the container network
if (process.env.NODE_ENV === 'development') {
config.endpoint = MEDIA_URL
}
const s3 = new AWS.S3(config)
// max 1000 keys per request
// see https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html
const batchSize = 1000
const deleted = []
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize)
await new Promise((resolve, reject) => {
const params = {
Bucket,
Delete: {
Objects: batch.map(key => ({ Key: String(key) }))
}
}
}, (err, data) => { err ? reject(err) : resolve(keys) })
})
s3.deleteObjects(params, (err, data) => {
if (err) return reject(err)
const deleted = data.Deleted?.map(({ Key }) => parseInt(Key)) || []
resolve(deleted)
})
}).then((deleteConfirmed) => {
deleted.push(...deleteConfirmed)
}).catch(console.error)
}
return deleted
}

View File

@ -1,14 +1,12 @@
import os from '@opensearch-project/opensearch'
const options = process.env.NODE_ENV === 'development'
? { node: process.env.OPENSEARCH_URL || 'http://localhost:9200' }
: {
node: process.env.OPENSEARCH_URL,
auth: {
username: process.env.OPENSEARCH_USERNAME,
password: process.env.OPENSEARCH_PASSWORD
}
}
const options = {
node: process.env.OPENSEARCH_URL,
auth: {
username: process.env.OPENSEARCH_USERNAME,
password: process.env.OPENSEARCH_PASSWORD
}
}
global.os = global.os || new os.Client(options)

View File

@ -7,12 +7,15 @@ import models from './models'
import { print } from 'graphql'
import lnd from './lnd'
import search from './search'
import { ME } from '../fragments/users'
import { PRICE } from '../fragments/price'
import { BLOCK_HEIGHT } from '../fragments/blockHeight'
import { CHAIN_FEE } from '../fragments/chainFee'
import { ME } from '@/fragments/users'
import { PRICE } from '@/fragments/price'
import { BLOCK_HEIGHT } from '@/fragments/blockHeight'
import { CHAIN_FEE } from '@/fragments/chainFee'
import { getServerSession } from 'next-auth/next'
import { getAuthOptions } from '../pages/api/auth/[...nextauth]'
import { getAuthOptions } from '@/pages/api/auth/[...nextauth]'
import { NOFOLLOW_LIMIT } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { MULTI_AUTH_ANON, MULTI_AUTH_LIST } from '@/lib/auth'
export default async function getSSRApolloClient ({ req, res, me = null }) {
const session = req && await getServerSession(req, res, getAuthOptions(req))
@ -40,20 +43,93 @@ export default async function getSSRApolloClient ({ req, res, me = null }) {
watchQuery: {
fetchPolicy: 'no-cache',
nextFetchPolicy: 'no-cache',
canonizeResults: true,
ssr: true
},
query: {
fetchPolicy: 'no-cache',
nextFetchPolicy: 'no-cache',
canonizeResults: true,
ssr: true
}
}
})
await client.clearStore()
return client
}
function oneDayReferral (request, { me }) {
if (!me) return
const refHeader = request.headers['x-stacker-news-referrer']
if (!refHeader) return
const referrers = refHeader.split('; ').filter(Boolean)
for (const referrer of referrers) {
let prismaPromise, getData
if (referrer.startsWith('item-')) {
prismaPromise = models.item.findUnique({
where: {
id: parseInt(referrer.slice(5)),
msats: {
gt: satsToMsats(NOFOLLOW_LIMIT)
},
weightedVotes: {
gt: 0
}
}
})
getData = item => ({
referrerId: item.userId,
refereeId: parseInt(me.id),
type: item.parentId ? 'COMMENT' : 'POST',
typeId: String(item.id)
})
} else if (referrer.startsWith('profile-')) {
const name = referrer.slice(8)
// exclude all pages that are not user profiles
if (['api', 'auth', 'day', 'invites', 'invoices', 'referrals', 'rewards',
'satistics', 'settings', 'stackers', 'wallet', 'withdrawals', '404', '500',
'email', 'live', 'login', 'notifications', 'offline', 'search', 'share',
'signup', 'territory', 'recent', 'top', 'edit', 'post', 'rss', 'saloon',
'faq', 'story', 'privacy', 'copyright', 'tos', 'changes', 'guide', 'daily',
'anon', 'ad'].includes(name)) continue
prismaPromise = models.user.findUnique({ where: { name } })
getData = user => ({
referrerId: user.id,
refereeId: parseInt(me.id),
type: 'PROFILE',
typeId: String(user.id)
})
} else if (referrer.startsWith('territory-')) {
prismaPromise = models.sub.findUnique({ where: { name: referrer.slice(10) } })
getData = sub => ({
referrerId: sub.userId,
refereeId: parseInt(me.id),
type: 'TERRITORY',
typeId: sub.name
})
} else {
prismaPromise = models.user.findUnique({ where: { name: referrer } })
getData = user => ({
referrerId: user.id,
refereeId: parseInt(me.id),
type: 'REFERRAL',
typeId: String(user.id)
})
}
prismaPromise?.then(ref => {
if (ref && getData) {
const data = getData(ref)
// can't refer yourself
if (data.refereeId === data.referrerId) return
models.oneDayReferral.create({ data }).catch(console.error)
}
}).catch(console.error)
}
}
/**
* Takes a query and variables and returns a getServerSideProps function
*
@ -76,13 +152,23 @@ export function getGetServerSideProps (
const client = await getSSRApolloClient({ req, res })
const { data: { me } } = await client.query({ query: ME })
let { data: { me } } = await client.query({ query: ME })
// required to redirect to /signup on page reload
// if we switched to anon and authentication is required
if (req.cookies[MULTI_AUTH_LIST] === MULTI_AUTH_ANON) {
me = null
}
if (authRequired && !me) {
const callback = process.env.PUBLIC_URL + req.url
let callback = process.env.NEXT_PUBLIC_URL + req.url
// On client-side routing, the callback is a NextJS URL
// so we need to remove the NextJS stuff.
// Example: /_next/data/development/territory.json
callback = callback.replace(/\/_next\/data\/\w+\//, '/').replace(/\.json$/, '')
return {
redirect: {
destination: `/login?callbackUrl=${encodeURIComponent(callback)}`
destination: `/signup?callbackUrl=${encodeURIComponent(callback)}`
}
}
}
@ -111,9 +197,10 @@ export function getGetServerSideProps (
}
if (error || !data || (notFound && notFound(data, vars, me))) {
return {
notFound: true
}
error && console.error(error)
res.writeHead(302, {
Location: '/404'
}).end()
}
props = {
@ -124,6 +211,8 @@ export function getGetServerSideProps (
}
}
oneDayReferral(req, { me })
return {
props: {
...props,

View File

@ -13,6 +13,8 @@ export default gql`
spenderGrowth(when: String, from: String, to: String): [TimeData!]!
stackingGrowth(when: String, from: String, to: String): [TimeData!]!
stackerGrowth(when: String, from: String, to: String): [TimeData!]!
itemGrowthSubs(when: String, from: String, to: String, sub: String): [TimeData!]!
revenueGrowthSubs(when: String, from: String, to: String, sub: String): [TimeData!]!
}
type TimeData {

View File

@ -1,16 +0,0 @@
import { gql } from 'graphql-tag'
export default gql`
type ImageFeesInfo {
totalFees: Int!
totalFeesMsats: Int!
imageFee: Int!
imageFeeMsats: Int!
nUnpaid: Int!
bytesUnpaid: Int!
bytes24h: Int!
}
extend type Query {
imageFeesInfo(s3Keys: [Int]!): ImageFeesInfo!
}
`

View File

@ -17,7 +17,8 @@ import price from './price'
import admin from './admin'
import blockHeight from './blockHeight'
import chainFee from './chainFee'
import image from './image'
import paidAction from './paidAction'
import vault from './vault'
const common = gql`
type Query {
@ -38,4 +39,4 @@ const common = gql`
`
export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite,
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, image]
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, paidAction, vault]

View File

@ -7,7 +7,7 @@ export default gql`
}
extend type Mutation {
createInvite(gift: Int!, limit: Int): Invite
createInvite(id: String, gift: Int!, limit: Int!, description: String): Invite
revokeInvite(id: ID!): Invite
}
@ -20,5 +20,6 @@ export default gql`
user: User!
revoked: Boolean!
poor: Boolean!
description: String
}
`

View File

@ -8,10 +8,18 @@ export default gql`
dupes(url: String!): [Item!]
related(cursor: String, title: String, id: ID, minMatch: String, limit: Limit): Items
search(q: String, sub: String, cursor: String, what: String, sort: String, when: String, from: String, to: String): Items
auctionPosition(sub: String, id: ID, bid: Int!): Int!
auctionPosition(sub: String, id: ID, boost: Int): Int!
boostPosition(sub: String, id: ID, boost: Int): BoostPositions!
itemRepetition(parentId: ID): Int!
}
type BoostPositions {
home: Boolean!
sub: Boolean!
homeMaxBoost: Int!
subMaxBoost: Int!
}
type TitleUnshorted {
title: String
unshorted: String
@ -20,36 +28,57 @@ export default gql`
type ItemActResult {
id: ID!
sats: Int!
path: String!
path: String
act: String!
}
type ItemAct {
id: ID!
act: String!
invoice: Invoice
}
extend type Mutation {
bookmarkItem(id: ID): Item
pinItem(id: ID): Item
subscribeItem(id: ID): Item
deleteItem(id: ID): Item
upsertLink(id: ID, sub: String, title: String!, url: String!, text: String, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int, hash: String, hmac: String, boost: Int, forward: [ItemForwardInput]): Item!
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
text: String!, url: String!, maxBid: Int!, status: String, logo: Int, hash: String, hmac: String): Item!
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
upsertLink(
id: ID, sub: String, title: String!, url: String!, text: String, boost: Int, forward: [ItemForwardInput],
hash: String, hmac: String): ItemPaidAction!
upsertDiscussion(
id: ID, sub: String, title: String!, text: String, boost: Int, forward: [ItemForwardInput],
hash: String, hmac: String): ItemPaidAction!
upsertBounty(
id: ID, sub: String, title: String!, text: String, bounty: Int, boost: Int, forward: [ItemForwardInput],
hash: String, hmac: String): ItemPaidAction!
upsertJob(
id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
text: String!, url: String!, boost: Int, status: String, logo: Int): ItemPaidAction!
upsertPoll(
id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], pollExpiresAt: Date,
hash: String, hmac: String): ItemPaidAction!
updateNoteId(id: ID!, noteId: String!): Item!
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item!
act(id: ID!, sats: Int, act: String, idempotent: Boolean, hash: String, hmac: String): ItemActResult!
pollVote(id: ID!, hash: String, hmac: String): ID!
upsertComment(id: ID, text: String!, parentId: ID, boost: Int, hash: String, hmac: String): ItemPaidAction!
act(id: ID!, sats: Int, act: String, hasSendWallet: Boolean): ItemActPaidAction!
pollVote(id: ID!): PollVotePaidAction!
toggleOutlaw(id: ID!): Item!
}
type PollVoteResult {
id: ID!
}
type PollOption {
id: ID,
option: String!
count: Int!
meVoted: Boolean!
}
type Poll {
meVoted: Boolean!
meInvoiceId: Int
meInvoiceActionState: InvoiceActionState
count: Int!
options: [PollOption!]!
}
@ -58,6 +87,7 @@ export default gql`
cursor: String
items: [Item!]!
pins: [Item!]
ad: Item
}
type Comments {
@ -65,12 +95,22 @@ export default gql`
comments: [Item!]!
}
enum InvoiceActionState {
PENDING
PENDING_HELD
HELD
PAID
FAILED
}
type Item {
id: ID!
createdAt: Date!
updatedAt: Date!
invoicePaidAt: Date
deletedAt: Date
deleteScheduledAt: Date
reminderScheduledAt: Date
title: String
searchTitle: String
url: String
@ -88,10 +128,13 @@ export default gql`
bountyPaidTo: [Int]
noteId: String
sats: Int!
credits: Int!
commentSats: Int!
commentCredits: Int!
lastCommentAt: Date
upvotes: Int!
meSats: Int!
meCredits: Int!
meDontLikeSats: Int!
meBookmark: Boolean!
meSubscription: Boolean!
@ -102,25 +145,30 @@ export default gql`
bio: Boolean!
paidImgLink: Boolean
ncomments: Int!
comments(sort: String): [Item!]!
nDirectComments: Int!
comments(sort: String, cursor: String): Comments!
path: String
position: Int
prior: Int
maxBid: Int
isJob: Boolean!
pollCost: Int
poll: Poll
pollExpiresAt: Date
company: String
location: String
remote: Boolean
sub: Sub
subName: String
status: String
status: String!
uploadId: Int
otsHash: String
parentOtsHash: String
forwards: [ItemForward]
imgproxyUrls: JSONObject
rel: String
apiKey: Boolean
invoice: Invoice
cost: Int!
}
input ItemForwardInput {

View File

@ -43,12 +43,24 @@ export default gql`
sortTime: Date!
}
type ItemMention {
id: ID!
item: Item!
sortTime: Date!
}
type Invitification {
id: ID!
invite: Invite!
sortTime: Date!
}
type Invoicification {
id: ID!
invoice: Invoice!
sortTime: Date!
}
type JobChanged {
id: ID!
item: Item!
@ -67,6 +79,7 @@ export default gql`
id: ID!
sortTime: Date!
days: Int
type: String!
}
type Earn {
@ -77,6 +90,19 @@ export default gql`
sources: EarnSources
}
type ReferralSources {
id: ID!
forever: Int!
oneDay: Int!
}
type ReferralReward {
id: ID!
earnedSats: Int!
sortTime: Date!
sources: ReferralSources
}
type Revenue {
id: ID!
earnedSats: Int!
@ -91,9 +117,19 @@ export default gql`
sortTime: Date!
}
type WithdrawlPaid {
id: ID!
earnedSats: Int!
sortTime: Date!
withdrawl: Withdrawl!
}
union ReferralSource = Item | Sub | User
type Referral {
id: ID!
sortTime: Date!
source: ReferralSource
}
type SubStatus {
@ -102,9 +138,29 @@ export default gql`
sortTime: Date!
}
type TerritoryPost {
id: ID!
item: Item!
sortTime: Date!
}
type TerritoryTransfer {
id: ID!
sub: Sub!
sortTime: Date!
}
type Reminder {
id: ID!
item: Item!
sortTime: Date!
}
union Notification = Reply | Votification | Mention
| Invitification | Earn | JobChanged | InvoicePaid | Referral
| Invitification | Earn | JobChanged | InvoicePaid | WithdrawlPaid | Referral
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
| TerritoryPost | TerritoryTransfer | Reminder | ItemMention | Invoicification
| ReferralReward
type Notifications {
lastChecked: Date

View File

@ -0,0 +1,61 @@
import { gql } from 'graphql-tag'
export default gql`
extend type Query {
paidAction(invoiceId: Int!): PaidAction
}
extend type Mutation {
retryPaidAction(invoiceId: Int!, newAttempt: Boolean): PaidAction!
}
enum PaymentMethod {
REWARD_SATS
FEE_CREDIT
ZERO_COST
OPTIMISTIC
PESSIMISTIC
}
interface PaidAction {
invoice: Invoice
paymentMethod: PaymentMethod!
}
type ItemPaidAction implements PaidAction {
result: Item
invoice: Invoice
paymentMethod: PaymentMethod!
}
type ItemActPaidAction implements PaidAction {
result: ItemActResult
invoice: Invoice
paymentMethod: PaymentMethod!
}
type PollVotePaidAction implements PaidAction {
result: PollVoteResult
invoice: Invoice
paymentMethod: PaymentMethod!
}
type SubPaidAction implements PaidAction {
result: Sub
invoice: Invoice
paymentMethod: PaymentMethod!
}
type DonatePaidAction implements PaidAction {
result: DonateResult
invoice: Invoice
paymentMethod: PaymentMethod!
}
type BuyCreditsPaidAction implements PaidAction {
result: BuyCreditsResult
invoice: Invoice
paymentMethod: PaymentMethod!
}
`

View File

@ -2,12 +2,6 @@ import { gql } from 'graphql-tag'
export default gql`
extend type Query {
referrals(when: String, from: String, to: String): Referrals!
}
type Referrals {
totalSats: Int!
totalReferrals: Int!
stats: [TimeData!]!
referrals(when: String, from: String, to: String): [TimeData!]!
}
`

View File

@ -7,13 +7,19 @@ export default gql`
}
extend type Mutation {
donateToRewards(sats: Int!, hash: String, hmac: String): Int!
donateToRewards(sats: Int!): DonatePaidAction!
}
type DonateResult {
sats: Int!
}
type Rewards {
total: Int!
time: Date!
sources: [NameValue!]!
leaderboard: UsersNullable
ad: Item
}
type Reward {

View File

@ -5,19 +5,33 @@ export default gql`
sub(name: String): Sub
subLatestPost(name: String!): String
subs: [Sub!]!
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
}
type Subs {
cursor: String
subs: [Sub!]!
}
extend type Mutation {
upsertSub(name: String!, desc: String, baseCost: Int!,
postTypes: [String!]!, allowFreebies: Boolean!,
upsertSub(oldName: String, name: String!, desc: String, baseCost: Int!,
replyCost: Int!,
postTypes: [String!]!,
billingType: String!, billingAutoRenew: Boolean!,
moderated: Boolean!, hash: String, hmac: String): Sub
paySub(name: String!, hash: String, hmac: String): Sub
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
paySub(name: String!): SubPaidAction!
toggleMuteSub(name: String!): Boolean!
toggleSubSubscription(name: String!): Boolean!
transferTerritory(subName: String!, userName: String!): Sub
unarchiveTerritory(name: String!, desc: String, baseCost: Int!,
replyCost: Int!, postTypes: [String!]!,
billingType: String!, billingAutoRenew: Boolean!,
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
}
type Sub {
name: ID!
name: String!
createdAt: Date!
userId: Int!
user: User!
@ -30,10 +44,27 @@ export default gql`
billingAutoRenew: Boolean!
rankingType: String!
billedLastAt: Date!
billPaidUntil: Date
baseCost: Int!
replyCost: Int!
status: String!
moderated: Boolean!
moderatedCount: Int!
meMuteSub: Boolean!
nsfw: Boolean!
nposts(when: String, from: String, to: String): Int!
ncomments(when: String, from: String, to: String): Int!
meSubscription: Boolean!
optional: SubOptional!
}
type SubOptional {
"""
conditionally private
"""
stacked(when: String, from: String, to: String): Int
spent(when: String, from: String, to: String): Int
revenue(when: String, from: String, to: String): Int
}
`

View File

@ -1,12 +1,26 @@
import { gql } from 'graphql-tag'
export default gql`
extend type Mutation {
getSignedPOST(type: String!, size: Int!, width: Int!, height: Int!, avatar: Boolean): SignedPost!
type UploadFees {
totalFees: Int!
totalFeesMsats: Int!
uploadFees: Int!
uploadFeesMsats: Int!
nUnpaid: Int!
bytesUnpaid: Int!
bytes24h: Int!
}
type SignedPost {
url: String!
fields: JSONObject!
}
extend type Query {
uploadFees(s3Keys: [Int]!): UploadFees!
}
extend type Mutation {
getSignedPOST(type: String!, size: Int!, width: Int!, height: Int!, avatar: Boolean): SignedPost!
}
`

View File

@ -7,11 +7,21 @@ export default gql`
user(id: ID, name: String): User
users: [User!]
nameAvailable(name: String!): Boolean!
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Users
topCowboys(cursor: String): Users
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): UsersNullable!
topCowboys(cursor: String): UsersNullable!
searchUsers(q: String!, limit: Limit, similarity: Float): [User!]!
userSuggestions(q: String, limit: Limit): [User!]!
hasNewNotes: Boolean!
mySubscribedUsers(cursor: String): Users!
myMutedUsers(cursor: String): Users!
userStatsActions(when: String, from: String, to: String): [TimeData!]!
userStatsIncomingSats(when: String, from: String, to: String): [TimeData!]!
userStatsOutgoingSats(when: String, from: String, to: String): [TimeData!]!
}
type UsersNullable {
cursor: String
users: [User]!
}
type Users {
@ -23,7 +33,7 @@ export default gql`
setName(name: String!): String
setSettings(settings: SettingsInput!): User
setPhoto(photoId: ID!): Int!
upsertBio(bio: String!): User!
upsertBio(text: String!): ItemPaidAction!
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
unlinkAuth(authType: String!): AuthMethods!
linkUnverifiedEmail(email: String!): Boolean
@ -31,20 +41,29 @@ export default gql`
subscribeUserPosts(id: ID): User
subscribeUserComments(id: ID): User
toggleMute(id: ID): User
generateApiKey(id: ID!): String
deleteApiKey(id: ID!): User
disableFreebies: Boolean
}
type User {
id: ID!
createdAt: Date!
name: String
name: String!
nitems(when: String, from: String, to: String): Int!
nposts(when: String, from: String, to: String): Int!
nterritories(when: String, from: String, to: String): Int!
ncomments(when: String, from: String, to: String): Int!
bio: Item
bioId: Int
photoId: Int
since: Int
"""
this is only returned when we sort stackers by value
"""
proportion: Float
optional: UserOptional!
privates: UserPrivates
@ -56,31 +75,47 @@ export default gql`
input SettingsInput {
autoDropBolt11s: Boolean!
diagnostics: Boolean!
noReferralLinks: Boolean!
fiatCurrency: String!
greeterMode: Boolean!
satsFilter: Int!
disableFreebies: Boolean
hideBookmarks: Boolean!
hideCowboyHat: Boolean!
hideGithub: Boolean!
hideNostr: Boolean!
hideTwitter: Boolean!
hideFromTopUsers: Boolean!
hideInvoiceDesc: Boolean!
hideIsContributor: Boolean!
hideWalletBalance: Boolean!
imgproxyOnly: Boolean!
showImagesAndVideos: Boolean!
nostrCrossposting: Boolean!
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!
noteDeposits: Boolean!,
noteWithdrawals: Boolean!,
noteEarning: Boolean!
noteForwardedSats: Boolean!
noteInvites: Boolean!
noteItemSats: Boolean!
noteJobIndicator: Boolean!
noteMentions: Boolean!
noteItemMentions: Boolean!
nsfwMode: Boolean!
tipDefault: Int!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean!
zapUndos: Int
wildWestMode: Boolean!
withdrawMaxFeeDefault: Int!
proxyReceive: Boolean
directReceive: Boolean
receiveCreditsBelowSats: Int!
sendCreditsBelowSats: Int!
}
type AuthMethods {
@ -88,7 +123,8 @@ export default gql`
nostr: Boolean!
github: Boolean!
twitter: Boolean!
email: String
email: Boolean!
apiKey: Boolean
}
type UserPrivates {
@ -96,7 +132,9 @@ export default gql`
extremely sensitive
"""
sats: Int!
credits: Int!
authMethods: AuthMethods!
lnAddr: String
"""
only relevant to user
@ -106,37 +144,61 @@ export default gql`
tipPopover: Boolean!
upvotePopover: Boolean!
hasInvites: Boolean!
apiKeyEnabled: Boolean!
"""
mirrors SettingsInput
"""
autoDropBolt11s: Boolean!
diagnostics: Boolean!
noReferralLinks: Boolean!
fiatCurrency: String!
satsFilter: Int!
disableFreebies: Boolean
greeterMode: Boolean!
hideBookmarks: Boolean!
hideCowboyHat: Boolean!
hideGithub: Boolean!
hideNostr: Boolean!
hideTwitter: Boolean!
hideFromTopUsers: Boolean!
hideInvoiceDesc: Boolean!
hideIsContributor: Boolean!
hideWalletBalance: Boolean!
imgproxyOnly: Boolean!
showImagesAndVideos: Boolean!
nostrCrossposting: Boolean!
nostrPubkey: String
nostrRelays: [String!]
noteAllDescendants: Boolean!
noteCowboyHat: Boolean!
noteDeposits: Boolean!
noteWithdrawals: Boolean!
noteEarning: Boolean!
noteForwardedSats: Boolean!
noteInvites: Boolean!
noteItemSats: Boolean!
noteJobIndicator: Boolean!
noteMentions: Boolean!
noteItemMentions: Boolean!
nsfwMode: Boolean!
tipDefault: Int!
tipRandom: Boolean!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean!
zapUndos: Int
wildWestMode: Boolean!
withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int
autoWithdrawMaxFeePercent: Float
autoWithdrawMaxFeeTotal: Int
vaultKeyHash: String
walletsUpdatedAt: Date
proxyReceive: Boolean
directReceive: Boolean
receiveCreditsBelowSats: Int!
sendCreditsBelowSats: Int!
}
type UserOptional {
@ -147,7 +209,22 @@ export default gql`
spent(when: String, from: String, to: String): Int
referrals(when: String, from: String, to: String): Int
streak: Int
gunStreak: Int
horseStreak: Int
maxStreak: Int
isContributor: Boolean
githubId: String
twitterId: String
nostrAuthPubkey: String
}
type NameValue {
name: String!
value: Float!
}
type TimeData {
time: Date!
data: [NameValue!]!
}
`

29
api/typeDefs/vault.js Normal file
View File

@ -0,0 +1,29 @@
import { gql } from 'graphql-tag'
export default gql`
type VaultEntry {
id: ID!
key: String!
iv: String!
value: String!
createdAt: Date!
updatedAt: Date!
}
input VaultEntryInput {
key: String!
iv: String!
value: String!
walletId: ID
}
extend type Query {
getVaultEntry(key: String!): VaultEntry
getVaultEntries(keysFilter: [String!]): [VaultEntry!]!
}
extend type Mutation {
clearVault: Boolean
updateVaultKey(entries: [VaultEntryInput!]!, hash: String!): Boolean
}
`

View File

@ -1,29 +1,125 @@
import { gql } from 'graphql-tag'
import { fieldToGqlArg, fieldToGqlArgOptional, generateResolverName, generateTypeDefName } from '@/wallets/graphql'
import { isServerField } from '@/wallets/common'
import walletDefs from '@/wallets/server'
export default gql`
function injectTypeDefs (typeDefs) {
const injected = [rawTypeDefs(), mutationTypeDefs()]
return `${typeDefs}\n\n${injected.join('\n\n')}\n`
}
function mutationTypeDefs () {
console.group('injected GraphQL mutations:')
const typeDefs = walletDefs.map((w) => {
let args = 'id: ID, '
const serverFields = w.fields
.filter(isServerField)
.map(fieldToGqlArgOptional)
if (serverFields.length > 0) args += serverFields.join(', ') + ','
args += 'enabled: Boolean, priority: Int, vaultEntries: [VaultEntryInput!], settings: AutowithdrawSettings, validateLightning: Boolean'
const resolverName = generateResolverName(w.walletField)
const typeDef = `${resolverName}(${args}): Wallet`
console.log(typeDef)
return typeDef
})
console.groupEnd()
return `extend type Mutation {\n${typeDefs.join('\n')}\n}`
}
function rawTypeDefs () {
console.group('injected GraphQL type defs:')
const typeDefs = walletDefs.map((w) => {
let args = w.fields
.filter(isServerField)
.map(fieldToGqlArg)
.map(s => ' ' + s)
.join('\n')
if (!args) {
// add a placeholder arg so the type is not empty
args = ' _empty: Boolean'
}
const typeDefName = generateTypeDefName(w.walletType)
const typeDef = `type ${typeDefName} {\n${args}\n}`
console.log(typeDef)
return typeDef
})
let union = 'union WalletDetails = '
union += walletDefs.map((w) => {
const typeDefName = generateTypeDefName(w.walletType)
return typeDefName
}).join(' | ')
console.log(union)
console.groupEnd()
return typeDefs.join('\n\n') + union
}
const typeDefs = `
extend type Query {
invoice(id: ID!): Invoice!
withdrawl(id: ID!): Withdrawl!
direct(id: ID!): Direct!
numBolt11s: Int!
connectAddress: String!
walletHistory(cursor: String, inc: String): History
wallets(includeReceivers: Boolean, includeSenders: Boolean, onlyEnabled: Boolean, prioritySort: String): [Wallet!]!
wallet(id: ID!): Wallet
walletByType(type: String!): Wallet
walletLogs(type: String, from: String, to: String, cursor: String): WalletLog!
failedInvoices: [Invoice!]!
}
extend type Mutation {
createInvoice(amount: Int!, expireSecs: Int, hodlInvoice: Boolean): Invoice!
createInvoice(amount: Int!): InvoiceOrDirect!
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl!
cancelInvoice(hash: String!, hmac: String!): Invoice!
dropBolt11(id: ID): Withdrawl
cancelInvoice(hash: String!, hmac: String, userCancel: Boolean): Invoice!
dropBolt11(hash: String!): Boolean
removeWallet(id: ID!): Boolean
deleteWalletLogs(wallet: String): Boolean
setWalletPriority(id: ID!, priority: Int!): Boolean
buyCredits(credits: Int!): BuyCreditsPaidAction!
}
type Invoice {
type BuyCreditsResult {
credits: Int!
}
interface InvoiceOrDirect {
id: ID!
}
type Wallet {
id: ID!
createdAt: Date!
updatedAt: Date!
type: String!
enabled: Boolean!
priority: Int!
wallet: WalletDetails!
vaultEntries: [VaultEntry!]!
}
input AutowithdrawSettings {
autoWithdrawThreshold: Int!
autoWithdrawMaxFeePercent: Float!
autoWithdrawMaxFeeTotal: Int!
}
type Invoice implements InvoiceOrDirect {
id: ID!
createdAt: Date!
hash: String!
bolt11: String!
expiresAt: Date!
cancelled: Boolean!
cancelledAt: Date
confirmedAt: Date
satsReceived: Int
satsRequested: Int!
@ -33,6 +129,14 @@ export default gql`
hmac: String
isHeld: Boolean
confirmedPreimage: String
actionState: String
actionType: String
actionError: String
invoiceForward: Boolean
item: Item
itemAct: ItemAct
forwardedSats: Int
forwardStatus: String
}
type Withdrawl {
@ -45,6 +149,21 @@ export default gql`
satsFeePaying: Int!
satsFeePaid: Int
status: String
autoWithdraw: Boolean!
preimage: String
forwardedActionType: String
}
type Direct implements InvoiceOrDirect {
id: ID!
createdAt: Date!
bolt11: String
hash: String
sats: Int
preimage: String
nostr: JSONObject
comment: String
lud18Data: JSONObject
}
type Fact {
@ -55,6 +174,7 @@ export default gql`
bolt11: String
status: String
description: String
autoWithdraw: Boolean
item: Item
invoiceComment: String
invoicePayerData: JSONObject
@ -65,4 +185,20 @@ export default gql`
facts: [Fact!]!
cursor: String
}
type WalletLog {
entries: [WalletLogEntry!]!
cursor: String
}
type WalletLogEntry {
id: ID!
createdAt: Date!
wallet: ID!
level: String!
message: String!
context: JSONObject
}
`
export default gql`${injectTypeDefs(typeDefs)}`

View File

@ -1,114 +0,0 @@
import webPush from 'web-push'
import models from '../models'
import { COMMENT_DEPTH_LIMIT } from '../../lib/constants'
import removeMd from 'remove-markdown'
const webPushEnabled = process.env.NODE_ENV === 'production' ||
(process.env.VAPID_MAILTO && process.env.NEXT_PUBLIC_VAPID_PUBKEY && process.env.VAPID_PRIVKEY)
if (webPushEnabled) {
webPush.setVapidDetails(
process.env.VAPID_MAILTO,
process.env.NEXT_PUBLIC_VAPID_PUBKEY,
process.env.VAPID_PRIVKEY
)
} else {
console.warn('VAPID_* env vars not set, skipping webPush setup')
}
const createPayload = (notification) => {
// https://web.dev/push-notifications-display-a-notification/#visual-options
let { title, body, ...options } = notification
if (body) body = removeMd(body)
return JSON.stringify({
title,
options: {
body,
timestamp: Date.now(),
icon: '/icons/icon_x96.png',
...options
}
})
}
const createUserFilter = (tag) => {
// filter users by notification settings
const tagMap = {
REPLY: 'noteAllDescendants',
MENTION: 'noteMentions',
TIP: 'noteItemSats',
FORWARDEDTIP: 'noteForwardedSats',
REFERRAL: 'noteInvites',
INVITE: 'noteInvites',
EARN: 'noteEarning',
DEPOSIT: 'noteDeposits',
STREAK: 'noteCowboyHat'
}
const key = tagMap[tag.split('-')[0]]
return key ? { user: { [key]: true } } : undefined
}
const createItemUrl = async ({ id }) => {
const [rootItem] = await models.$queryRawUnsafe(
'SELECT subpath(path, -LEAST(nlevel(path), $1::INTEGER), 1)::text AS id FROM "Item" WHERE id = $2::INTEGER',
COMMENT_DEPTH_LIMIT + 1, Number(id)
)
return `/items/${rootItem.id}` + (rootItem.id !== id ? `?commentId=${id}` : '')
}
const sendNotification = (subscription, payload) => {
if (!webPushEnabled) {
console.warn('webPush not configured. skipping notification')
return
}
const { id, endpoint, p256dh, auth } = subscription
return webPush.sendNotification({ endpoint, keys: { p256dh, auth } }, payload)
.catch(async (err) => {
if (err.statusCode === 400) {
console.log('[webPush] invalid request: ', err)
} else if ([401, 403].includes(err.statusCode)) {
console.log('[webPush] auth error: ', err)
} else if (err.statusCode === 404 || err.statusCode === 410) {
console.log('[webPush] subscription has expired or is no longer valid: ', err)
const deletedSubscripton = await models.pushSubscription.delete({ where: { id } })
console.log(`[webPush] deleted subscription ${id} of user ${deletedSubscripton.userId} due to push error`)
} else if (err.statusCode === 413) {
console.log('[webPush] payload too large: ', err)
} else if (err.statusCode === 429) {
console.log('[webPush] too many requests: ', err)
} else {
console.log('[webPush] error: ', err)
}
})
}
export async function sendUserNotification (userId, notification) {
try {
notification.data ??= {}
if (notification.item) {
notification.data.url ??= await createItemUrl(notification.item)
notification.data.itemId ??= notification.item.id
delete notification.item
}
const userFilter = createUserFilter(notification.tag)
const payload = createPayload(notification)
const subscriptions = await models.pushSubscription.findMany({
where: { userId, ...userFilter }
})
await Promise.allSettled(
subscriptions.map(subscription => sendNotification(subscription, payload))
)
} catch (err) {
console.log('[webPush] error sending user notification: ', err)
}
}
export async function replyToSubscription (subscriptionId, notification) {
try {
const payload = createPayload(notification)
const subscription = await models.pushSubscription.findUnique({ where: { id: subscriptionId } })
await sendNotification(subscription, payload)
} catch (err) {
console.log('[webPush] error sending subscription reply: ', err)
}
}

207
awards.csv Normal file
View File

@ -0,0 +1,207 @@
name,type,pr id,issue ids,difficulty,priority,changes requested,notes,amount,receive method,date paid
jp30566347,pr,#898,#680,good-first-issue,,,,20k,jpmelanson@getalby.com,2024-03-16
NEEDcreations,issue,#898,#680,good-first-issue,,,,2k,NEEDcreations@stacker.news,2024-03-16
SatsAllDay,docs,#925,,,,,typo,100,weareallsatoshi@getalby.com,2024-03-16
SatsAllDay,issue,#933,#928,medium,,,,25k,weareallsatoshi@getalby.com,2024-03-18
SatsAllDay,code review,#933,#928,medium,,,,25k,weareallsatoshi@getalby.com,2024-03-18
SatsAllDay,pr,#942,#941,good-first-issue,,,,20k,weareallsatoshi@getalby.com,2024-03-20
SatsAllDay,pr,#954,#926,easy,,,,100k,weareallsatoshi@getalby.com,2024-03-23
SatsAllDay,pr,#956,,good-first-issue,,,,22k,weareallsatoshi@getalby.com,2024-03-23
cointastical,issue,#960,#735,good-first-issue,,,,2k,cointastical@stacker.news,2024-03-24
SatsAllDay,pr,#960,#735,good-first-issue,,,,20k,weareallsatoshi@getalby.com,2024-03-24
cointastical,issue,,#932,,,,,10k,cointastical@stacker.news,2024-03-25
SatsAllDay,pr,#955,#901,good-first-issue,,,,20k,weareallsatoshi@getalby.com,2024-03-25
SatsAllDay,issue,#964,#959,easy,,,,10k,weareallsatoshi@getalby.com,2024-03-25
SatsAllDay,code review,#964,#959,easy,,,,10k,weareallsatoshi@getalby.com,2024-03-25
AustinKelsay,pr,#970,,,,,,20k,bitcoinplebdev@stacker.news,2024-03-25
felipebueno,pr,#948,,,,,,100k,felipe@stacker.news,2024-03-26
benalleng,pr,#972,#923,good-first-issue,,,,20k,BenAllenG@stacker.news,2024-03-26
SatsAllDay,issue,#972,#923,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-03-26
felipebueno,pr,#974,#884,good-first-issue,,,,20k,felipe@stacker.news,2024-03-26
h0dlr,issue,#974,#884,good-first-issue,,,,2k,HODLR@stacker.news,2024-04-04
benalleng,pr,#975,,,,,,20k,BenAllenG@stacker.news,2024-03-26
SatsAllDay,security,#980,GHSA-qg4g-m4xq-695p,,,,,100k,weareallsatoshi@getalby.com,2024-03-28
SatsAllDay,code review,#980,GHSA-qg4g-m4xq-695p,medium,,,,25k,weareallsatoshi@getalby.com,2024-03-28
Darth-Coin,issue,#1009,#1002,easy,,,,10k,darthcoin@stacker.news,2024-04-04
atomantic,issue,#1009,#679,medium,high,,,50k,antic@stacker.news,2024-04-04
aniskhalfallah,pr,#1001,#976,good-first-issue,,,,20k,aniskhalfallah@stacker.news,2024-04-04
SatsAllDay,pr,#944,#1000,medium,,,,250k,weareallsatoshi@getalby.com,2024-04-04
SatsAllDay,issue,#944,#1000,medium,,,,25k,weareallsatoshi@getalby.com,2024-04-04
SatsAllDay,pr,#989,#984,medium,,,,250k,weareallsatoshi@getalby.com,2024-04-04
SatsAllDay,issue,#989,#984,medium,,,,25k,weareallsatoshi@getalby.com,2024-04-04
SouthKoreaLN,pr,#1015,#1010,good-first-issue,,,,20k,south_korea_ln@stacker.news,2024-04-04
SatsAllDay,issue,#1015,#1010,good-first-issue,,,,20k,weareallsatoshi@getalby.com,2024-04-04
jp30566347,pr,#991,#718,good-first-issue,,,,20k,jpmelanson@getalby.com,2024-04-04
benalleng,helpfulness,#1015,#1010,good-first-issue,,,,2k,BenAllenG@stacker.news,2024-04-04
felipebueno,pr,#1012,,,,,,20k,felipe@stacker.news,2024-04-04
abhiShandy,helpfulness,#1018,#1006,good-first-issue,,,identified problem,2k,bolt11,2024-04-04
benalleng,issue,#1018,#1006,good-first-issue,,,,2k,benalleng@mutiny.plus,2024-04-28
benalleng,issue,#1011,#993,easy,high,,,20k,benalleng@mutiny.plus,2024-04-28
benalleng,pr,#1011,#993,easy,high,,tortured them,200k,benalleng@mutiny.plus,2024-04-28
abhiShandy,pr,#1031,#908,good-first-issue,,1,,18k,abhishandy@stacker.news,2024-04-12
SatsAllDay,issue,#1031,#908,good-first-issue,,1,,1.8k,weareallsatoshi@getalby.com,2024-04-12
SatsAllDay,pr,#1034,#934,medium,,,,250k,weareallsatoshi@getalby.com,2024-04-12
SatsAllDay,issue,#1034,#934,medium,,,,25k,weareallsatoshi@getalby.com,2024-04-12
benalleng,pr,#1037,#1036,easy,,1,,90k,benalleng@mutiny.plus,2024-04-28
SatsAllDay,pr,#1038,#1033,easy,,,,100k,weareallsatoshi@getalby.com,2024-04-12
SatsAllDay,issue,#1038,#1033,easy,,,,10k,weareallsatoshi@getalby.com,2024-04-12
felipebueno,pr,#1043,,easy,,,,100k,felipe@stacker.news,2024-04-12
benalleng,pr,#1050,,good-first-issue,,,,20k,benalleng@mutiny.plus,2024-04-28
jp30566347,pr,#1055,#771,medium,,,extra mile,300k,jpmelanson@getalby.com,2024-04-12
benalleng,helpfulness,#1063,202,medium,,,did much of the legwork in another pr,100k,benalleng@mutiny.plus,2024-04-28
benalleng,code review,#1063,202,medium,,,,25k,benalleng@mutiny.plus,2024-04-28
benalleng,pr,#1066,#1060,good-first-issue,,,,20k,benalleng@mutiny.plus,2024-04-28
benalleng,pr,#1068,#1067,good-first-issue,,,,20k,benalleng@mutiny.plus,2024-04-28
abhiShandy,helpfulness,#1068,#1067,good-first-issue,,,,2k,abhishandy@stacker.news,2024-04-14
bumi,pr,#1076,,,,,,20k,bumi@getalby.com,2024-04-16
benalleng,pr,#1079,#977,easy,,,,100k,benalleng@mutiny.plus,2024-04-28
felipebueno,pr,#1024,,,,,,20k,felipe@stacker.news,2024-04-21
SatsAllDay,pr,#1075,#1064,medium-hard,,1,,450k,weareallsatoshi@getalby.com,2024-04-21
aChrisYouKnow,issue,#1075,#1064,medium-hard,,1,,45k,ACYK@stacker.news,2024-04-22
SatsAllDay,pr,#1098,,,,,,20k,weareallsatoshi@getalby.com,2024-04-21
SatsAllDay,pr,#1095,#728,medium,,,,250k,weareallsatoshi@getalby.com,2024-04-21
benalleng,pr,#1090,#1077,good-first-issue,,,,20k,benalleng@mutiny.plus,2024-04-28
benalleng,helpfulness,#1087,,,,,informed fix,20k,benalleng@mutiny.plus,2024-04-28
benalleng,pr,#1099,#794,medium-hard,,,refined in a commit,450k,benalleng@mutiny.plus,2024-04-28
dillon-co,helpfulness,#1099,#794,medium-hard,,,#988 did much of the legwork,225k,bolt11,2024-04-29
abhiShandy,pr,#1119,#1110,good-first-issue,,,,20k,abhishandy@stacker.news,2024-04-28
felipebueno,issue,#1119,#1110,good-first-issue,,,,2k,felipe@stacker.news,2024-04-28
SatsAllDay,pr,#1111,#622,medium-hard,,,,500k,weareallsatoshi@getalby.com,2024-05-04
itsrealfake,pr,#1130,#622,good-first-issue,,,,20k,itsrealfake2@stacker.news,2024-05-06
Darth-Coin,issue,#1130,#622,easy,,,,2k,darthcoin@stacker.news,2024-05-04
benalleng,pr,#1137,#1125,good-first-issue,,,,20k,benalleng@mutiny.plus,2024-05-04
SatsAllDay,issue,#1137,#1125,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-05-04
SatsAllDay,helpfulness,#1137,#1125,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-05-04
itsrealfake,pr,#1138,#995,good-first-issue,,,,20k,itsrealfake2@stacker.news,2024-05-06
SouthKoreaLN,issue,#1138,#995,good-first-issue,,,,2k,south_korea_ln@stacker.news,2024-05-04
mateusdeap,helpfulness,#1138,#995,good-first-issue,,,,1k,mateusdeap@stacker.news,2024-05-17
felipebueno,pr,#1094,,,,2,,80k,felipebueno@getalby.com,2024-05-06
benalleng,helpfulness,#1127,#927,good-first-issue,,,,2k,benalleng@mutiny.plus,2024-05-04
itsrealfake,pr,#1135,#1016,good-first-issue,,,nonideal solution,10k,itsrealfake2@stacker.news,2024-05-06
SatsAllDay,issue,#1135,#1016,good-first-issue,,,,1k,weareallsatoshi@getalby.com,2024-05-04
s373nZ,issue,#1136,#1107,medium,high,,,50k,se7enz@minibits.cash,2024-05-05
abhiShandy,pr,#1123,#624,good-first-issue,,,,20k,abhishandy@stacker.news,2024-05-17
hkarani,pr,#1147,#1143,good-first-issue,,,,20k,asterisk32@stacker.news,2024-05-17
benalleng,helpfulness,#1147,#1143,good-first-issue,,,,2k,benalleng@mutiny.plus,2024-05-17
abhiShandy,pr,#1157,#1148,good-first-issue,,,,20k,abhishandy@stacker.news,2024-05-17
SatsAllDay,issue,#1157,#1148,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-05-17
abhiShandy,pr,#1158,#1139,good-first-issue,,,,20k,abhishandy@stacker.news,2024-05-17
SatsAllDay,issue,#1158,#1139,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-05-17
SatsAllDay,pr,#1145,#717,medium,,,,250k,weareallsatoshi@getalby.com,2024-05-17
benalleng,pr,#1129,#491,good-first-issue,,,paid for advice out of band,20k,benalleng@mutiny.plus,2024-05-17
benalleng,pr,#1129,#1045,easy,,2,post-humously upgraded to easy,80k,benalleng@mutiny.plus,2024-05-17
SouthKoreaLN,issue,#1129,#1045,easy,,,,8k,south_korea_ln@stacker.news,2024-05-17
tsmith123,pr,#1171,#1124,good-first-issue,,,bonus for refactor,40k,stickymarch60@walletofsatoshi.com,2024-05-17
SatsAllDay,issue,#1171,#1124,good-first-issue,,,,4k,weareallsatoshi@getalby.com,2024-05-17
felipebueno,pr,#1162,,,,2,,200k,felipebueno@getalby.com,2024-05-17
Radentor,issue,,#1177,easy,,,,10k,Radentor@stacker.news,2024-05-17
tsmith123,pr,#1179,#790,good-first-issue,high,,,40k,stickymarch60@walletofsatoshi.com,2024-05-17
SatsAllDay,pr,#1159,#510,medium-hard,,1,,450k,weareallsatoshi@getalby.com,2024-05-22
Darth-Coin,issue,#1159,#510,medium-hard,,1,,45k,darthcoin@stacker.news,2024-05-22
OneOneSeven117,issue,#1187,#1164,easy,,,,10k,OneOneSeven@stacker.news,2024-05-23
tsmith123,pr,#1191,#134,medium,,,required small fix,225k,stickymarch60@walletofsatoshi.com,2024-05-28
benalleng,helpfulness,#1191,#134,medium,,,did most of this before,100k,benalleng@mutiny.plus,2024-05-28
cointastical,issue,#1191,#134,medium,,,,22k,cointastical@stacker.news,2024-05-28
kravhen,pr,#1198,#1180,good-first-issue,,,required linting,18k,nichro@getalby.com,2024-05-28
OneOneSeven117,issue,#1198,#1180,good-first-issue,,,required linting,2k,OneOneSeven@stacker.news,2024-05-28
tsmith123,pr,#1207,#837,easy,high,1,,180k,stickymarch60@walletofsatoshi.com,2024-05-31
SatsAllDay,pr,#1214,#1199,good-first-issue,,,,20k,weareallsatoshi@getalby.com,2024-06-03
SatsAllDay,pr,#1197,#1192,medium,,,,250k,weareallsatoshi@getalby.com,2024-06-03
tsmith123,pr,#1216,#1213,easy,,1,,90k,stickymarch60@walletofsatoshi.com,2024-06-03
tsmith123,pr,#1231,#1230,good-first-issue,,,,20k,stickymarch60@walletofsatoshi.com,2024-06-13
felipebueno,issue,#1231,#1230,good-first-issue,,,,2k,felipebueno@getalby.com,2024-06-13
tsmith123,pr,#1223,#107,medium,,2,10k bonus for our slowness,210k,stickymarch60@walletofsatoshi.com,2024-06-22
cointastical,issue,#1223,#107,medium,,2,,20k,cointastical@stacker.news,2024-06-22
kravhen,pr,#1215,#253,medium,,2,upgraded to medium,200k,nichro@getalby.com,2024-06-28
dillon-co,pr,#1140,#633,hard,,,requested advance,500k,bolt11,2024-07-02
takitakitanana,issue,,#1257,good-first-issue,,,,2k,takitakitanana@stacker.news,2024-07-11
SatsAllDay,pr,#1263,#1112,medium,,,1,225k,weareallsatoshi@getalby.com,2024-07-31
OneOneSeven117,issue,#1272,#1268,easy,,,,10k,OneOneSeven@stacker.news,2024-07-31
aniskhalfallah,pr,#1264,#1226,good-first-issue,,,,20k,aniskhalfallah@stacker.news,2024-07-31
Gudnessuche,issue,#1264,#1226,good-first-issue,,,,2k,everythingsatoshi@getalby.com,2024-08-10
aniskhalfallah,pr,#1289,,easy,,,,100k,aniskhalfallah@blink.sv,2024-08-12
riccardobl,pr,#1293,#1142,medium,high,,,500k,rblb@getalby.com,2024-08-18
tsmith123,pr,#1306,#832,medium,,,,250k,stickymarch60@walletofsatoshi.com,2024-08-20
riccardobl,pr,#1311,#864,medium,high,,pending unrelated refactor,500k,rblb@getalby.com,2024-08-27
brugeman,issue,#1311,#864,medium,high,,,50k,brugeman@stacker.news,2024-08-27
riccardobl,pr,#1342,#1141,hard,high,,pending unrelated rearchitecture,1m,rblb@getalby.com,2024-09-09
SatsAllDay,issue,#1368,#1331,medium,,,,25k,weareallsatoshi@getalby.com,2024-09-16
benalleng,helpfulness,#1368,#1170,medium,,,did a lot of it in #1175,25k,BenAllenG@stacker.news,2024-09-16
humble-GOAT,issue,#1412,#1407,good-first-issue,,,,2k,humble_GOAT@stacker.news,2024-09-18
felipebueno,issue,#1425,#986,medium,,,,25k,felipebueno@getalby.com,2024-09-26
riccardobl,pr,#1373,#1304,hard,high,,,2m,bolt11,2024-10-01
tsmith123,pr,#1428,#1397,easy,,1,superceded,90k,stickymarch60@walletofsatoshi.com,2024-10-02
toyota-corolla0,pr,#1449,,good-first-issue,,,,20k,toyota_corolla0@stacker.news,2024-10-02
toyota-corolla0,pr,#1455,#1437,good-first-issue,,,,20k,toyota_corolla0@stacker.news,2024-10-02
SouthKoreaLN,issue,#1436,,easy,,,,10k,south_korea_ln@stacker.news,2024-10-02
TonyGiorgio,issue,#1462,,easy,urgent,,,30k,TonyGiorgio@stacker.news,2024-10-07
hkarani,issue,#1369,#1458,good-first-issue,,,,2k,asterisk32@stacker.news,2024-10-21
toyota-corolla0,pr,#1369,#1458,good-first-issue,,,,20k,toyota_corolla0@stacker.news,2024-10-20
Soxasora,pr,#1593,#1569,good-first-issue,,,,20k,soxasora@blink.sv,2024-11-19
Soxasora,pr,#1599,#1258,medium,,,,250k,soxasora@blink.sv,2024-11-19
aegroto,pr,#1585,#1522,easy,high,,1,180k,aegroto@blink.sv,2024-11-19
sig47,issue,#1585,#1522,easy,high,,1,18k,siggy47@stacker.news,2024-11-19
aegroto,pr,#1583,#1572,easy,,,2,80k,aegroto@blink.sv,2024-11-19
Soxasora,pr,#1617,#1616,easy,,,,100k,soxasora@blink.sv,2024-11-20
Soxasora,issue,#1617,#1616,easy,,,,10k,soxasora@blink.sv,2024-11-20
AndreaDiazCorreia,helpfulness,#1605,#1566,good-first-issue,,,tried in pr,2k,andrea@lawallet.ar,2024-11-20
Soxasora,pr,#1653,,medium,,,determined unecessary,250k,soxasora@blink.sv,2024-12-07
Soxasora,pr,#1659,#1657,easy,,,,100k,soxasora@blink.sv,2024-12-07
sig47,issue,#1659,#1657,easy,,,,10k,siggy47@stacker.news,2024-12-07
Gudnessuche,issue,#1662,#1661,good-first-issue,,,,2k,everythingsatoshi@getalby.com,2024-12-07
aegroto,pr,#1589,#1586,easy,,,,100k,aegroto@blink.sv,2024-12-07
aegroto,issue,#1589,#1586,easy,,,,10k,aegroto@blink.sv,2024-12-07
aegroto,pr,#1619,#914,easy,,,,100k,aegroto@blink.sv,2024-12-07
felipebueno,pr,#1620,,medium,,,1,225k,felipebueno@getalby.com,2024-12-09
Soxasora,pr,#1647,#1645,easy,,,,100k,soxasora@blink.sv,2024-12-07
Soxasora,pr,#1667,#1568,easy,,,,100k,soxasora@blink.sv,2024-12-07
aegroto,pr,#1633,#1471,easy,,,1,90k,aegroto@blink.sv,2024-12-07
Darth-Coin,issue,#1649,#1421,medium,,,,25k,darthcoin@stacker.news,2024-12-07
Soxasora,pr,#1685,,medium,,,,250k,soxasora@blink.sv,2024-12-07
aegroto,pr,#1606,#1242,medium,,,,250k,aegroto@blink.sv,2024-12-07
sfr0xyz,issue,#1696,#1196,good-first-issue,,,,2k,sefiro@getalby.com,2024-12-10
Soxasora,pr,#1794,#756,hard,urgent,,includes #411,3m,bolt11,2025-01-09
Soxasora,pr,#1786,#363,easy,,,,100k,bolt11,2025-01-09
Soxasora,pr,#1768,#1186,medium-hard,,,,500k,bolt11,2025-01-09
Soxasora,pr,#1750,#1035,medium,,,,250k,bolt11,2025-01-09
SatsAllDay,issue,#1794,#411,hard,high,,,200k,weareallsatoshi@getalby.com,2025-01-20
felipebueno,issue,#1786,#363,easy,,,,10k,felipebueno@blink.sv,2025-01-27
cyphercosmo,pr,#1745,#1648,good-first-issue,,,2,16k,cyphercosmo@getalby.com,2025-01-27
Radentor,issue,#1768,#1186,medium-hard,,,,50k,revisedbird84@walletofsatoshi.com,2025-01-27
Soxasora,pr,#1841,#1692,good-first-issue,,,,20k,soxasora@blink.sv,2025-01-27
Soxasora,pr,#1839,#1790,easy,,,1,90k,soxasora@blink.sv,2025-01-27
Soxasora,pr,#1820,#1819,easy,,,1,90k,soxasora@blink.sv,2025-01-27
SatsAllDay,issue,#1820,#1819,easy,,,1,9k,weareallsatoshi@getalby.com,2025-01-27
Soxasora,pr,#1814,#1736,easy,,,,100k,soxasora@blink.sv,2025-01-27
jason-me,pr,#1857,,easy,,,,100k,rrbtc@vlt.ge,2025-02-08
ed-kung,pr,#1901,#323,good-first-issue,,,,20k,simplestacker@getalby.com,2025-02-14
Scroogey-SN,pr,#1911,#1905,good-first-issue,,,1,18k,Scroogey@coinos.io,2025-03-10
Scroogey-SN,pr,#1928,#1924,good-first-issue,,,,20k,Scroogey@coinos.io,2025-03-10
dtonon,issue,#1928,#1924,good-first-issue,,,,2k,???,???
ed-kung,pr,#1926,#1914,medium-hard,,,,500k,simplestacker@getalby.com,2025-03-10
ed-kung,issue,#1926,#1914,medium-hard,,,,50k,simplestacker@getalby.com,2025-03-10
ed-kung,pr,#1926,#1927,easy,,,,100k,simplestacker@getalby.com,2025-03-10
ed-kung,issue,#1926,#1927,easy,,,,10k,simplestacker@getalby.com,2025-03-10
ed-kung,issue,#1913,#1890,good-first-issue,,,,2k,simplestacker@getalby.com,2025-03-10
Scroogey-SN,pr,#1930,#1167,good-first-issue,,,,20k,Scroogey@coinos.io,2025-03-10
itsrealfake,issue,#1930,#1167,good-first-issue,,,,2k,smallimagination100035@getalby.com,???
Scroogey-SN,pr,#1948,#1849,medium,urgent,,,750k,Scroogey@coinos.io,2025-03-10
felipebueno,issue,#1947,#1945,good-first-issue,,,,2k,felipebueno@blink.sv,2025-03-10
ed-kung,pr,#1952,#1951,easy,,,,100k,simplestacker@getalby.com,2025-03-10
ed-kung,issue,#1952,#1951,easy,,,,10k,simplestacker@getalby.com,2025-03-10
Scroogey-SN,pr,#1973,#1959,good-first-issue,,,,20k,Scroogey@coinos.io,???
benthecarman,issue,#1953,#1950,good-first-issue,,,,2k,???,???
ed-kung,pr,#2012,#2004,easy,,,,100k,simplestacker@getalby.com,???
ed-kung,issue,#2012,#2004,easy,,,,10k,simplestacker@getalby.com,???
ed-kung,pr,#1993,#1982,good-first-issue,,,,20k,simplestacker@getalby.com,???
rideandslide,issue,#1993,#1982,good-first-issue,,,,2k,???,???
ed-kung,pr,#1972,#1254,good-first-issue,,,,20k,simplestacker@getalby.com,???
SatsAllDay,issue,#1972,#1254,good-first-issue,,,,2k,weareallsatoshi@getalby.com,???
ed-kung,pr,#1962,#1343,good-first-issue,,,,20k,simplestacker@getalby.com,???
ed-kung,pr,#1962,#1217,good-first-issue,,,,20k,simplestacker@getalby.com,???
ed-kung,pr,#1962,#866,easy,,,,100k,simplestacker@getalby.com,???
felipebueno,issue,#1962,#866,easy,,,,10k,felipebueno@blink.sv,???
cointastical,issue,#1962,#1217,good-first-issue,,,,2k,cointastical@stacker.news,???
Scroogey-SN,pr,#1975,#1964,good-first-issue,,,,20k,Scroogey@coinos.io,???
rideandslide,issue,#1986,#1985,good-first-issue,,,,2k,???,???
kristapsk,issue,#1976,#841,good-first-issue,,,,2k,???,???
1 name type pr id issue ids difficulty priority changes requested notes amount receive method date paid
2 jp30566347 pr #898 #680 good-first-issue 20k jpmelanson@getalby.com 2024-03-16
3 NEEDcreations issue #898 #680 good-first-issue 2k NEEDcreations@stacker.news 2024-03-16
4 SatsAllDay docs #925 typo 100 weareallsatoshi@getalby.com 2024-03-16
5 SatsAllDay issue #933 #928 medium 25k weareallsatoshi@getalby.com 2024-03-18
6 SatsAllDay code review #933 #928 medium 25k weareallsatoshi@getalby.com 2024-03-18
7 SatsAllDay pr #942 #941 good-first-issue 20k weareallsatoshi@getalby.com 2024-03-20
8 SatsAllDay pr #954 #926 easy 100k weareallsatoshi@getalby.com 2024-03-23
9 SatsAllDay pr #956 good-first-issue 22k weareallsatoshi@getalby.com 2024-03-23
10 cointastical issue #960 #735 good-first-issue 2k cointastical@stacker.news 2024-03-24
11 SatsAllDay pr #960 #735 good-first-issue 20k weareallsatoshi@getalby.com 2024-03-24
12 cointastical issue #932 10k cointastical@stacker.news 2024-03-25
13 SatsAllDay pr #955 #901 good-first-issue 20k weareallsatoshi@getalby.com 2024-03-25
14 SatsAllDay issue #964 #959 easy 10k weareallsatoshi@getalby.com 2024-03-25
15 SatsAllDay code review #964 #959 easy 10k weareallsatoshi@getalby.com 2024-03-25
16 AustinKelsay pr #970 20k bitcoinplebdev@stacker.news 2024-03-25
17 felipebueno pr #948 100k felipe@stacker.news 2024-03-26
18 benalleng pr #972 #923 good-first-issue 20k BenAllenG@stacker.news 2024-03-26
19 SatsAllDay issue #972 #923 good-first-issue 2k weareallsatoshi@getalby.com 2024-03-26
20 felipebueno pr #974 #884 good-first-issue 20k felipe@stacker.news 2024-03-26
21 h0dlr issue #974 #884 good-first-issue 2k HODLR@stacker.news 2024-04-04
22 benalleng pr #975 20k BenAllenG@stacker.news 2024-03-26
23 SatsAllDay security #980 GHSA-qg4g-m4xq-695p 100k weareallsatoshi@getalby.com 2024-03-28
24 SatsAllDay code review #980 GHSA-qg4g-m4xq-695p medium 25k weareallsatoshi@getalby.com 2024-03-28
25 Darth-Coin issue #1009 #1002 easy 10k darthcoin@stacker.news 2024-04-04
26 atomantic issue #1009 #679 medium high 50k antic@stacker.news 2024-04-04
27 aniskhalfallah pr #1001 #976 good-first-issue 20k aniskhalfallah@stacker.news 2024-04-04
28 SatsAllDay pr #944 #1000 medium 250k weareallsatoshi@getalby.com 2024-04-04
29 SatsAllDay issue #944 #1000 medium 25k weareallsatoshi@getalby.com 2024-04-04
30 SatsAllDay pr #989 #984 medium 250k weareallsatoshi@getalby.com 2024-04-04
31 SatsAllDay issue #989 #984 medium 25k weareallsatoshi@getalby.com 2024-04-04
32 SouthKoreaLN pr #1015 #1010 good-first-issue 20k south_korea_ln@stacker.news 2024-04-04
33 SatsAllDay issue #1015 #1010 good-first-issue 20k weareallsatoshi@getalby.com 2024-04-04
34 jp30566347 pr #991 #718 good-first-issue 20k jpmelanson@getalby.com 2024-04-04
35 benalleng helpfulness #1015 #1010 good-first-issue 2k BenAllenG@stacker.news 2024-04-04
36 felipebueno pr #1012 20k felipe@stacker.news 2024-04-04
37 abhiShandy helpfulness #1018 #1006 good-first-issue identified problem 2k bolt11 2024-04-04
38 benalleng issue #1018 #1006 good-first-issue 2k benalleng@mutiny.plus 2024-04-28
39 benalleng issue #1011 #993 easy high 20k benalleng@mutiny.plus 2024-04-28
40 benalleng pr #1011 #993 easy high tortured them 200k benalleng@mutiny.plus 2024-04-28
41 abhiShandy pr #1031 #908 good-first-issue 1 18k abhishandy@stacker.news 2024-04-12
42 SatsAllDay issue #1031 #908 good-first-issue 1 1.8k weareallsatoshi@getalby.com 2024-04-12
43 SatsAllDay pr #1034 #934 medium 250k weareallsatoshi@getalby.com 2024-04-12
44 SatsAllDay issue #1034 #934 medium 25k weareallsatoshi@getalby.com 2024-04-12
45 benalleng pr #1037 #1036 easy 1 90k benalleng@mutiny.plus 2024-04-28
46 SatsAllDay pr #1038 #1033 easy 100k weareallsatoshi@getalby.com 2024-04-12
47 SatsAllDay issue #1038 #1033 easy 10k weareallsatoshi@getalby.com 2024-04-12
48 felipebueno pr #1043 easy 100k felipe@stacker.news 2024-04-12
49 benalleng pr #1050 good-first-issue 20k benalleng@mutiny.plus 2024-04-28
50 jp30566347 pr #1055 #771 medium extra mile 300k jpmelanson@getalby.com 2024-04-12
51 benalleng helpfulness #1063 202 medium did much of the legwork in another pr 100k benalleng@mutiny.plus 2024-04-28
52 benalleng code review #1063 202 medium 25k benalleng@mutiny.plus 2024-04-28
53 benalleng pr #1066 #1060 good-first-issue 20k benalleng@mutiny.plus 2024-04-28
54 benalleng pr #1068 #1067 good-first-issue 20k benalleng@mutiny.plus 2024-04-28
55 abhiShandy helpfulness #1068 #1067 good-first-issue 2k abhishandy@stacker.news 2024-04-14
56 bumi pr #1076 20k bumi@getalby.com 2024-04-16
57 benalleng pr #1079 #977 easy 100k benalleng@mutiny.plus 2024-04-28
58 felipebueno pr #1024 20k felipe@stacker.news 2024-04-21
59 SatsAllDay pr #1075 #1064 medium-hard 1 450k weareallsatoshi@getalby.com 2024-04-21
60 aChrisYouKnow issue #1075 #1064 medium-hard 1 45k ACYK@stacker.news 2024-04-22
61 SatsAllDay pr #1098 20k weareallsatoshi@getalby.com 2024-04-21
62 SatsAllDay pr #1095 #728 medium 250k weareallsatoshi@getalby.com 2024-04-21
63 benalleng pr #1090 #1077 good-first-issue 20k benalleng@mutiny.plus 2024-04-28
64 benalleng helpfulness #1087 informed fix 20k benalleng@mutiny.plus 2024-04-28
65 benalleng pr #1099 #794 medium-hard refined in a commit 450k benalleng@mutiny.plus 2024-04-28
66 dillon-co helpfulness #1099 #794 medium-hard #988 did much of the legwork 225k bolt11 2024-04-29
67 abhiShandy pr #1119 #1110 good-first-issue 20k abhishandy@stacker.news 2024-04-28
68 felipebueno issue #1119 #1110 good-first-issue 2k felipe@stacker.news 2024-04-28
69 SatsAllDay pr #1111 #622 medium-hard 500k weareallsatoshi@getalby.com 2024-05-04
70 itsrealfake pr #1130 #622 good-first-issue 20k itsrealfake2@stacker.news 2024-05-06
71 Darth-Coin issue #1130 #622 easy 2k darthcoin@stacker.news 2024-05-04
72 benalleng pr #1137 #1125 good-first-issue 20k benalleng@mutiny.plus 2024-05-04
73 SatsAllDay issue #1137 #1125 good-first-issue 2k weareallsatoshi@getalby.com 2024-05-04
74 SatsAllDay helpfulness #1137 #1125 good-first-issue 2k weareallsatoshi@getalby.com 2024-05-04
75 itsrealfake pr #1138 #995 good-first-issue 20k itsrealfake2@stacker.news 2024-05-06
76 SouthKoreaLN issue #1138 #995 good-first-issue 2k south_korea_ln@stacker.news 2024-05-04
77 mateusdeap helpfulness #1138 #995 good-first-issue 1k mateusdeap@stacker.news 2024-05-17
78 felipebueno pr #1094 2 80k felipebueno@getalby.com 2024-05-06
79 benalleng helpfulness #1127 #927 good-first-issue 2k benalleng@mutiny.plus 2024-05-04
80 itsrealfake pr #1135 #1016 good-first-issue nonideal solution 10k itsrealfake2@stacker.news 2024-05-06
81 SatsAllDay issue #1135 #1016 good-first-issue 1k weareallsatoshi@getalby.com 2024-05-04
82 s373nZ issue #1136 #1107 medium high 50k se7enz@minibits.cash 2024-05-05
83 abhiShandy pr #1123 #624 good-first-issue 20k abhishandy@stacker.news 2024-05-17
84 hkarani pr #1147 #1143 good-first-issue 20k asterisk32@stacker.news 2024-05-17
85 benalleng helpfulness #1147 #1143 good-first-issue 2k benalleng@mutiny.plus 2024-05-17
86 abhiShandy pr #1157 #1148 good-first-issue 20k abhishandy@stacker.news 2024-05-17
87 SatsAllDay issue #1157 #1148 good-first-issue 2k weareallsatoshi@getalby.com 2024-05-17
88 abhiShandy pr #1158 #1139 good-first-issue 20k abhishandy@stacker.news 2024-05-17
89 SatsAllDay issue #1158 #1139 good-first-issue 2k weareallsatoshi@getalby.com 2024-05-17
90 SatsAllDay pr #1145 #717 medium 250k weareallsatoshi@getalby.com 2024-05-17
91 benalleng pr #1129 #491 good-first-issue paid for advice out of band 20k benalleng@mutiny.plus 2024-05-17
92 benalleng pr #1129 #1045 easy 2 post-humously upgraded to easy 80k benalleng@mutiny.plus 2024-05-17
93 SouthKoreaLN issue #1129 #1045 easy 8k south_korea_ln@stacker.news 2024-05-17
94 tsmith123 pr #1171 #1124 good-first-issue bonus for refactor 40k stickymarch60@walletofsatoshi.com 2024-05-17
95 SatsAllDay issue #1171 #1124 good-first-issue 4k weareallsatoshi@getalby.com 2024-05-17
96 felipebueno pr #1162 2 200k felipebueno@getalby.com 2024-05-17
97 Radentor issue #1177 easy 10k Radentor@stacker.news 2024-05-17
98 tsmith123 pr #1179 #790 good-first-issue high 40k stickymarch60@walletofsatoshi.com 2024-05-17
99 SatsAllDay pr #1159 #510 medium-hard 1 450k weareallsatoshi@getalby.com 2024-05-22
100 Darth-Coin issue #1159 #510 medium-hard 1 45k darthcoin@stacker.news 2024-05-22
101 OneOneSeven117 issue #1187 #1164 easy 10k OneOneSeven@stacker.news 2024-05-23
102 tsmith123 pr #1191 #134 medium required small fix 225k stickymarch60@walletofsatoshi.com 2024-05-28
103 benalleng helpfulness #1191 #134 medium did most of this before 100k benalleng@mutiny.plus 2024-05-28
104 cointastical issue #1191 #134 medium 22k cointastical@stacker.news 2024-05-28
105 kravhen pr #1198 #1180 good-first-issue required linting 18k nichro@getalby.com 2024-05-28
106 OneOneSeven117 issue #1198 #1180 good-first-issue required linting 2k OneOneSeven@stacker.news 2024-05-28
107 tsmith123 pr #1207 #837 easy high 1 180k stickymarch60@walletofsatoshi.com 2024-05-31
108 SatsAllDay pr #1214 #1199 good-first-issue 20k weareallsatoshi@getalby.com 2024-06-03
109 SatsAllDay pr #1197 #1192 medium 250k weareallsatoshi@getalby.com 2024-06-03
110 tsmith123 pr #1216 #1213 easy 1 90k stickymarch60@walletofsatoshi.com 2024-06-03
111 tsmith123 pr #1231 #1230 good-first-issue 20k stickymarch60@walletofsatoshi.com 2024-06-13
112 felipebueno issue #1231 #1230 good-first-issue 2k felipebueno@getalby.com 2024-06-13
113 tsmith123 pr #1223 #107 medium 2 10k bonus for our slowness 210k stickymarch60@walletofsatoshi.com 2024-06-22
114 cointastical issue #1223 #107 medium 2 20k cointastical@stacker.news 2024-06-22
115 kravhen pr #1215 #253 medium 2 upgraded to medium 200k nichro@getalby.com 2024-06-28
116 dillon-co pr #1140 #633 hard requested advance 500k bolt11 2024-07-02
117 takitakitanana issue #1257 good-first-issue 2k takitakitanana@stacker.news 2024-07-11
118 SatsAllDay pr #1263 #1112 medium 1 225k weareallsatoshi@getalby.com 2024-07-31
119 OneOneSeven117 issue #1272 #1268 easy 10k OneOneSeven@stacker.news 2024-07-31
120 aniskhalfallah pr #1264 #1226 good-first-issue 20k aniskhalfallah@stacker.news 2024-07-31
121 Gudnessuche issue #1264 #1226 good-first-issue 2k everythingsatoshi@getalby.com 2024-08-10
122 aniskhalfallah pr #1289 easy 100k aniskhalfallah@blink.sv 2024-08-12
123 riccardobl pr #1293 #1142 medium high 500k rblb@getalby.com 2024-08-18
124 tsmith123 pr #1306 #832 medium 250k stickymarch60@walletofsatoshi.com 2024-08-20
125 riccardobl pr #1311 #864 medium high pending unrelated refactor 500k rblb@getalby.com 2024-08-27
126 brugeman issue #1311 #864 medium high 50k brugeman@stacker.news 2024-08-27
127 riccardobl pr #1342 #1141 hard high pending unrelated rearchitecture 1m rblb@getalby.com 2024-09-09
128 SatsAllDay issue #1368 #1331 medium 25k weareallsatoshi@getalby.com 2024-09-16
129 benalleng helpfulness #1368 #1170 medium did a lot of it in #1175 25k BenAllenG@stacker.news 2024-09-16
130 humble-GOAT issue #1412 #1407 good-first-issue 2k humble_GOAT@stacker.news 2024-09-18
131 felipebueno issue #1425 #986 medium 25k felipebueno@getalby.com 2024-09-26
132 riccardobl pr #1373 #1304 hard high 2m bolt11 2024-10-01
133 tsmith123 pr #1428 #1397 easy 1 superceded 90k stickymarch60@walletofsatoshi.com 2024-10-02
134 toyota-corolla0 pr #1449 good-first-issue 20k toyota_corolla0@stacker.news 2024-10-02
135 toyota-corolla0 pr #1455 #1437 good-first-issue 20k toyota_corolla0@stacker.news 2024-10-02
136 SouthKoreaLN issue #1436 easy 10k south_korea_ln@stacker.news 2024-10-02
137 TonyGiorgio issue #1462 easy urgent 30k TonyGiorgio@stacker.news 2024-10-07
138 hkarani issue #1369 #1458 good-first-issue 2k asterisk32@stacker.news 2024-10-21
139 toyota-corolla0 pr #1369 #1458 good-first-issue 20k toyota_corolla0@stacker.news 2024-10-20
140 Soxasora pr #1593 #1569 good-first-issue 20k soxasora@blink.sv 2024-11-19
141 Soxasora pr #1599 #1258 medium 250k soxasora@blink.sv 2024-11-19
142 aegroto pr #1585 #1522 easy high 1 180k aegroto@blink.sv 2024-11-19
143 sig47 issue #1585 #1522 easy high 1 18k siggy47@stacker.news 2024-11-19
144 aegroto pr #1583 #1572 easy 2 80k aegroto@blink.sv 2024-11-19
145 Soxasora pr #1617 #1616 easy 100k soxasora@blink.sv 2024-11-20
146 Soxasora issue #1617 #1616 easy 10k soxasora@blink.sv 2024-11-20
147 AndreaDiazCorreia helpfulness #1605 #1566 good-first-issue tried in pr 2k andrea@lawallet.ar 2024-11-20
148 Soxasora pr #1653 medium determined unecessary 250k soxasora@blink.sv 2024-12-07
149 Soxasora pr #1659 #1657 easy 100k soxasora@blink.sv 2024-12-07
150 sig47 issue #1659 #1657 easy 10k siggy47@stacker.news 2024-12-07
151 Gudnessuche issue #1662 #1661 good-first-issue 2k everythingsatoshi@getalby.com 2024-12-07
152 aegroto pr #1589 #1586 easy 100k aegroto@blink.sv 2024-12-07
153 aegroto issue #1589 #1586 easy 10k aegroto@blink.sv 2024-12-07
154 aegroto pr #1619 #914 easy 100k aegroto@blink.sv 2024-12-07
155 felipebueno pr #1620 medium 1 225k felipebueno@getalby.com 2024-12-09
156 Soxasora pr #1647 #1645 easy 100k soxasora@blink.sv 2024-12-07
157 Soxasora pr #1667 #1568 easy 100k soxasora@blink.sv 2024-12-07
158 aegroto pr #1633 #1471 easy 1 90k aegroto@blink.sv 2024-12-07
159 Darth-Coin issue #1649 #1421 medium 25k darthcoin@stacker.news 2024-12-07
160 Soxasora pr #1685 medium 250k soxasora@blink.sv 2024-12-07
161 aegroto pr #1606 #1242 medium 250k aegroto@blink.sv 2024-12-07
162 sfr0xyz issue #1696 #1196 good-first-issue 2k sefiro@getalby.com 2024-12-10
163 Soxasora pr #1794 #756 hard urgent includes #411 3m bolt11 2025-01-09
164 Soxasora pr #1786 #363 easy 100k bolt11 2025-01-09
165 Soxasora pr #1768 #1186 medium-hard 500k bolt11 2025-01-09
166 Soxasora pr #1750 #1035 medium 250k bolt11 2025-01-09
167 SatsAllDay issue #1794 #411 hard high 200k weareallsatoshi@getalby.com 2025-01-20
168 felipebueno issue #1786 #363 easy 10k felipebueno@blink.sv 2025-01-27
169 cyphercosmo pr #1745 #1648 good-first-issue 2 16k cyphercosmo@getalby.com 2025-01-27
170 Radentor issue #1768 #1186 medium-hard 50k revisedbird84@walletofsatoshi.com 2025-01-27
171 Soxasora pr #1841 #1692 good-first-issue 20k soxasora@blink.sv 2025-01-27
172 Soxasora pr #1839 #1790 easy 1 90k soxasora@blink.sv 2025-01-27
173 Soxasora pr #1820 #1819 easy 1 90k soxasora@blink.sv 2025-01-27
174 SatsAllDay issue #1820 #1819 easy 1 9k weareallsatoshi@getalby.com 2025-01-27
175 Soxasora pr #1814 #1736 easy 100k soxasora@blink.sv 2025-01-27
176 jason-me pr #1857 easy 100k rrbtc@vlt.ge 2025-02-08
177 ed-kung pr #1901 #323 good-first-issue 20k simplestacker@getalby.com 2025-02-14
178 Scroogey-SN pr #1911 #1905 good-first-issue 1 18k Scroogey@coinos.io 2025-03-10
179 Scroogey-SN pr #1928 #1924 good-first-issue 20k Scroogey@coinos.io 2025-03-10
180 dtonon issue #1928 #1924 good-first-issue 2k ??? ???
181 ed-kung pr #1926 #1914 medium-hard 500k simplestacker@getalby.com 2025-03-10
182 ed-kung issue #1926 #1914 medium-hard 50k simplestacker@getalby.com 2025-03-10
183 ed-kung pr #1926 #1927 easy 100k simplestacker@getalby.com 2025-03-10
184 ed-kung issue #1926 #1927 easy 10k simplestacker@getalby.com 2025-03-10
185 ed-kung issue #1913 #1890 good-first-issue 2k simplestacker@getalby.com 2025-03-10
186 Scroogey-SN pr #1930 #1167 good-first-issue 20k Scroogey@coinos.io 2025-03-10
187 itsrealfake issue #1930 #1167 good-first-issue 2k smallimagination100035@getalby.com ???
188 Scroogey-SN pr #1948 #1849 medium urgent 750k Scroogey@coinos.io 2025-03-10
189 felipebueno issue #1947 #1945 good-first-issue 2k felipebueno@blink.sv 2025-03-10
190 ed-kung pr #1952 #1951 easy 100k simplestacker@getalby.com 2025-03-10
191 ed-kung issue #1952 #1951 easy 10k simplestacker@getalby.com 2025-03-10
192 Scroogey-SN pr #1973 #1959 good-first-issue 20k Scroogey@coinos.io ???
193 benthecarman issue #1953 #1950 good-first-issue 2k ??? ???
194 ed-kung pr #2012 #2004 easy 100k simplestacker@getalby.com ???
195 ed-kung issue #2012 #2004 easy 10k simplestacker@getalby.com ???
196 ed-kung pr #1993 #1982 good-first-issue 20k simplestacker@getalby.com ???
197 rideandslide issue #1993 #1982 good-first-issue 2k ??? ???
198 ed-kung pr #1972 #1254 good-first-issue 20k simplestacker@getalby.com ???
199 SatsAllDay issue #1972 #1254 good-first-issue 2k weareallsatoshi@getalby.com ???
200 ed-kung pr #1962 #1343 good-first-issue 20k simplestacker@getalby.com ???
201 ed-kung pr #1962 #1217 good-first-issue 20k simplestacker@getalby.com ???
202 ed-kung pr #1962 #866 easy 100k simplestacker@getalby.com ???
203 felipebueno issue #1962 #866 easy 10k felipebueno@blink.sv ???
204 cointastical issue #1962 #1217 good-first-issue 2k cointastical@stacker.news ???
205 Scroogey-SN pr #1975 #1964 good-first-issue 20k Scroogey@coinos.io ???
206 rideandslide issue #1986 #1985 good-first-issue 2k ??? ???
207 kristapsk issue #1976 #841 good-first-issue 2k ??? ???

1
capture/.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

17
capture/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM ghcr.io/puppeteer/puppeteer:21.10.0
EXPOSE 5678
USER root
WORKDIR /home/pptruser
ENV PUPPETEER_SKIP_DOWNLOAD true
COPY ./package*.json ./
RUN npm ci
COPY . .
ADD https://deb.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb fonts-noto-color-emoji.deb
RUN dpkg -i fonts-noto-color-emoji.deb
CMD [ "node", "index.js" ]
USER pptruser

116
capture/index.js Normal file
View File

@ -0,0 +1,116 @@
import express from 'express'
import puppeteer from 'puppeteer'
const captureUrl = process.env.CAPTURE_URL || 'http://host.docker.internal:3000/'
const port = process.env.PORT || 5678
const maxPages = Number(process.env.MAX_PAGES) || 5
const timeout = Number(process.env.TIMEOUT) || 10000
const cache = process.env.CACHE || 60000
const width = process.env.WIDTH || 600
const height = process.env.HEIGHT || 315
const deviceScaleFactor = process.env.SCALE_FACTOR || 2
// from https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
const args = [
'--autoplay-policy=user-gesture-required',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-client-side-phishing-detection',
'--disable-component-update',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-domain-reliability',
'--disable-extensions',
'--disable-features=AudioServiceOutOfProcess',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-notifications',
'--disable-offer-store-unmasked-wallet-cards',
'--disable-popup-blocking',
'--disable-print-preview',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--disable-setuid-sandbox',
'--disable-speech-api',
'--disable-sync',
'--hide-scrollbars',
'--ignore-gpu-blacklist',
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
'--no-first-run',
'--no-pings',
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
'--use-gl=swiftshader',
'--use-mock-keychain'
]
let browser
const app = express()
app.get('/health', (req, res) => {
res.status(200).end()
})
app.get('/*', async (req, res) => {
const url = new URL(req.originalUrl, captureUrl)
const timeLabel = `${Date.now()}-${url.href}`
const urlParams = new URLSearchParams(url.search)
const commentId = urlParams.get('commentId')
let page, pages
try {
console.time(timeLabel)
browser ||= await puppeteer.launch({
headless: 'new',
useDataDir: './data',
executablePath: 'google-chrome-stable',
args,
protocolTimeout: timeout,
defaultViewport: { width, height, deviceScaleFactor }
})
pages = (await browser.pages()).length
console.timeLog(timeLabel, 'capturing', 'current pages', pages)
// limit number of active pages
if (pages > maxPages + 1) {
console.timeLog(timeLabel, 'too many pages')
return res.writeHead(503, {
'Retry-After': 1
}).end()
}
page = await browser.newPage()
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }])
await page.goto(url.href, { waitUntil: 'load', timeout })
console.timeLog(timeLabel, 'page loaded')
if (commentId) {
console.timeLog(timeLabel, 'scrolling to comment')
await page.waitForSelector('.outline-it')
await new Promise((resolve, _reject) => setTimeout(resolve, 100))
}
const file = await page.screenshot({ type: 'png', captureBeyondViewport: false })
console.timeLog(timeLabel, 'screenshot complete')
res.setHeader('Content-Type', 'image/png')
res.setHeader('Cache-Control', `public, max-age=${cache}, immutable, stale-while-revalidate=${cache * 24}, stale-if-error=${cache * 24}`)
return res.status(200).end(file)
} catch (err) {
console.timeLog(timeLabel, 'error', err)
return res.status(500).end()
} finally {
console.timeEnd(timeLabel, 'pages at start', pages)
page?.close().catch(console.error)
}
})
app.listen(port, () =>
console.log(`Screenshot listen on http://:${port}`)
)

2637
capture/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
capture/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "capture",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"puppeteer": "^20.8.2"
},
"type": "module"
}

5
chat-web/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
# use vectorim/element-web as base but copy config.json to /app/config.json
FROM vectorim/element-web:latest
COPY config.json /app/config.json

41
chat-web/config.json Normal file
View File

@ -0,0 +1,41 @@
{
"default_server_name": "https://sndev.team",
"default_server_config": {
"m.homeserver": {
"base_url": "https://sndev.team"
},
"m.identity_server": {
"base_url": "https://sndev.team"
}
},
"brand": "chat.sndev.team",
"permalink_prefix": "https://chat.sndev.team",
"show_labs_settings": false,
"mobile_guide_toast": false,
"default_country_code": "US",
"disable_3pid_login": true,
"disable_custom_urls": true,
"disable_guests": true,
"disable_login_language_selector": true,
"room_directory": {
"servers": ["sndev.team"]
},
"enable_presence_by_hs_url": {
"https://matrix.org": false,
"https://matrix-client.matrix.org": false
},
"terms_and_conditions_links": [
{
"url": "https://element.io/privacy",
"text": "Privacy Policy"
},
{
"url": "https://element.io/cookie-policy",
"text": "Cookie Policy"
}
],
"privacy_policy_url": "https://element.io/cookie-policy",
"setting_defaults": {
"RustCrypto.staged_rollout_percent": 10
}
}

View File

@ -1,14 +1,24 @@
import Accordion from 'react-bootstrap/Accordion'
import AccordionContext from 'react-bootstrap/AccordionContext'
import { useAccordionButton } from 'react-bootstrap/AccordionButton'
import ArrowRight from '../svgs/arrow-right-s-fill.svg'
import ArrowDown from '../svgs/arrow-down-s-fill.svg'
import { useContext } from 'react'
import ArrowRight from '@/svgs/arrow-right-s-fill.svg'
import ArrowDown from '@/svgs/arrow-down-s-fill.svg'
import { useContext, useEffect, useState } from 'react'
import classNames from 'classnames'
function ContextAwareToggle ({ children, headerColor = 'var(--theme-grey)', eventKey }) {
const KEY_ID = '0'
function ContextAwareToggle ({ children, headerColor = 'var(--theme-grey)', eventKey, show }) {
const { activeEventKey } = useContext(AccordionContext)
const decoratedOnClick = useAccordionButton(eventKey)
useEffect(() => {
// if we want to show the accordian and it's not open, open it
if (show && activeEventKey !== eventKey) {
decoratedOnClick()
}
}, [show])
const isCurrentEventKey = activeEventKey === eventKey
return (
@ -21,11 +31,21 @@ function ContextAwareToggle ({ children, headerColor = 'var(--theme-grey)', even
)
}
export default function AccordianItem ({ header, body, headerColor = 'var(--theme-grey)', show }) {
export default function AccordianItem ({ header, body, className, headerColor = 'var(--theme-grey)', show }) {
const [activeKey, setActiveKey] = useState()
useEffect(() => {
setActiveKey(show ? KEY_ID : null)
}, [show])
const handleOnSelect = () => {
setActiveKey(activeKey === KEY_ID ? null : KEY_ID)
}
return (
<Accordion defaultActiveKey={show ? '0' : undefined}>
<ContextAwareToggle eventKey='0'><div style={{ color: headerColor }}>{header}</div></ContextAwareToggle>
<Accordion.Collapse eventKey='0' className='mt-2'>
<Accordion defaultActiveKey={activeKey} activeKey={activeKey} onSelect={handleOnSelect}>
<ContextAwareToggle show={show} eventKey={KEY_ID} headerColor={headerColor}><div style={{ color: headerColor }}>{header}</div></ContextAwareToggle>
<Accordion.Collapse eventKey={KEY_ID} className={classNames('mt-2', className)}>
<div>{body}</div>
</Accordion.Collapse>
</Accordion>

View File

@ -1,141 +1,176 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/router'
import cookie from 'cookie'
import { useMe } from './me'
import { ANON_USER_ID, SSR } from '../lib/constants'
import { USER } from '../fragments/users'
import { useApolloClient, useQuery } from '@apollo/client'
import { UserListRow } from './user-list'
import * as cookie from 'cookie'
import { useMe } from '@/components/me'
import { USER_ID, SSR } from '@/lib/constants'
import { USER } from '@/fragments/users'
import { useQuery } from '@apollo/client'
import { UserListRow } from '@/components/user-list'
import Link from 'next/link'
import AddIcon from '@/svgs/add-fill.svg'
import { MultiAuthErrorBanner } from '@/components/banners'
import { cookieOptions, MULTI_AUTH_ANON, MULTI_AUTH_LIST, MULTI_AUTH_POINTER } from '@/lib/auth'
const AccountContext = createContext()
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
const CHECK_ERRORS_INTERVAL_MS = 5_000
const secureCookie = cookie => {
return window.location.protocol === 'https:' ? cookie + '; Secure' : cookie
}
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
export const AccountProvider = ({ children }) => {
const { me } = useMe()
const [accounts, setAccounts] = useState([])
const [isAnon, setIsAnon] = useState(true)
const [meAnon, setMeAnon] = useState(true)
const [errors, setErrors] = useState([])
const updateAccountsFromCookie = useCallback(() => {
try {
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
const accounts = multiAuthCookie
? JSON.parse(b64Decode(multiAuthCookie))
: me ? [{ id: Number(me.id), name: me.name, photoId: me.photoId }] : []
setAccounts(accounts)
// required for backwards compatibility: sync cookie with accounts if no multi auth cookie exists
// this is the case for sessions that existed before we deployed account switching
if (!multiAuthCookie && !!me) {
document.cookie = secureCookie(`multi_auth=${b64Encode(accounts)}; Path=/`)
}
} catch (err) {
console.error('error parsing cookies:', err)
}
}, [setAccounts])
useEffect(() => {
updateAccountsFromCookie()
const { [MULTI_AUTH_LIST]: listCookie } = cookie.parse(document.cookie)
const accounts = listCookie
? JSON.parse(b64Decode(listCookie))
: []
setAccounts(accounts)
}, [])
const addAccount = useCallback(user => {
setAccounts(accounts => [...accounts, user])
}, [setAccounts])
const removeAccount = useCallback(userId => {
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
}, [setAccounts])
const multiAuthSignout = useCallback(async () => {
// switch to next available account
const { status } = await fetch('/api/signout', { credentials: 'include' })
// if status is 201, this mean the server was able to switch us to the next available account
const nextAccount = useCallback(async () => {
const { status } = await fetch('/api/next-account', { credentials: 'include' })
// if status is 302, this means the server was able to switch us to the next available account
// and the current account was simply removed from the list of available accounts including the corresponding JWT.
// -> update needed to sync state with cookies
if (status === 201) updateAccountsFromCookie()
return status
const switchSuccess = status === 302
if (switchSuccess) updateAccountsFromCookie()
return switchSuccess
}, [updateAccountsFromCookie])
useEffect(() => {
// document not defined on server
if (SSR) return
const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
setIsAnon(multiAuthUserIdCookie === 'anonymous')
const checkErrors = useCallback(() => {
const {
[MULTI_AUTH_LIST]: listCookie,
[MULTI_AUTH_POINTER]: pointerCookie
} = cookie.parse(document.cookie)
const errors = []
if (!listCookie) errors.push(`${MULTI_AUTH_LIST} cookie not found`)
if (!pointerCookie) errors.push(`${MULTI_AUTH_POINTER} cookie not found`)
setErrors(errors)
}, [])
return <AccountContext.Provider value={{ accounts, addAccount, removeAccount, isAnon, setIsAnon, multiAuthSignout }}>{children}</AccountContext.Provider>
useEffect(() => {
if (SSR) return
updateAccountsFromCookie()
const { [MULTI_AUTH_POINTER]: pointerCookie } = cookie.parse(document.cookie)
setMeAnon(pointerCookie === 'anonymous')
const interval = setInterval(checkErrors, CHECK_ERRORS_INTERVAL_MS)
return () => clearInterval(interval)
}, [updateAccountsFromCookie, checkErrors])
const value = useMemo(
() => ({
accounts,
meAnon,
setMeAnon,
nextAccount,
multiAuthErrors: errors
}),
[accounts, meAnon, setMeAnon, nextAccount])
return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>
}
export const useAccounts = () => useContext(AccountContext)
const AccountListRow = ({ account, ...props }) => {
const { isAnon, setIsAnon } = useAccounts()
const { meAnon, setMeAnon } = useAccounts()
const { me, refreshMe } = useMe()
const anonRow = account.id === ANON_USER_ID
const selected = (isAnon && anonRow) || Number(me?.id) === Number(account.id)
const client = useApolloClient()
const anonRow = account.id === USER_ID.anon
const selected = (meAnon && anonRow) || Number(me?.id) === Number(account.id)
const router = useRouter()
// fetch updated names and photo ids since they might have changed since we were issued the JWTs
const [name, setName] = useState(account.name)
const [photoId, setPhotoId] = useState(account.photoId)
useQuery(USER,
const { data, error } = useQuery(USER,
{
variables: { id: account.id },
onCompleted ({ user: { name, photoId } }) {
if (photoId) setPhotoId(photoId)
if (name) setName(name)
}
variables: { id: account.id }
}
)
if (error) console.error(`query for user ${account.id} failed:`, error)
const name = data?.user?.name || account.name
const photoId = data?.user?.photoId || account.photoId
const onClick = async (e) => {
// prevent navigation
e.preventDefault()
document.cookie = secureCookie(`multi_auth.user-id=${anonRow ? 'anonymous' : account.id}; Path=/`)
// update pointer cookie
const options = cookieOptions({ httpOnly: false })
document.cookie = cookie.serialize(MULTI_AUTH_POINTER, anonRow ? MULTI_AUTH_ANON : account.id, options)
// update state
if (anonRow) {
// order is important to prevent flashes of no session
setIsAnon(true)
setMeAnon(true)
await refreshMe()
} else {
await refreshMe()
// order is important to prevent flashes of inconsistent data in switch account dialog
setIsAnon(account.id === ANON_USER_ID)
setMeAnon(account.id === USER_ID.anon)
}
await client.refetchQueries({ include: 'active' })
// reload whatever page we're on to avoid any bugs due to missing authorization etc.
router.reload()
}
return (
<div className='d-flex flex-row'>
<UserListRow user={{ ...account, photoId, name }} className='d-flex align-items-center me-2' {...props} onNymClick={onClick} />
{selected && <div className='text-muted fst-italic text-muted'>selected</div>}
<UserListRow
user={{ ...account, photoId, name }}
className='d-flex align-items-center me-2'
{...props}
onNymClick={onClick}
selected={selected}
/>
</div>
)
}
export default function SwitchAccountList () {
const { accounts } = useAccounts()
const { accounts, multiAuthErrors } = useAccounts()
const router = useRouter()
const addAccount = () => {
router.push({
pathname: '/login',
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
})
const hasError = multiAuthErrors.length > 0
if (hasError) {
return (
<>
<div className='my-2'>
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
<MultiAuthErrorBanner errors={multiAuthErrors} />
</div>
</div>
</>
)
}
// can't show hat since the streak is not included in the JWT payload
return (
<>
<div className='my-2'>
<div className='d-flex flex-column flex-wrap'>
<AccountListRow account={{ id: ANON_USER_ID, name: 'anon' }} showHat={false} />
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
<h4 className='text-muted'>Accounts</h4>
<AccountListRow account={{ id: USER_ID.anon, name: 'anon' }} showHat={false} />
{
accounts.map((account) => <AccountListRow key={account.id} account={account} showHat={false} />)
}
<div style={{ cursor: 'pointer' }} onClick={addAccount}>+ add account</div>
</div>
<Link
href={{
pathname: '/login',
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
}}
className='text-reset fw-bold'
>
<AddIcon height={20} width={20} /> another account
</Link>
</div>
</>
)

View File

@ -1,6 +1,6 @@
import Dropdown from 'react-bootstrap/Dropdown'
import styles from './item.module.css'
import MoreIcon from '../svgs/more-fill.svg'
import MoreIcon from '@/svgs/more-fill.svg'
export default function ActionDropdown ({ children }) {
if (!children) {

View File

@ -8,19 +8,26 @@ export default function ActionTooltip ({ children, notForm, disable, overlayText
if (!notForm) {
formik = useFormikContext()
}
if (disable) {
if (disable || !overlayText) {
return children
}
return (
<OverlayTrigger
placement={placement || 'bottom'}
overlay={
<Tooltip>
{overlayText || '1 sat'}
<Tooltip style={{ position: 'fixed' }}>
{overlayText}
</Tooltip>
}
trigger={['hover', 'focus']}
show={formik?.isSubmitting ? false : undefined}
popperConfig={{
modifiers: {
preventOverflow: {
enabled: false
}
}
}}
>
<span>
{children}

View File

@ -1,14 +1,20 @@
import { useState, useEffect, useMemo, useCallback } from 'react'
import AccordianItem from './accordian-item'
import { Input, InputUserSuggest, VariableInput, Checkbox } from './form'
import InputGroup from 'react-bootstrap/InputGroup'
import { BOOST_MIN, BOOST_MULT, MAX_FORWARDS } from '../lib/constants'
import { DEFAULT_CROSSPOSTING_RELAYS } from '../lib/nostr'
import { BOOST_MIN, BOOST_MULT, MAX_FORWARDS, SSR } from '@/lib/constants'
import { DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr'
import Info from './info'
import { numWithUnits } from '../lib/format'
import { abbrNum, numWithUnits } from '@/lib/format'
import styles from './adv-post-form.module.css'
import { useMe } from './me'
import { useRouter } from 'next/router'
import { useFeeButton } from './fee-button'
import { useRouter } from 'next/router'
import { useFormikContext } from 'formik'
import { gql, useQuery } from '@apollo/client'
import useDebounceCallback from './use-debounce-callback'
import { Button } from 'react-bootstrap'
import classNames from 'classnames'
const EMPTY_FORWARD = { nym: '', pct: '' }
@ -19,50 +25,241 @@ export function AdvPostInitial ({ forward, boost }) {
}
}
export default function AdvPostForm ({ children }) {
const FormStatus = {
DIRTY: 'dirty',
ERROR: 'error'
}
export function BoostHelp () {
return (
<ol style={{ lineHeight: 1.25 }}>
<li>Boost ranks items higher based on the amount</li>
<li>The highest boost in a territory over the last 30 days is pinned to the top of the territory</li>
<li>The highest boost across all territories over the last 30 days is pinned to the top of the homepage</li>
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
<li>Each {numWithUnits(BOOST_MULT, { abbreviate: false })} of boost is equivalent to a zap-vote from a maximally trusted stacker (very rare)
<ul>
<li>e.g. {numWithUnits(BOOST_MULT * 5, { abbreviate: false })} is like five zap-votes from a maximally trusted stacker</li>
</ul>
</li>
<li>boost can take a few minutes to show higher ranking in feed</li>
<li>100% of boost goes to the territory founder and top stackers as rewards</li>
</ol>
)
}
export function BoostInput ({ onChange, ...props }) {
const feeButton = useFeeButton()
let merge
if (feeButton) {
({ merge } = feeButton)
}
return (
<Input
label={
<div className='d-flex align-items-center'>boost
<Info>
<BoostHelp />
</Info>
</div>
}
name='boost'
onChange={(_, e) => {
merge?.({
boost: {
term: `+ ${e.target.value}`,
label: 'boost',
op: '+',
modifier: cost => cost + Number(e.target.value)
}
})
onChange && onChange(_, e)
}}
hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
{...props}
/>
)
}
const BoostMaxes = ({ subName, homeMax, subMax, boost, updateBoost }) => {
return (
<div className='d-flex flex-row mb-2'>
<Button
className={classNames(styles.boostMax, 'me-2', homeMax + BOOST_MULT <= (boost || 0) && 'invisible')}
size='sm'
onClick={() => updateBoost(homeMax + BOOST_MULT)}
>
{abbrNum(homeMax + BOOST_MULT)} <small>top of homepage</small>
</Button>
{subName &&
<Button
className={classNames(styles.boostMax, subMax + BOOST_MULT <= (boost || 0) && 'invisible')}
size='sm'
onClick={() => updateBoost(subMax + BOOST_MULT)}
>
{abbrNum(subMax + BOOST_MULT)} <small>top of ~{subName}</small>
</Button>}
</div>
)
}
// act means we are adding to existing boost
export function BoostItemInput ({ item, sub, act = false, ...props }) {
// act adds boost to existing boost
const existingBoost = act ? Number(item?.boost || 0) : 0
const [boost, setBoost] = useState(act ? 0 : Number(item?.boost || 0))
const { data, previousData, refetch } = useQuery(gql`
query BoostPosition($sub: String, $id: ID, $boost: Int) {
boostPosition(sub: $sub, id: $id, boost: $boost) {
home
sub
homeMaxBoost
subMaxBoost
}
}`,
{
variables: { sub: item?.subName || sub?.name, boost: existingBoost + boost, id: item?.id },
fetchPolicy: 'cache-and-network',
skip: !!item?.parentId || SSR
})
const getPositionDebounce = useDebounceCallback((...args) => refetch(...args), 1000, [refetch])
const updateBoost = useCallback((boost) => {
const boostToUse = Number(boost || 0)
setBoost(boostToUse)
getPositionDebounce({ sub: item?.subName || sub?.name, boost: Number(existingBoost + boostToUse), id: item?.id })
}, [getPositionDebounce, item?.id, item?.subName, sub?.name, existingBoost])
const dat = data || previousData
const boostMessage = useMemo(() => {
if (!item?.parentId && boost >= BOOST_MULT) {
if (dat?.boostPosition?.home || dat?.boostPosition?.sub || boost > dat?.boostPosition?.homeMaxBoost || boost > dat?.boostPosition?.subMaxBoost) {
const boostPinning = []
if (dat?.boostPosition?.home || boost > dat?.boostPosition?.homeMaxBoost) {
boostPinning.push('homepage')
}
if ((item?.subName || sub?.name) && (dat?.boostPosition?.sub || boost > dat?.boostPosition?.subMaxBoost)) {
boostPinning.push(`~${item?.subName || sub?.name}`)
}
return `pins to the top of ${boostPinning.join(' and ')}`
}
}
return 'ranks posts higher based on the amount'
}, [boost, dat?.boostPosition?.home, dat?.boostPosition?.sub, item?.subName, sub?.name])
return (
<>
<BoostInput
hint={<span className='text-muted'>{boostMessage}</span>}
onChange={(_, e) => {
if (e.target.value >= 0) {
updateBoost(Number(e.target.value))
}
}}
overrideValue={boost}
{...props}
groupClassName='mb-1'
/>
{!item?.parentId &&
<BoostMaxes
subName={item?.subName || sub?.name}
homeMax={(dat?.boostPosition?.homeMaxBoost || 0) - existingBoost}
subMax={(dat?.boostPosition?.subMaxBoost || 0) - existingBoost}
boost={existingBoost + boost}
updateBoost={updateBoost}
/>}
</>
)
}
export default function AdvPostForm ({ children, item, sub, storageKeyPrefix }) {
const { me } = useMe()
const router = useRouter()
const { merge } = useFeeButton()
const [itemType, setItemType] = useState()
const formik = useFormikContext()
const [show, setShow] = useState(false)
useEffect(() => {
const isDirty = formik?.values.forward?.[0].nym !== '' || formik?.values.forward?.[0].pct !== '' ||
formik?.values.boost !== '' || (router.query?.type === 'link' && formik?.values.text !== '')
// if the adv post form is dirty on first render, show the accordian
if (isDirty) {
setShow(FormStatus.DIRTY)
}
// HACK ... TODO: we should generically handle this kind of local storage stuff
// in the form component, overriding the initial values
if (storageKeyPrefix) {
for (let i = 0; i < MAX_FORWARDS; i++) {
['nym', 'pct'].forEach(key => {
const value = window.localStorage.getItem(`${storageKeyPrefix}-forward[${i}].${key}`)
if (value) {
formik?.setFieldValue(`forward[${i}].${key}`, value)
}
})
}
}
}, [formik?.values, storageKeyPrefix])
useEffect(() => {
// force show the accordian if there is an error and the form is submitting
const hasError = !!formik?.errors?.boost || formik?.errors?.forward?.length > 0
// if it's open we don't want to collapse on submit
setShow(show => hasError && formik?.isSubmitting ? FormStatus.ERROR : show)
}, [formik?.isSubmitting])
useEffect(() => {
const determineItemType = () => {
if (router && router.query.type) {
return router.query.type
} else if (item) {
const typeMap = {
url: 'link',
bounty: 'bounty',
pollCost: 'poll'
}
for (const [key, type] of Object.entries(typeMap)) {
if (item[key]) {
return type
}
}
return 'discussion'
}
}
const type = determineItemType()
setItemType(type)
}, [item, router])
function renderCrosspostDetails (itemType) {
switch (itemType) {
case 'discussion':
return <li>crosspost this discussion as a NIP-23 event</li>
case 'link':
return <li>crosspost this link as a NIP-01 event</li>
case 'bounty':
return <li>crosspost this bounty as a NIP-99 event</li>
case 'poll':
return <li>crosspost this poll as a NIP-41 event</li>
default:
return null
}
}
return (
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>}
show={show}
body={
<>
{children}
<Input
label={
<div className='d-flex align-items-center'>boost
<Info>
<ol className='fw-bold'>
<li>Boost ranks posts higher temporarily based on the amount</li>
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
<li>Each {numWithUnits(BOOST_MULT, { abbreviate: false })} of boost is equivalent to one trusted upvote
<ul>
<li>e.g. {numWithUnits(BOOST_MULT * 5, { abbreviate: false })} is like 5 votes</li>
</ul>
</li>
<li>The decay of boost "votes" increases at 1.25x the rate of organic votes
<ul>
<li>i.e. boost votes fall out of ranking faster</li>
</ul>
</li>
<li>100% of sats from boost are given back to top stackers as rewards</li>
</ol>
</Info>
</div>
}
name='boost'
onChange={(_, e) => merge({
boost: {
term: `+ ${e.target.value}`,
label: 'boost',
modifier: cost => cost + Number(e.target.value)
}
})}
hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<BoostItemInput item={item} sub={sub} />
<VariableInput
label='forward sats to'
name='forward'
@ -93,16 +290,16 @@ export default function AdvPostForm ({ children }) {
)
}}
</VariableInput>
{me && router.query.type === 'discussion' &&
{me && itemType &&
<Checkbox
label={
<div className='d-flex align-items-center'>crosspost to nostr
<Info>
<ul className='fw-bold'>
<li>crosspost this discussion item to nostr</li>
<ul>
{renderCrosspostDetails(itemType)}
<li>requires NIP-07 extension for signing</li>
<li>we use your NIP-05 relays if set</li>
<li>otherwise we default to these relays:</li>
<li>we use these relays by default:</li>
<ul>
{DEFAULT_CROSSPOSTING_RELAYS.map((relay, i) => (
<li key={i}>{relay}</li>

View File

@ -9,4 +9,11 @@
display: flex;
flex: 0 1 fit-content;
height: fit-content;
}
.boostMax small {
font-weight: 400;
margin-left: 0.25rem;
margin-right: 0.25rem;
opacity: 0.5;
}

Some files were not shown because too many files have changed in this diff Show More