Compare commits

..

601 commits
v1.2.0 ... main

Author SHA1 Message Date
Alex Gleason
8182fb43c1
Display all users in nostr.json 2025-11-14 02:04:28 -06:00
Alex Gleason
370deac1af
Bump gleasonator-policy in deno.lock 2025-06-06 08:16:07 -05:00
Alex Gleason
c79cd80859
Bump gleasonator-policy in deno.lock 2025-06-06 07:11:19 -05:00
Alex Gleason
24d27c4cdd
Index I/i tags 2025-06-01 19:45:36 -05:00
Alex Gleason
96799d1918
Index kind 1111 tags 2025-06-01 14:45:29 -05:00
Alex Gleason
293a0d10fb
Bump gleasonator-policy in lockfile 2025-05-09 13:03:41 -05:00
Alex Gleason
789b6c7e93
Guard against duplicate ephemeral events being streamed 2025-05-08 19:52:41 -05:00
Alex Gleason
984f76d184
DittoPgStore: remove fulfillment batching 2025-05-08 19:42:10 -05:00
Alex Gleason
fd87bad238
Add subscriptions bytes metrics 2025-04-29 13:24:36 -05:00
Alex Gleason
808c3f70f5 Merge branch 'fix-logging-defaults' into 'main'
fix logging default config

See merge request soapbox-pub/ditto!735
2025-04-29 18:06:56 +00:00
Siddharth Singh
b75cc0e372
fix early return in jsonl path for log level 2025-04-29 23:34:46 +05:30
Siddharth Singh
319620a414
remove relative import from ditto/utils in DittoConf 2025-04-29 23:32:37 +05:30
Siddharth Singh
e2ce3f32c3
fix logging default config 2025-04-29 23:25:38 +05:30
Alex Gleason
f8d4fa57c6
Also don't log more expected SQL errors 2025-04-29 12:54:36 -05:00
Alex Gleason
49e509e420
Fix LOG_CONFIG parsing 2025-04-29 12:42:40 -05:00
Alex Gleason
3a849700db
KyselyLogger: don't log expected errors 2025-04-29 12:32:44 -05:00
Alex Gleason
3a260f7873 Merge branch '10x-better-logging' into 'main'
Make logging 10x better

See merge request soapbox-pub/ditto!734
2025-04-29 14:24:48 +00:00
Alex Gleason
f7c8024c2f Merge branch 'mint-cashu' into 'main'
Allow users to mint for new tokens (cashu)

See merge request soapbox-pub/ditto!666
2025-04-22 15:34:19 +00:00
P. Reis
9d375f4afe goose created files remove 2025-04-08 12:23:12 -03:00
P. Reis
fb7c57a1d0 Merge branch 'main' into mint-cashu
Conflicts:
	packages/ditto/interfaces/DittoEvent.ts
	packages/ditto/storages/hydrate.ts
2025-04-08 12:16:53 -03:00
P. Reis
aadc3079fa refactor: make GET wallet a bit faster 2025-04-08 12:14:47 -03:00
Siddharth Singh
1b55932193
apply review fixes (thanks @alexgleason) 2025-04-08 17:09:05 +05:30
Siddharth Singh
8305cea81e
fmt ... again 2025-04-06 15:53:06 +05:30
Siddharth Singh
a1aed97dec
enable all subscopes of a given scope when it's supplied 2025-04-06 15:50:25 +05:30
Siddharth Singh
8f51a9d6d7
only show extra params when they are actually present 2025-04-06 15:46:24 +05:30
Siddharth Singh
1b19ff415a
add types to public api for dittoconf 2025-04-06 15:40:42 +05:30
Siddharth Singh
d3a15a699e
fmt 2025-04-06 15:33:36 +05:30
Siddharth Singh
82e7f854b9
replace logi handler 2025-04-06 15:28:54 +05:30
Siddharth Singh
6778a42d54
remove isCI conf var 2025-04-06 15:28:54 +05:30
Siddharth Singh
5dc2cc405f
add log configuration variables 2025-04-06 15:28:54 +05:30
Siddharth Singh
feab5f8cd0
stop logging every single query 2025-04-06 15:28:54 +05:30
Alex Gleason
63ebad0412 Merge branch 'client-tag-improvements' into 'main'
Client tag improvements

See merge request soapbox-pub/ditto!733
2025-04-03 17:48:06 +00:00
Alex Gleason
1c2d7a6793
Index client addr in search extensions 2025-04-03 12:38:39 -05:00
Alex Gleason
621d92dcf6
Render "client" tags in statuses even if they don't have an application handler event 2025-04-03 12:31:10 -05:00
Alex Gleason
0abee76e38
nip89: add ["t", "ditto"] tag to application handler event 2025-04-03 12:29:58 -05:00
Alex Gleason
c425a9d39a
DittoPgStore: index "client" tag 2025-04-03 12:29:45 -05:00
Alex Gleason
be5c58c615 Merge branch 'client-tag' into 'main'
Support NIP-89 "client" tag

See merge request soapbox-pub/ditto!732
2025-04-02 01:38:12 +00:00
Alex Gleason
23eb531305
Render client tags 2025-04-01 20:26:05 -05:00
Alex Gleason
caf59f4078
Insert NIP-89 "client" tags when users post statuses 2025-04-01 19:49:31 -05:00
Alex Gleason
c8b1c2050b
Publish nip89 application handler on startup 2025-04-01 18:55:36 -05:00
Alex Gleason
b56fe678f2 Merge branch 'fix-close' into 'main'
Fix relay always sending a CLOSED message after the client sends CLOSE

See merge request soapbox-pub/ditto!730
2025-03-28 22:53:05 +00:00
Alex Gleason
8dc9ea98e2
Fix relay always sending a CLOSED message after the client sends CLOSE 2025-03-28 17:50:51 -05:00
Alex Gleason
98c967dd22 Merge branch 'goosehints' into 'main'
Add .goosehints file

See merge request soapbox-pub/ditto!729
2025-03-28 21:24:08 +00:00
Alex Gleason
1a6c114a57
Fix inline codeblock in .goosehints 2025-03-28 16:22:03 -05:00
Alex Gleason
34a29c8f4e
NIP-11 improvements 2025-03-28 16:15:20 -05:00
Alex Gleason
278e13a6ef
Add .goosehints file 2025-03-28 16:08:28 -05:00
P. Reis
69fe86890f feat: add accepts_zaps_cashu boolean field to DittoEvent and hydrate it 2025-03-27 09:48:29 -03:00
P. Reis
8a75f9e944 feat: implement GET statuses/:id{[0-9a-f]{64}}/nutzapped_by (with tests) 2025-03-26 15:55:59 -03:00
P. Reis
71a558a9de Merge branch 'main' into mint-cashu 2025-03-25 18:23:13 -03:00
P. Reis
cda5196007 fix: remove unused import 2025-03-25 18:22:51 -03:00
P. Reis
7dc56f594b feat: add zaps_amount_cashu to event_stats (with tests)
add zapped_cashu and zaps_amount_cashu field to MastodonStatus
2025-03-25 18:19:40 -03:00
P. Reis
83c96c88b7 feat: support pagination in GET transactions
remove getTransactions function and replace it with renderTransaction function (all tests updated)
2025-03-24 23:02:04 -03:00
Alex Gleason
95c10fd2b9 Merge branch 'fix-nip05-notification' into 'main'
Fix name grant notification not being rendered

See merge request soapbox-pub/ditto!728
2025-03-23 17:15:59 +00:00
Alex Gleason
06c9c8d898
Fix name grant notification not being rendered 2025-03-23 12:11:39 -05:00
Alex Gleason
1ff83212c4 Merge branch 'fix-index-k-tag' into 'main'
fix: index k tag even if the value is not a number and count is less than 3

See merge request soapbox-pub/ditto!727
2025-03-21 20:59:27 +00:00
P. Reis
4451aafadc fix: index k tag even if the value is not a number and count is less than 3 2025-03-21 16:56:24 -03:00
P. Reis
1360484ae9 fix: call paginationSchema as a function 2025-03-21 10:49:58 -03:00
P. Reis
0b8b9e726a Merge branch 'main' into mint-cashu 2025-03-21 10:47:49 -03:00
P. Reis
7226d503f5 fix(getTransactions): coerce tags to string in schema
This is not really needed but it doesn't hurt
2025-03-21 10:09:38 -03:00
P. Reis
d69854fa3c fix: save amount tag value as a string 2025-03-21 10:07:34 -03:00
P. Reis
f9c4ec5835 feat: create GET transactions endpoint (with tests) 2025-03-20 14:02:43 -03:00
Alex Gleason
fab34dbf5b Merge branch 'fix-mem-leak' into 'main'
Fix memory leak in relay?

See merge request soapbox-pub/ditto!726
2025-03-19 20:20:13 +00:00
Alex Gleason
8fc0092150
Fix memory leak in relay? 2025-03-19 15:15:44 -05:00
P. Reis
06ac326ec3 refactor: add relay of conf.relay in case no relays are present 2025-03-19 13:01:06 -03:00
P. Reis
feff31f094 feat: allow to edit the wallet mints and relays (with tests updated) 2025-03-18 18:30:52 -03:00
P. Reis
7c1297e865 fix: remove unused import 2025-03-17 22:48:43 -03:00
P. Reis
46ab6005d1 fix: https://jsr.io/go/slow-type-missing-explicit-type 2025-03-17 22:47:38 -03:00
P. Reis
5ff8bf4479 refactor: remove useless comments 2025-03-17 22:41:36 -03:00
P. Reis
355c53fd64 refactor: create getWallet function, with tests 2025-03-17 22:28:36 -03:00
P. Reis
07d0d4c4e5 fix: only create kind 5 if there are events to delete 2025-03-17 11:12:30 -03:00
P. Reis
c344cb6b74 refactor: call organizeProofs in a try-catch block 2025-03-17 11:10:08 -03:00
P. Reis
6afa0bb7f1 refactor: simplify deletion of expired quote ids expiredQuoteIds 2025-03-17 11:01:11 -03:00
Alex Gleason
497b02002e
streaming: ensure close handler is called even when socket is closed by server 2025-03-16 18:02:28 -05:00
Alex Gleason
f27609feb8
Set a max subscriptions size per connection 2025-03-16 17:51:07 -05:00
P. Reis
0a05dd232c fix: only create new kind 7375 if there is proofs to keep 2025-03-15 20:48:33 -03:00
P. Reis
a719c2aabf fix: add swapNutzapsMiddleware to nutzap endpoint 2025-03-15 20:33:01 -03:00
P. Reis
0e0166419e test(nutzap): add more checks 2025-03-15 20:32:20 -03:00
Alex Gleason
a6b58b50cf Merge branch 'custom-reacts' into 'main'
Support custom emoji reactions

See merge request soapbox-pub/ditto!724
2025-03-15 22:21:09 +00:00
Alex Gleason
09b0bf8ef4
Test paginationSchema with a custom limit 2025-03-15 17:20:26 -05:00
Alex Gleason
88ef8087a5
Let paginationMiddleware be configurable, add pagination to reactions handler 2025-03-15 17:18:18 -05:00
Alex Gleason
36ffd4283a
Fix DittoPgStore req test 2025-03-15 17:03:20 -05:00
Alex Gleason
006a96d0eb
Disable notify in tests 2025-03-15 16:50:26 -05:00
Alex Gleason
66979c7c11
Disallow +/- emojis in Pleroma API 2025-03-15 16:42:34 -05:00
Alex Gleason
be922bf07f
Enable pleroma_custom_emoji_reactions feature in instance endpoint 2025-03-15 16:37:00 -05:00
Alex Gleason
753413f071
Support custom emoji reactions 2025-03-15 16:32:11 -05:00
Alex Gleason
c40c6e8b30
Support custom emoji reactions in event_stats 2025-03-15 15:31:24 -05:00
Alex Gleason
755ed884d4
Pleroma API supports custom emojis with or without colons 2025-03-15 15:01:07 -05:00
Alex Gleason
1dd50e2e63
Add tests for pleromaStatusesRoute 2025-03-15 14:38:19 -05:00
Alex Gleason
f27dac342f
Small refactors to pleromaStatusesRoute 2025-03-15 13:57:06 -05:00
Alex Gleason
2f65be7dc4
Move reaction controllers to pleromaStatusesRoute 2025-03-15 13:49:42 -05:00
Alex Gleason
55a171fd2c
Merge remote-tracking branch 'origin/main' into custom-reacts 2025-03-15 13:42:00 -05:00
Alex Gleason
c2f214c9bc Merge branch 'mw-cleanup' into 'main'
Remove redundant userMiddleware usages in app.ts

See merge request soapbox-pub/ditto!723
2025-03-15 18:41:43 +00:00
Alex Gleason
c9b0ffb537
Remove redundant userMiddleware usages in app.ts 2025-03-15 13:26:55 -05:00
Alex Gleason
ed0a8dc36b Merge branch 'emoji-unauth' into 'main'
Allow custom_emojis endpoint to be accessed without a user

See merge request soapbox-pub/ditto!722
2025-03-15 18:24:52 +00:00
Alex Gleason
66dcf98a22
Allow reacting with custom emoji 2025-03-15 13:24:02 -05:00
Alex Gleason
e5dd285e6b
Allow custom_emojis endpoint to be accessed without a user 2025-03-15 12:50:05 -05:00
Alex Gleason
d0d400901c Merge branch 'custom-emojis' into 'main'
Implement custom emojis

See merge request soapbox-pub/ditto!721
2025-03-15 05:22:14 +00:00
Alex Gleason
974e07981e
getCustomEmojis: new Map() -> emojis 2025-03-15 00:19:50 -05:00
Alex Gleason
28275b7611
Insert "emoji" tags into statuses 2025-03-15 00:16:52 -05:00
Alex Gleason
b1a1ace0ac
Refactor getCustomEmojis function, support emoji categories 2025-03-14 23:58:42 -05:00
Alex Gleason
f15b6f79c0
Implement custom emojis 2025-03-14 23:36:53 -05:00
P. Reis
e1d2139c31 test: POST nutzap endpoint, with full mocks 2025-03-14 19:30:50 -03:00
P. Reis
6a24ca126a refactor(cashu): move each fetch mock to its own test 2025-03-13 17:26:40 -03:00
P. Reis
cfdd5e1929 Merge branch 'main' into mint-cashu 2025-03-13 11:51:20 -03:00
Alex Gleason
bfe693c2f8 Merge branch 'fix-pglite-closed' into 'main'
fix: Error: PGlite is closed

See merge request soapbox-pub/ditto!720
2025-03-13 14:45:40 +00:00
P. Reis
bc0580dd05 fix: Error: PGlite is closed 2025-03-13 10:45:03 -03:00
P. Reis
921f478279 Merge branch 'main' into mint-cashu 2025-03-13 10:41:17 -03:00
Alex Gleason
60fbe70212 Merge branch 'strip-media-space' into 'main'
Strip media URLs regardless of whitespace delimiter

See merge request soapbox-pub/ditto!719
2025-03-12 21:55:42 +00:00
Alex Gleason
80fcda2b01
Fix matching multiple whitespace characters at the end of a string 2025-03-12 16:53:22 -05:00
Alex Gleason
69a9534463
removeTrailingUrls -> removeTrailingTokens 2025-03-12 16:48:31 -05:00
Alex Gleason
bd71b45a8d
Strip media URLs regardless of whitespace delimiter 2025-03-12 16:46:32 -05:00
P. Reis
37d81cf2a2 Merge branch 'main' into mint-cashu 2025-03-12 17:08:02 -03:00
Alex Gleason
2e4456dba2 Merge branch 'legacy-media-strip' into 'main'
Hide media URLs from posts without imeta tags

See merge request soapbox-pub/ditto!717
2025-03-12 17:04:00 +00:00
Alex Gleason
eec78efc0e
Hide media URLs from posts without imeta tags 2025-03-12 11:56:04 -05:00
P. Reis
8d0b5ba078 test: getMintsToProofs works 2025-03-12 13:15:52 -03:00
P. Reis
803b2abacc fix: add explicit return type - tokenEventSchema 2025-03-11 12:23:04 -03:00
P. Reis
dc711bce6a fix: add explicit return type 2025-03-11 11:49:39 -03:00
P. Reis
a002b1a005 refactor: create @ditto/cashu package 2025-03-11 11:34:42 -03:00
P. Reis
1c709b04be Merge branch 'main' into mint-cashu 2025-03-10 10:46:01 -03:00
Alex Gleason
0264b16553 Merge branch 'remove-console-log' into 'main'
refactor: remove console.logs

See merge request soapbox-pub/ditto!716
2025-03-09 21:56:12 +00:00
P. Reis
3e35102e13 refactor: remove console.logs 2025-03-09 13:02:53 -03:00
Alex Gleason
592d6c1d36
Upgrade gleasonator-policy in deno.lock 2025-03-08 21:03:58 -06:00
Alex Gleason
026568ea71
Fix firehose 2025-03-08 21:03:36 -06:00
Alex Gleason
6b3f5ac8a6
Expose db to custom policy script 2025-03-08 20:44:22 -06:00
Alex Gleason
a400021a0d
Bump signal to 5s 2025-03-08 19:44:16 -06:00
Alex Gleason
3cac9b6e26 Merge branch 'link-preview-db' into 'main'
Store link previews in the database

Closes #301

See merge request soapbox-pub/ditto!715
2025-03-09 01:39:56 +00:00
Alex Gleason
26990bcf5e
Remove linkPreview cache config, rm stray console.log 2025-03-08 19:36:53 -06:00
Alex Gleason
affea45a08
Store link previews in the database
Fixes https://gitlab.com/soapbox-pub/ditto/-/issues/301
2025-03-08 19:33:15 -06:00
Alex Gleason
1abe487115 Merge branch 'lookup' into 'main'
Look up identifiers in search on remote relays

See merge request soapbox-pub/ditto!714
2025-03-09 00:41:53 +00:00
Alex Gleason
f01b4c0791
Look up identifiers in search on remote relays 2025-03-08 18:36:24 -06:00
Alex Gleason
478debfda1 Merge branch 'outboxy-req-router' into 'main'
Add an outboxy-style REQ router

See merge request soapbox-pub/ditto!713
2025-03-08 21:52:17 +00:00
Alex Gleason
239c427c78
Add DittoPool tests 2025-03-08 15:46:02 -06:00
Alex Gleason
920e558c06
Add an outboxy-style REQ router 2025-03-08 15:29:31 -06:00
Alex Gleason
7248aaf6c6 Merge branch 'req-abort' into 'main'
Fix REQ abort logic

See merge request soapbox-pub/ditto!712
2025-03-08 05:01:13 +00:00
Alex Gleason
e085082a76
Fix REQ abort logic 2025-03-07 22:58:55 -06:00
Alex Gleason
413056e841
Prewarm cards only for kind 1 2025-03-07 16:31:01 -06:00
Alex Gleason
f12cce9802 Merge branch 'fix-relay-close' into 'main'
Ensure relay close handler is called

See merge request soapbox-pub/ditto!711
2025-03-07 18:52:06 +00:00
Alex Gleason
5162e051ad
Ensure relay close handler is called 2025-03-07 12:34:47 -06:00
Alex Gleason
f8eb4fc79c
Add a metric for active author subscriptions 2025-03-06 22:55:23 -06:00
Alex Gleason
9c3307f923
Purify event before sending it out to relay 2025-03-06 15:38:14 -06:00
Alex Gleason
e5e0ddd7aa
DittoPgStore: purify events before streaming them 2025-03-06 15:34:31 -06:00
Alex Gleason
6eace14850 Merge branch 'fetch-authors' into 'main'
Automatically fetch missing authors from pool

See merge request soapbox-pub/ditto!710
2025-03-06 21:12:43 +00:00
Alex Gleason
e85e89e0af
Automatically fetch missing authors from pool 2025-03-06 14:55:40 -06:00
Alex Gleason
1ab77fdeab Merge branch 'fix-revoke-nip05' into 'main'
fix(attempt): revoke username

See merge request soapbox-pub/ditto!700
2025-03-05 23:31:17 +00:00
P. Reis
e549a9d34a refactor: stop fetching author and set nip05_last_verified_at to null 2025-03-05 20:28:22 -03:00
P. Reis
9e726baa2a refactor: remove try catch 2025-03-05 20:15:16 -03:00
P. Reis
1eb1f4206d refactor: use db.kysely.updateTable rather than db.kysely.insertInto 2025-03-05 20:12:42 -03:00
P. Reis
19244aec2c test/fix: use waitFor function 2025-03-05 19:35:25 -03:00
Alex Gleason
811a56e406
ci: try moving the timeout to the actual test job 2025-03-05 15:05:26 -06:00
Alex Gleason
9b422d8e31
Add note about hanging queries in DittoPostgres test 2025-03-05 14:47:35 -06:00
Alex Gleason
0b72533b05
DittoPglite: test that queries reject after it's closed 2025-03-05 14:06:13 -06:00
Alex Gleason
4e0479f7c8
Add DittoPostgres test 2025-03-05 13:27:34 -06:00
Alex Gleason
c7175f8301
ci: add 2 minute timeout 2025-03-05 13:02:01 -06:00
Alex Gleason
f3bdabc13a
DittoRelayStore: improve error handling around this.listen(), remove await 2025-03-05 12:44:16 -06:00
P. Reis
842150b6e2 fix: remove useless 'p' tag in admin kind 5 event 2025-03-05 11:33:57 -03:00
P. Reis
fc4f5fd522 Merge branch 'main' into fix-revoke-nip05
Conflicts:
	packages/ditto/storages/DittoRelayStore.test.ts
2025-03-05 11:25:04 -03:00
P. Reis
d8a0eca891 refactor: get author from grant event (30360), before doing the admin deletion 2025-03-05 11:22:07 -03:00
P. Reis
dd5397e795 test: check if nip05 exists and then check again to see if it's null 2025-03-05 11:02:13 -03:00
P. Reis
dfff63fab4 fix: await Promise.allSettled 2025-03-05 11:00:28 -03:00
Alex Gleason
b5858dd54c
Catch pool.query 2025-03-04 23:09:20 -06:00
Alex Gleason
815b903e28
Add a timeout on pool.query 2025-03-04 23:07:45 -06:00
Alex Gleason
773cee8b92 Merge branch 'fetch-related' into 'main'
Fetch related events by ID through the RelayStore

See merge request soapbox-pub/ditto!709
2025-03-05 04:28:48 +00:00
Alex Gleason
c5e9017d24
Fetch related events by ID through the RelayStore 2025-03-04 22:14:56 -06:00
P. Reis
ab6054efe8 Merge branch 'main' into fix-revoke-nip05
Conflicts:
	packages/ditto/controllers/api/admin.ts
	packages/ditto/storages/DittoRelayStore.test.ts
2025-03-04 14:53:21 -03:00
P. Reis
aa1311ccae fix: call store.event 2025-03-04 11:42:42 -03:00
P. Reis
c5fdc97e58 refactor: stop returning author 2025-03-04 10:51:10 -03:00
Alex Gleason
c39ea9212f
Lowercase all captcha bg filenames for suitability in a URL path 2025-03-03 19:37:35 -06:00
Alex Gleason
29109cec21
Give @ditto/ditto 1.1.0 2025-03-03 19:34:05 -06:00
Alex Gleason
2ca216dfdf
Set all packages to version 0.1.0 2025-03-03 19:31:41 -06:00
P. Reis
44d525eccb fix: use author pubkey, not admin pubkey 2025-03-03 21:17:17 -03:00
Alex Gleason
363816d930
Fix nostr.json cache by returning non-200 status 2025-03-03 17:10:46 -06:00
Alex Gleason
ff4c3381ba Merge branch 'feat-promove-admin' into 'main'
feat: promote users to admin

Closes #298

See merge request soapbox-pub/ditto!707
2025-03-03 22:56:18 +00:00
Alex Gleason
0d1b7b8d37
Fix DittoRoute test 2025-03-03 16:53:54 -06:00
Alex Gleason
8528c4c39e
Add more permission group tests 2025-03-03 16:36:38 -06:00
Alex Gleason
b7bf2fc76f
Move Pleroma permission_groups controller to its own routes file, add tests 2025-03-03 16:33:28 -06:00
Alex Gleason
d29bc8c020
Merge remote-tracking branch 'origin/main' into feat-promove-admin 2025-03-03 16:00:16 -06:00
Alex Gleason
a2f019993d
Simplify promote controller 2025-03-03 15:58:13 -06:00
Alex Gleason
10ea6f7635 Merge branch 'name-request-refactor' into 'main'
Name request refactor

See merge request soapbox-pub/ditto!708
2025-03-03 21:48:18 +00:00
P. Reis
7525cd6ef9 refactor: set nip05 to null in handleRevokeNip05 function 2025-03-03 18:42:26 -03:00
Alex Gleason
bcd2ed18ef
Fix error handling of DittoRoute 2025-03-03 15:41:24 -06:00
Alex Gleason
b8b6174fcc
Test that DittoRelayStore generates nip05 request set event 2025-03-03 15:24:26 -06:00
Alex Gleason
0dd085b559
Remove old testApp 2025-03-03 14:46:05 -06:00
Alex Gleason
9be9f7c9d0
Add tests for dittoNameRoute 2025-03-03 14:39:01 -06:00
P. Reis
28c3b07a3e fix: remove unused imports 2025-03-03 17:34:13 -03:00
P. Reis
3bcf716795 refactor: simplify promote user to admin 2025-03-03 17:32:28 -03:00
Alex Gleason
14b809b1e8
db: add TestDB module 2025-03-03 12:21:02 -06:00
Alex Gleason
a21ec4600a
Move /api/v1/ditto/names to a DittoRoute 2025-03-03 11:50:49 -06:00
Alex Gleason
59ad40fb3f
Remove extra space 2025-03-02 18:03:21 -06:00
Alex Gleason
484625842b
Skip ffmpeg tests in CI 2025-03-02 18:02:03 -06:00
Alex Gleason
ac0285293e
Remove pipelineEncounters.ts 2025-03-02 14:06:15 -06:00
Alex Gleason
2415dbe4e5 Merge branch 'transcode' into 'main'
Transcode uploaded videos with ffmpeg

See merge request soapbox-pub/ditto!706
2025-03-02 05:41:29 +00:00
Alex Gleason
7e8ff1f329
Get video dimensions from frame 2025-03-01 22:15:32 -06:00
Alex Gleason
5168ccd748
Write uploads to disk so ffmpeg can seek properly 2025-03-01 20:59:14 -06:00
Alex Gleason
c2e6e10a3a
Don't call ffprobe unless mediaTranscode is enabled 2025-03-01 18:03:57 -06:00
Alex Gleason
3035ecaca9
Make transcoding optional, refactor a bit 2025-03-01 17:45:01 -06:00
Alex Gleason
414a3b7651
Let FFMPEG_PATH and FFPROBE_PATH be configurable 2025-03-01 17:01:39 -06:00
Alex Gleason
8a94be803d
Give transcoded videos the proper file type 2025-03-01 16:39:09 -06:00
Alex Gleason
8c61efd75e
Improve blurhash performance of HD images 2025-03-01 13:56:17 -06:00
Alex Gleason
9051a59733
Measure performance of uploads 2025-03-01 13:09:42 -06:00
Alex Gleason
bd4c94852f
Call ffprobe, improve transcoding 2025-02-28 21:21:30 -06:00
Alex Gleason
2533d2f469
Support readable in extractVideoFrame 2025-02-28 20:52:11 -06:00
Alex Gleason
a28e6509fd
Make analyze return a lot more information 2025-02-28 19:48:53 -06:00
Alex Gleason
ad9cc676e6
Improve getVideoDimensions 2025-02-28 18:41:13 -06:00
Alex Gleason
5f10f92d4e
Let ffprobe pipe data to stdin 2025-02-28 18:29:41 -06:00
P. Reis
c79867d8ca feat: promote users to admin 2025-02-28 18:25:34 -03:00
Alex Gleason
d70a00a8ca
Expose thumb and image to Attachment view, don't set thumb (since it's redundant with image) 2025-02-28 10:51:08 -06:00
Alex Gleason
6774975051
Get blurhash and dimensions of video 2025-02-28 10:24:45 -06:00
Alex Gleason
7ecca587a9
ffmpeg: explicitly set stdin to null 2025-02-28 10:20:47 -06:00
Alex Gleason
aeaa60df45
transcode: add -nostdin -safe 1 2025-02-28 10:17:39 -06:00
Alex Gleason
43ec58085a
Extract a poster image from videos (not efficient yet) 2025-02-28 09:58:28 -06:00
Alex Gleason
813026e734
Transcode video before uploading 2025-02-28 09:36:49 -06:00
P. Reis
dcfdfb1c7f Merge branch 'main' into mint-cashu
Conflicts:
	packages/ditto/controllers/api/cashu.ts
2025-02-28 10:51:02 -03:00
P. Reis
822f623018 refactor: this.handleRevokeNip05 before relay.event 2025-02-28 10:19:10 -03:00
Alex Gleason
26ca4aa7f7
Go back to just installing ffmpeg with apt 2025-02-28 00:01:28 -06:00
Alex Gleason
57c1871852
Try installing ffmpeg with curl 2025-02-27 23:54:06 -06:00
Alex Gleason
39821f8371
Install ffmpeg in GitLab CI 2025-02-27 23:47:27 -06:00
Alex Gleason
4f2aad9c5d
Export stuff from transcode/mod.ts 2025-02-27 23:42:29 -06:00
Alex Gleason
8f02c4125a
Add ffprobe module 2025-02-27 23:38:57 -06:00
Alex Gleason
e46b7bfa85
Rework ffmpeg to accept file URIs 2025-02-27 23:19:27 -06:00
Alex Gleason
d36efb7a30
transcode: export existing code in mod.ts 2025-02-27 22:06:03 -06:00
Alex Gleason
6ce64822e1
Split transcode and ffmpeg into separate modules 2025-02-27 21:59:19 -06:00
Alex Gleason
bb13a8dc71
Add ffmpeg module 2025-02-27 21:34:05 -06:00
Alex Gleason
bd47ae5930
Add @ditto/transcode package 2025-02-27 21:22:19 -06:00
Alex Gleason
462c00a3e7
transcode: simplify pipe 2025-02-27 20:16:25 -06:00
Alex Gleason
044930cc8d
transcodeVideoStream -> transcodeVideo 2025-02-27 20:08:26 -06:00
Alex Gleason
25d5db8db6
transcode: use a structured object for ffmpeg options 2025-02-27 20:07:28 -06:00
Alex Gleason
82f16e0cfe
Add transcode module 2025-02-27 20:01:11 -06:00
Alex Gleason
5fc874b768 Merge branch 'deno-2.2.2' into 'main'
Upgrade Deno to v2.2.2

See merge request soapbox-pub/ditto!705
2025-02-28 01:21:25 +00:00
Alex Gleason
96c845205b
Upgrade Deno to v2.2.2 2025-02-27 19:19:33 -06:00
Alex Gleason
381cbfd36f
Add ip to logiMiddleware 2025-02-27 18:24:05 -06:00
Alex Gleason
d458fc8464 Merge branch 'request-id' into 'main'
Add requestId middleware

See merge request soapbox-pub/ditto!704
2025-02-28 00:10:49 +00:00
Alex Gleason
368bf91b1f
Log requestId in some convenient spots 2025-02-27 18:08:55 -06:00
Alex Gleason
0cdb7b8cd5
Add requestId middleware 2025-02-27 17:57:22 -06:00
Alex Gleason
f683642478 Merge branch 'upload-conf' into 'main'
Remove `@/config.ts` import from utils/upload.ts

See merge request soapbox-pub/ditto!703
2025-02-27 23:37:57 +00:00
Alex Gleason
ee58a9f265 Merge branch 'metadata-conf' into 'main'
Remove `@/config.ts` import from utils/instance.ts

See merge request soapbox-pub/ditto!702
2025-02-27 23:34:46 +00:00
Alex Gleason
1bce9e2982
Remove @/config.ts import from utils/upload.ts 2025-02-27 17:34:35 -06:00
Alex Gleason
2acd23ebbe
Remove @/config.ts import from utils/instance.ts 2025-02-27 17:32:54 -06:00
Alex Gleason
f7ab4a6763 Merge branch 'stats-conf' into 'main'
Remove `@/config.ts` import from utils/stats.ts

See merge request soapbox-pub/ditto!701
2025-02-27 22:58:12 +00:00
Alex Gleason
70d8c2ed59
DittoPgStore: destructure conf 2025-02-27 16:54:12 -06:00
Alex Gleason
7e00b9f9a2
Add PolicyWorker test 2025-02-27 16:52:09 -06:00
Alex Gleason
5f5d0bc324
Remove @/config.ts import from utils/stats.ts 2025-02-27 16:35:06 -06:00
P. Reis
4792e568ef fix: event.pubkey, not event.id in p tag 2025-02-27 19:15:33 -03:00
P. Reis
e6f4f8d23e fix(attempt): revoke username 2025-02-27 19:10:13 -03:00
Alex Gleason
ddf1a9d6dc Merge branch 'pleroma-conf' into 'main'
Remove `@/config.ts` import from utils/pleroma.ts

See merge request soapbox-pub/ditto!699
2025-02-27 20:27:21 +00:00
Alex Gleason
7fe06753aa
Remove @/config.ts import from utils/pleroma.ts 2025-02-27 14:25:09 -06:00
Alex Gleason
fa0add60d5 Merge branch 'note-conf' into 'main'
Remove `@/config.ts` import from utils/note.ts

See merge request soapbox-pub/ditto!698
2025-02-27 20:16:53 +00:00
Alex Gleason
0e667995c1
Remove @/config.ts import from utils/note.ts 2025-02-27 14:15:02 -06:00
Alex Gleason
c07c88f2f5 Merge branch 'zapsplit-conf' into 'main'
Remove `@/config.ts` import from utils/zap-split.ts

See merge request soapbox-pub/ditto!697
2025-02-27 20:04:34 +00:00
Alex Gleason
2266152df3
Remove @/config.ts import from utils/zap-split.ts 2025-02-27 14:02:44 -06:00
Alex Gleason
549f3ebc5d Merge branch 'conf-tests' into 'main'
Remove `@/config.ts` imports from tests

See merge request soapbox-pub/ditto!696
2025-02-27 19:57:48 +00:00
Alex Gleason
23b8edefef
Remove @/config.ts imports from tests 2025-02-27 13:56:02 -06:00
Alex Gleason
4b1d7c1502 Merge branch 'sentry-conf' into 'main'
Remove `@/config.ts` import when starting Sentry

See merge request soapbox-pub/ditto!695
2025-02-27 19:53:51 +00:00
Alex Gleason
432f65ff61
Remove @/config.ts import when starting Sentry 2025-02-27 13:51:57 -06:00
Alex Gleason
92da5e6ac3
Actually fix policy.worker import (hack) 2025-02-26 18:19:30 -06:00
Alex Gleason
55115d3592
PolicyWorker: move Deno.env set above imports 2025-02-26 18:14:42 -06:00
Alex Gleason
ccf6d2074e Merge branch 'mastoapi-entities' into 'main'
Move ditto/interfaces to @ditto/mastoapi/types

See merge request soapbox-pub/ditto!693
2025-02-27 00:03:45 +00:00
Alex Gleason
b81da2c0d7
Move ditto/interfaces to @ditto/mastoapi/types 2025-02-26 18:01:32 -06:00
P. Reis
7e4e6635ad fix: type error 2025-02-26 20:54:11 -03:00
P. Reis
53f83f78c7 Merge branch 'main' into mint-cashu 2025-02-26 20:48:38 -03:00
P. Reis
8479143ed8 refactor: minimalist check of status_id and account_id 2025-02-26 20:48:12 -03:00
Alex Gleason
466245633c Merge branch 'fix-ensure-remove-exists' into 'main'
fix: call remove if it's available

See merge request soapbox-pub/ditto!692
2025-02-26 23:43:25 +00:00
P. Reis
0f20f1b26b refactor: return 500 http code in case of error 2025-02-26 20:21:42 -03:00
P. Reis
0c7ab03643 refactor: use ?. 2025-02-26 20:06:57 -03:00
P. Reis
40824280b4 fix: call remove if it's available 2025-02-26 19:53:55 -03:00
Alex Gleason
ec3fa3daeb Merge branch 'rm-config' into 'main'
Remove precheck.ts, perform check in DittoConf constructor

See merge request soapbox-pub/ditto!691
2025-02-26 22:30:27 +00:00
Alex Gleason
f650541b23
Remove precheck.ts, perform check in DittoConf constructor 2025-02-26 16:16:40 -06:00
Alex Gleason
7256b5dcf8 Merge branch 'dittoapp-opts' into 'main'
DittoApp: combine opts and vars into one object

See merge request soapbox-pub/ditto!690
2025-02-26 21:21:21 +00:00
Alex Gleason
89905d76a6
DittoApp: combine opts and vars into one object 2025-02-26 15:19:20 -06:00
Alex Gleason
c889cd68d2 Merge branch 'ditto-captcha' into 'main'
Add @ditto/captcha package

See merge request soapbox-pub/ditto!689
2025-02-26 21:14:25 +00:00
Alex Gleason
4b55acb796
Clean up captchaController 2025-02-26 15:10:41 -06:00
Alex Gleason
a2aaa55b89
Add tests to captcha modules 2025-02-26 15:09:01 -06:00
Alex Gleason
5f617b2d1a
Break captcha package into smaller modules 2025-02-26 14:53:13 -06:00
Alex Gleason
a2732642a5
Break @ditto/captcha into a separate library 2025-02-26 14:46:47 -06:00
Alex Gleason
4b321f21fc Merge branch 'refactor-remove-await' into 'main'
refactor: remove await

See merge request soapbox-pub/ditto!688
2025-02-26 14:30:54 +00:00
P. Reis
1730274e70 refactor: remove await 2025-02-26 11:19:15 -03:00
Alex Gleason
e17360d49c Merge branch 'fix-mentions-bug' into 'main'
fix: filter out invalid pubkeys

Closes #290

See merge request soapbox-pub/ditto!687
2025-02-25 23:26:22 +00:00
P. Reis
c82cfb9e8b refactor: remove duplicate filter 2025-02-25 20:16:28 -03:00
P. Reis
05a4a5a5c9 fix: filter out invalid pubkeys 2025-02-25 20:04:28 -03:00
P. Reis
cf4d888ad5 fix: total balance can be equal to the amount, access undefined with ? 2025-02-25 16:20:40 -03:00
P. Reis
e08603a42a fix: get lock pubkey from nutzap info 2025-02-25 15:59:40 -03:00
P. Reis
51faffc9e2 fix: get mints from nutzap info 2025-02-25 15:56:41 -03:00
P. Reis
33b8078fc6 fix: add pubkey lock to proofs 2025-02-25 13:58:57 -03:00
P. Reis
99a20bd129 Merge branch 'main' into mint-cashu
Conflicts:
	packages/ditto/controllers/api/cashu.ts
	packages/ditto/middleware/swapNutzapsMiddleware.ts
2025-02-24 20:07:12 -03:00
P. Reis
1d2a317029 feat: nutzap a post or user (no tests) 2025-02-24 17:34:14 -03:00
Alex Gleason
73beef72ac Merge branch 'pool-whoops' into 'main'
Switch the pools, whoops

See merge request soapbox-pub/ditto!686
2025-02-24 14:51:55 +00:00
Alex Gleason
11a589fb01
Switch the pools, whoops 2025-02-24 08:50:06 -06:00
Alex Gleason
e78e0c2460
Upgrade Nostrify 2025-02-23 23:08:46 -06:00
Alex Gleason
40221410e8 Merge branch 'apistore' into 'main'
Replace pipeline with DittoAPIStore

See merge request soapbox-pub/ditto!685
2025-02-24 03:05:42 +00:00
Alex Gleason
9df50a5b0d
app.ts: minor variable name cleanup 2025-02-23 20:53:48 -06:00
Alex Gleason
751c09035c
Pass DittoAPIStore to MastoAPI endpoints, DittoRelayStore to /relay 2025-02-23 19:41:17 -06:00
Alex Gleason
cce78f2b0c
Make DittoAPIStore and DittoRelay separate things 2025-02-23 18:54:04 -06:00
P. Reis
61cc7c335a feat: create organizedProofs function
test: organizedProofs function
2025-02-23 21:06:34 -03:00
Alex Gleason
52a9017730
Add a new DittoAPIStore extending DittoRelayStore 2025-02-23 18:03:41 -06:00
Alex Gleason
ec498653fb
Rename DittoAPIStore to DittoRelayStore 2025-02-23 18:02:45 -06:00
Alex Gleason
77c0ac3561
Hotfix for relay publishing 2025-02-23 14:55:19 -06:00
Alex Gleason
e88a7d01d4
Purify event before sending to pool 2025-02-23 14:50:45 -06:00
Alex Gleason
e99496306a
DittoAPIStore: fix handleEvent not being called 2025-02-23 13:08:19 -06:00
Alex Gleason
44f3721d36
DittoAPIStore: test that kind 0 with nip05 updates author_stats table 2025-02-23 12:41:59 -06:00
Alex Gleason
3f9f0468d2
Remove now unnecessary idleTimeout opt from socket upgrades 2025-02-23 12:05:53 -06:00
Alex Gleason
decb3ac618
Fix streaming API hydration ( ͡° ͜ʖ ͡°) 2025-02-23 11:56:29 -06:00
Alex Gleason
a52fe9fbc6
Try to fix pkey migration 2025-02-23 09:30:00 -06:00
Alex Gleason
02d4235abd
Rename nostr_events_new_pkey to nostr_events_pkey 2025-02-23 09:21:14 -06:00
Alex Gleason
497d5d12c9
Fix DittoPgStore tests 2025-02-23 00:39:41 -06:00
Alex Gleason
a9c696936b
Upgrade Nostrify 2025-02-23 00:11:42 -06:00
Alex Gleason
237f6e55ad
Fix DittoEnv type check 2025-02-22 21:52:26 -06:00
Alex Gleason
f1cb8c778a
Normalize Link header URLs 2025-02-22 21:45:04 -06:00
Alex Gleason
7f059b4dac
Fix event hydration with getEvent/getAuthor 2025-02-22 21:40:53 -06:00
Alex Gleason
6f1312b67f
Remove old paginationSchema 2025-02-22 21:38:48 -06:00
Alex Gleason
979f2cffb4
Fix stats:recompute script 2025-02-22 21:30:57 -06:00
Alex Gleason
1ae9da5793
Fix hydrate tests 2025-02-22 21:26:23 -06:00
Alex Gleason
6cd64500ce
Fix stats test 2025-02-22 21:24:43 -06:00
Alex Gleason
4f46a69131
I did a fucked up polymorphism 2025-02-22 20:56:53 -06:00
Alex Gleason
f893a81464
DittoAPIStore: add limit 0 2025-02-22 19:55:58 -06:00
Alex Gleason
70f0eb3b03
Fix pagination lint errors 2025-02-22 19:49:43 -06:00
Alex Gleason
f2e2072184
Export PolicyWorker as a regular class 2025-02-22 19:46:38 -06:00
Alex Gleason
3b17fd9b45
Remove @/storages.ts (jesus christ) 2025-02-22 19:27:53 -06:00
Alex Gleason
ca5c887705
Remove storages.ts from scripts 2025-02-22 15:54:31 -06:00
Alex Gleason
63c0f8b032
ditto/db: make adapters use classes instead of static classes 2025-02-22 15:32:47 -06:00
Alex Gleason
398d79b45e
DittoAPIStore: console.error -> logi 2025-02-22 13:05:50 -06:00
Alex Gleason
79fc568548
Add DittoPool class 2025-02-22 12:59:36 -06:00
Alex Gleason
2f0dbc44e4
Copy all the pipeline logic into DittoAPIStore (and some into DittoPgStore) 2025-02-22 12:01:50 -06:00
Alex Gleason
48bd7618f7
Start building DittoAPIStore 2025-02-22 03:13:05 -06:00
Alex Gleason
02e284f3aa
Remove unused DittoFilter interface 2025-02-22 01:06:53 -06:00
Alex Gleason
045eb4e1d6 Merge branch 'router' into 'main'
Switch to @ditto/router

See merge request soapbox-pub/ditto!683
2025-02-22 07:05:11 +00:00
Alex Gleason
4ed0640766
@ditto/router -> @ditto/mastoapi/router 2025-02-21 23:32:15 -06:00
Alex Gleason
ecb2b8d8f1 Merge branch 'lowercase-nip05' into 'main'
Make NIP-05 case insensitive

See merge request soapbox-pub/ditto!684
2025-02-22 03:59:20 +00:00
Alex Gleason
084c6aa944
Fix DummyDB test 2025-02-21 21:55:00 -06:00
Alex Gleason
8ef03e7926
Merge remote-tracking branch 'origin/lowercase-nip05' into router 2025-02-21 21:54:32 -06:00
Alex Gleason
6c70b4bc4e
Make NIP-05 case insensitive 2025-02-21 21:51:33 -06:00
Alex Gleason
07b68b71d2
Add missing types to testApp 2025-02-21 21:31:42 -06:00
Alex Gleason
9c97cc387f
mastoapi: add a test module 2025-02-21 21:15:57 -06:00
Alex Gleason
d4fc10fe3e
Add userMiddleware tests 2025-02-21 21:09:22 -06:00
Alex Gleason
403b16a67b
Merge remote-tracking branch 'origin/main' into router 2025-02-21 20:51:33 -06:00
Alex Gleason
4cfb6543c7
Don't lowercase nip05 name before fetching (for now) 2025-02-21 20:50:09 -06:00
Alex Gleason
8437da1200
Fix error handling in nameRequestController 2025-02-21 20:33:44 -06:00
Alex Gleason
5fec5deb06
publishEvent: publish to pool in background, catch errors and log 2025-02-21 20:22:58 -06:00
Alex Gleason
82446e3ef1
Add method and pathname to ditto.http error 2025-02-21 20:06:39 -06:00
Alex Gleason
d2abb1f1e4
Fix MuteListPolicy test 2025-02-21 18:59:14 -06:00
Alex Gleason
6b1aadc24c
nip98: add explicit types to exported functions 2025-02-21 18:46:43 -06:00
Alex Gleason
26e87b3962
tokenMiddleware: pass token to streaming API 2025-02-21 17:44:56 -06:00
Alex Gleason
806bfc1b45
Delete auth98Middleware, replace with userMiddleware 2025-02-21 16:54:38 -06:00
Alex Gleason
adeff1cae5
tokenMiddleware: support nip98 auth 2025-02-21 15:53:29 -06:00
Alex Gleason
f0add87c6d
Create @ditto/nip98 package 2025-02-21 15:35:03 -06:00
Alex Gleason
72851bc536
Remove AdminStore from storages 2025-02-21 15:08:58 -06:00
Alex Gleason
e5657d67c0
app -> route 2025-02-21 15:08:37 -06:00
Alex Gleason
d0c7cc7a45
Improve cashu test 2025-02-21 15:05:54 -06:00
Alex Gleason
438ab09216
Split userMiddleware into tokenMiddleware and a new userMiddleware 2025-02-21 14:52:17 -06:00
P. Reis
ac684194a0 feat: create tokenEventSchema and add tests for cashu schemas 2025-02-21 16:48:44 -03:00
Alex Gleason
5ad7f1d5d7
userMiddleware -> tokenMiddleware 2025-02-21 13:27:19 -06:00
P. Reis
08d2f7fe3f refactor: add cashu proof schema 2025-02-21 11:41:45 -03:00
Alex Gleason
f83925331a
Apply the UserStore to the userMiddleware 2025-02-20 20:04:57 -06:00
Alex Gleason
8f49b99935
Consolidate AdminStore and UserStore 2025-02-20 20:03:31 -06:00
Alex Gleason
8a978b088b
Use the user's store in a few places where it matters 2025-02-20 18:57:55 -06:00
Alex Gleason
33786d2e5d
Fix cashu tests, sorta 2025-02-20 18:48:44 -06:00
P. Reis
d87f650da3 fix: remove unused imports 2025-02-20 19:36:02 -03:00
P. Reis
a6fba9c8f1 fix: remove unused imports 2025-02-20 19:34:58 -03:00
P. Reis
d98a4bd263 refactor: use validateAndParseWallet function in GET /wallet 2025-02-20 19:33:25 -03:00
P. Reis
36640dd400 refactor: explain better the 'toBeRedeemed' field 2025-02-20 19:21:20 -03:00
P. Reis
71fd6ef965 refactor: extract repetitive validation and put it into a new function called 'validateAndParseWallet', tests included 2025-02-20 19:12:35 -03:00
Alex Gleason
e1bf86eb21
Make auth middleware work again (in a hacky way for now) 2025-02-20 14:45:44 -06:00
Alex Gleason
67aec57990
Rename @ditto/api to @ditto/mastoapi, start using the new router and middleware in app 2025-02-20 14:29:22 -06:00
Alex Gleason
22d7a5fa55
Merge remote-tracking branch 'origin/main' into router 2025-02-20 12:07:18 -06:00
Alex Gleason
64e71b0ba8 Merge branch 'rm-admin-signer' into 'main'
Remove AdminSigner, Conf.pubkey, Conf.nsec, add Conf.signer

See merge request soapbox-pub/ditto!682
2025-02-20 18:07:01 +00:00
Alex Gleason
0841563d69
Remove AdminSigner, Conf.pubkey, Conf.nsec, add Conf.signer 2025-02-20 12:04:52 -06:00
Alex Gleason
6bdb613522
Merge remote-tracking branch 'origin/main' into router 2025-02-20 11:37:33 -06:00
Alex Gleason
5ad38b4058 Merge branch 'dittodb' into 'main'
Swap the names of DittoDB and DittoDatabase, rename DittoDatabase to DittoPolyPg

See merge request soapbox-pub/ditto!681
2025-02-20 17:37:25 +00:00
Alex Gleason
44c4b3188c
DittoPolyPg: fix path to migrations 2025-02-20 11:35:37 -06:00
Alex Gleason
5231c8a94f
Rename DittoDatabase to DittoPolyPg 2025-02-20 11:32:04 -06:00
Alex Gleason
c7624e99d7
Swap the names of DittoDB and DittoDatabase 2025-02-20 11:26:27 -06:00
Alex Gleason
5c0a350776
Add @ditto/router package 2025-02-20 11:19:50 -06:00
Alex Gleason
f5947eda8b Merge branch 'deepl-fix' into 'main'
Fix DeepL Response parsing, mock DeepL tests so they can always run without API keys

See merge request soapbox-pub/ditto!680
2025-02-20 17:10:03 +00:00
Alex Gleason
91f9bd9442
Add mock LibreTranslate tests 2025-02-20 11:07:41 -06:00
Alex Gleason
1afb09e604
DittoTranslator: source_lang -> sourceLang 2025-02-20 10:39:31 -06:00
Alex Gleason
2150259aba
languageSchema does not lowercase the code 2025-02-20 10:36:43 -06:00
Alex Gleason
d791a9b350
Fix DeepL Response parsing, mock DeepL tests so they can always run without API keys 2025-02-20 10:35:27 -06:00
Alex Gleason
403d0ac5c2
Add logi log to translate controller 2025-02-20 10:03:01 -06:00
P. Reis
c530aa310d fix: get multiple proofs from nutzap event 2025-02-20 11:27:50 -03:00
Alex Gleason
2ce283e9a5
return new Response() -> return c.newResponse() 2025-02-20 00:57:47 -06:00
Alex Gleason
f72fcdbd65
Upgrade socket before closing with 1008 (ratelimit) 2025-02-20 00:23:06 -06:00
Alex Gleason
e2f23e51d2 Merge branch 'rm-pubsub' into 'main'
Remove pubsub storage

See merge request soapbox-pub/ditto!676
2025-02-20 05:13:11 +00:00
Alex Gleason
521b63185b
Catch webPush 2025-02-19 22:14:38 -06:00
Alex Gleason
351d03bde7
Remove accidentally added DittoAPIStore 2025-02-19 22:02:49 -06:00
Alex Gleason
841b83f573
Hopeless Cashu tests leak even more 2025-02-19 22:01:16 -06:00
Alex Gleason
6f7fc11635
Super duper extra close the database 2025-02-19 22:00:09 -06:00
Alex Gleason
c6605ece77
Fix not being able to log in for chrissakes 2025-02-19 21:50:19 -06:00
Alex Gleason
aefa6bed6e
Add an initial limit back to the relay 2025-02-19 20:51:07 -06:00
Alex Gleason
9401c0e013
DittoPgStore: call expandFilters in .req 2025-02-19 20:40:30 -06:00
Alex Gleason
d05dd16507
EOSE after empty initial filters 2025-02-19 20:29:24 -06:00
Alex Gleason
f0c7ec0a99
Prevent the streaming API from paginating the whole database 2025-02-19 20:25:12 -06:00
Alex Gleason
f87f19d06c
DittoPgStore: rework realtime streaming so it actually works 2025-02-19 20:17:53 -06:00
P. Reis
741df0bee6 reminder: fix not getting multiple proofs 2025-02-19 21:26:34 -03:00
Alex Gleason
bc0830785a
Remove old startNotify code 2025-02-19 16:19:16 -06:00
Alex Gleason
d9a466c0ee
Remove InternalRelay (pubsub) store 2025-02-19 16:13:22 -06:00
Alex Gleason
6568dca191
DittoPgStore: support timeout in req, add special treatment for ephemeral events, yield event loop when processing many subscriptions 2025-02-19 15:55:09 -06:00
Alex Gleason
31044691e1
Merge remote-tracking branch 'origin/main' into rm-pubsub 2025-02-19 12:03:12 -06:00
P. Reis
e9210118b0 fix: pass mintUrl rather than mint instance 2025-02-19 11:34:33 -03:00
P. Reis
04d710b1b8 fix: return mints from the wallet, not just the ones from kind 7375 2025-02-19 11:14:05 -03:00
Alex Gleason
f86d1f1cad Merge branch 'deno-2.2.0' into 'main'
Upgrade Deno to v2.2.0

See merge request soapbox-pub/ditto!679
2025-02-19 02:06:08 +00:00
Alex Gleason
7deec54a2e
Upgrade Deno to v2.2.0 2025-02-18 20:04:23 -06:00
P. Reis
d055c80abe fix: pipe json payload into desired schema 2025-02-18 22:29:39 -03:00
P. Reis
a2165f0918 refactor: implement getMintsToProofs function 2025-02-18 22:19:41 -03:00
P. Reis
8890311e2d refactor: create getLastRedeemedNutzap function 2025-02-18 21:03:12 -03:00
Alex Gleason
3ed75434fe Merge branch 'genevent' into 'main'
Switch to genEvent from Nostrify

See merge request soapbox-pub/ditto!678
2025-02-18 22:37:55 +00:00
Alex Gleason
c29fc57a8c
Switch to genEvent from Nostrify 2025-02-18 16:35:45 -06:00
Alex Gleason
32813485a5 Merge branch 'rm-search-store' into 'main'
Remove SearchStore

See merge request soapbox-pub/ditto!677
2025-02-18 21:09:56 +00:00
Alex Gleason
aabe6350a7
Remove SearchStore 2025-02-18 15:08:00 -06:00
P. Reis
839aaca179 refactor: use zod 2025-02-18 17:13:16 -03:00
Alex Gleason
6fb873e72f
Make DittoPgStore pubsub capable 2025-02-18 13:45:14 -06:00
Alex Gleason
37f418899b
EventsDB -> DittoPgStore 2025-02-18 13:32:32 -06:00
Alex Gleason
3e7f33a63d Merge branch 'ditto-ratelimiter' into 'main'
Make @ditto/ratelimiter its own package

See merge request soapbox-pub/ditto!675
2025-02-18 19:18:44 +00:00
Alex Gleason
ac3a9fdf5a
Make @ditto/ratelimiter its own package 2025-02-18 13:16:42 -06:00
Alex Gleason
fc2928e72e Merge branch 'ditto-policies' into 'main'
Make @ditto/policies its own package

See merge request soapbox-pub/ditto!674
2025-02-18 19:14:38 +00:00
Alex Gleason
6f9081bbaf
Make @ditto/policies its own package 2025-02-18 13:13:05 -06:00
Alex Gleason
c029520937 Merge branch 'ditto-translators' into 'main'
Make @ditto/translators its own package

See merge request soapbox-pub/ditto!673
2025-02-18 19:07:33 +00:00
Alex Gleason
025a86fda2
translators: add missing return types 2025-02-18 13:05:47 -06:00
Alex Gleason
990646da26
Make @ditto/translators its own package 2025-02-18 13:03:48 -06:00
Alex Gleason
e163fcfa80 Merge branch 'ditto-lang' into 'main'
Make @ditto/lang its own package

See merge request soapbox-pub/ditto!672
2025-02-18 18:50:51 +00:00
Alex Gleason
d901a722e5
Make @ditto/lang its own package 2025-02-18 12:49:19 -06:00
Alex Gleason
7ef4fcecd3 Merge branch 'ditto-uploaders' into 'main'
Move uploaders into @ditto/uploaders

See merge request soapbox-pub/ditto!671
2025-02-18 18:45:34 +00:00
Alex Gleason
b2cd5c541b
Move uploaders into @ditto/uploaders 2025-02-18 12:43:57 -06:00
P. Reis
4b49dd0ddf refactor: delete get quote id state 2025-02-17 21:05:25 -03:00
P. Reis
fb72038373 feat: implement POST '/api/v1/ditto/cashu/mint/:quote_id' 2025-02-17 15:16:16 -03:00
P. Reis
29b1a20193 Merge branch 'main' into mint-cashu 2025-02-17 10:58:08 -03:00
P. Reis
b9d813804e feat: implement GET '/api/v1/ditto/cashu/quote/:quote_id' (not tested) 2025-02-16 20:00:14 -03:00
Alex Gleason
9e9a784416 Merge branch 'fix-policy-path' into 'main'
Fix path to datadir and custom policies

See merge request soapbox-pub/ditto!669
2025-02-16 18:54:08 +00:00
Alex Gleason
af0e688ca3
Fix path to datadir and custom policies 2025-02-16 12:52:27 -06:00
Alex Gleason
561efeffde Merge branch 'ditto-metrics' into 'main'
Add @ditto/metrics package

See merge request soapbox-pub/ditto!668
2025-02-16 18:03:47 +00:00
Alex Gleason
0ace14ffbb
metrics: add types to all exports 2025-02-16 11:56:41 -06:00
Alex Gleason
e100f72a9b
Add deno task lint 2025-02-16 11:53:34 -06:00
Alex Gleason
785ba1e053
Remove base alias from packages (make scripts use relative paths for now) 2025-02-16 11:50:25 -06:00
Alex Gleason
fbb5c63c33
metrics: make 'prefix' a variable 2025-02-16 11:45:08 -06:00
Alex Gleason
e1c1967a66
db: remove some external deps 2025-02-16 11:42:48 -06:00
Alex Gleason
dbfd759fba
Merge remote-tracking branch 'origin/main' into ditto-metrics 2025-02-16 00:45:01 -06:00
Alex Gleason
84493ceacc Merge branch 'ditto-db' into 'main'
Add @ditto/db package

See merge request soapbox-pub/ditto!667
2025-02-16 06:44:48 +00:00
Alex Gleason
425e0bf3f0
ci: deno lint --allow-import 2025-02-16 00:43:10 -06:00
Alex Gleason
4e0bb16b85
Fix db tests 2025-02-16 00:39:00 -06:00
Alex Gleason
739153afc9
Add @ditto/metrics package 2025-02-16 00:30:00 -06:00
Alex Gleason
773b5da461
Add db tests 2025-02-16 00:21:41 -06:00
Alex Gleason
c59bb421c6
Add @ditto/db package 2025-02-16 00:14:41 -06:00
Alex Gleason
a9744ff4ad
Add streak.expires to the API 2025-02-15 23:23:04 -06:00
Alex Gleason
222bf84eab
More msg tweaks 2025-02-15 23:05:41 -06:00
Alex Gleason
7622230c19
logi: ditto.relay.message -> ditto.relay.msg 2025-02-15 22:51:49 -06:00
Alex Gleason
aefc8c7163
Log verb with ditto.relay.message 2025-02-15 22:47:11 -06:00
P. Reis
8cba937962 Merge branch 'main' into mint-cashu
Conflicts:
	packages/ditto/controllers/api/cashu.ts
2025-02-15 21:59:12 -03:00
Alex Gleason
7fb60f6d1b Merge branch 'rm-conf' into 'main'
Replace `import Conf` calls with Hono context in some places

See merge request soapbox-pub/ditto!665
2025-02-16 00:52:42 +00:00
Alex Gleason
3073777d9b
Fix cashu tests 2025-02-15 18:51:15 -06:00
P. Reis
3bafb439bb feat: create POST '/api/v1/ditto/cashu/quote' endpoint
also create GET /quote/:quote_id and POST /mint/:quote_id endpoints (they are not implemented)
2025-02-15 21:44:43 -03:00
Alex Gleason
d0d37f5948
Remove Conf from middleware 2025-02-15 18:43:59 -06:00
Alex Gleason
8d2c83bb09
Remove Conf from S3Uploader, uploaderMiddleware 2025-02-15 18:38:20 -06:00
Alex Gleason
478c77bb62
Eliminate Conf from most controllers 2025-02-15 18:34:43 -06:00
Alex Gleason
cfd4d6bda5 Merge branch 'ditto-conf' into 'main'
@ditto/config -> @ditto/conf, DittoConfig -> DittoConf

See merge request soapbox-pub/ditto!664
2025-02-16 00:00:02 +00:00
Alex Gleason
02a7305ee9
@ditto/config -> @ditto/conf, DittoConfig -> DittoConf 2025-02-15 17:58:24 -06:00
Alex Gleason
3d2816dd05 Merge branch 'conf-mw' into 'main'
Add @ditto/api package with conf middleware

See merge request soapbox-pub/ditto!663
2025-02-15 23:56:14 +00:00
Alex Gleason
665be0c1b2
Add @ditto/api package with conf middleware 2025-02-15 17:54:38 -06:00
Alex Gleason
28f6ec8fc5 Merge branch 'ditto-config' into 'main'
Add a @ditto/config package, refactor config

See merge request soapbox-pub/ditto!662
2025-02-15 23:04:53 +00:00
Alex Gleason
9bfc7e6fe3
DittoConfig: fix missing return type of .external() 2025-02-15 17:02:53 -06:00
Alex Gleason
a2f273287d
config: test defaults 2025-02-15 16:59:12 -06:00
Alex Gleason
1e5278dc8c
Add basic DittoConfig tests 2025-02-15 16:55:59 -06:00
Alex Gleason
13db5498a5
config: break mergeURLPath into a separate module 2025-02-15 16:51:21 -06:00
Alex Gleason
5f6cdaf7d5
config: refactor schemas into a separate file 2025-02-15 16:37:33 -06:00
Alex Gleason
1636601bfe
config: crypto.ts -> utils/crypto.ts 2025-02-15 16:32:10 -06:00
Alex Gleason
cbe156ae2b
Create @ditto/config module 2025-02-15 16:28:26 -06:00
Alex Gleason
482b0092da Merge branch 'monorepo' into 'main'
Make Ditto a monorepo

See merge request soapbox-pub/ditto!661
2025-02-15 20:47:36 +00:00
Alex Gleason
2ccd4bad96
Move static/ into packages/ditto 2025-02-15 14:41:57 -06:00
Alex Gleason
5ee682ef8f
Remove accidental file: log.json 2025-02-15 14:38:15 -06:00
Alex Gleason
3476f45b41
Add @ditto/ditto as a workspace package 2025-02-15 14:31:30 -06:00
Alex Gleason
aa8bb03e5a
mv src packages/ditto 2025-02-15 14:21:05 -06:00
Alex Gleason
5bf6865f21 Merge branch 'notify-replaceable' into 'main'
NOTIFY when replaceable events update

See merge request soapbox-pub/ditto!660
2025-02-15 19:16:27 +00:00
Alex Gleason
d8d8cc20c9
NOTIFY when replaceable events update 2025-02-15 13:09:29 -06:00
Alex Gleason
6eb579e137 Merge branch 'cashu-test-pipeline-fast' into 'main'
Cashu test pipeline fast

See merge request soapbox-pub/ditto!659
2025-02-14 18:14:14 +00:00
P. Reis
31eb74b6e4 refactor: use mockFetch 2025-02-14 14:47:20 -03:00
P. Reis
028d41d585 refactor: use fetch mock 2025-02-14 11:52:37 -03:00
P. Reis
cb475f1126 dependency: add jsr:@std/testing/mock 2025-02-14 11:51:36 -03:00
P. Reis
9e7576f248 deno.lock: types/node 2025-02-14 11:36:26 -03:00
P. Reis
461feff952 fix: make cashu tests faster by giving invalid URL 2025-02-14 11:35:12 -03:00
P. Reis
ad967bbb5d Revert "Delete cashu.test.ts"
This reverts commit 032b29cfba.
2025-02-14 10:46:59 -03:00
Alex Gleason
032b29cfba
Delete cashu.test.ts 2025-02-13 21:34:10 -06:00
Alex Gleason
8d19eb3ec6
Don't hydrate related events when posting a status 2025-02-13 21:33:34 -06:00
Alex Gleason
64c703cef4
Log ip in relay 2025-02-13 21:11:59 -06:00
Alex Gleason
24ee33489d Merge branch 'trigger-insert' into 'main'
Trigger NOTIFY only on insert (not update)

See merge request soapbox-pub/ditto!658
2025-02-14 02:48:49 +00:00
Alex Gleason
dd336232d5
Trigger NOTIFY only on insert (not update) 2025-02-13 20:45:02 -06:00
Alex Gleason
053c362d44 Merge branch 'deno-2.1.10' into 'main'
Upgrade Deno to v2.1.10

See merge request soapbox-pub/ditto!657
2025-02-14 00:21:06 +00:00
Alex Gleason
b07ba9423b
Remove png-to-ico (this augments @types/node to a wrong version) 2025-02-13 18:16:52 -06:00
Alex Gleason
30b4900992
Upgrade Deno to v2.1.10 2025-02-13 17:46:28 -06:00
Alex Gleason
dda1bebfbc Merge branch 'cleanup' into 'main'
Cleanup

See merge request soapbox-pub/ditto!656
2025-02-13 23:45:26 +00:00
Alex Gleason
a2de8cdf82
Remove unnecessary lint rules from deno.json, avoid explicit any 2025-02-13 17:41:30 -06:00
Alex Gleason
359558e2d6
Remove unused docs and ansible directories 2025-02-13 17:21:33 -06:00
Alex Gleason
efb91d9f3f Merge branch 'cashu' into 'main'
Swap the tokens into the wallet

See merge request soapbox-pub/ditto!636
2025-02-13 22:54:54 +00:00
P. Reis
a5d4906257 refactor: just ignore leaky tests 2025-02-13 19:51:13 -03:00
Alex Gleason
26346b83ac
Fix leaky tests, but nutzapMiddleware is still broken 2025-02-13 15:47:53 -06:00
P. Reis
6473ab0f98 Merge branch 'main' into cashu 2025-02-13 13:25:06 -03:00
P. Reis
3418871a70 feat: create GET '/api/v1/ditto/cashu/mints' endpoint 2025-02-13 13:23:47 -03:00
Alex Gleason
00146dc192 Merge branch 'search-pagination' into 'main'
Try using offset pagination for account search

See merge request soapbox-pub/ditto!655
2025-02-13 05:41:22 +00:00
Alex Gleason
3c0e6dac76
Try using offset pagination for account search 2025-02-12 23:35:43 -06:00
Alex Gleason
a85daf1e67
EventsDB: fix domain query test 2025-02-12 23:11:09 -06:00
Alex Gleason
aabb20efa3
Rework domain queries so allow querying all subdomains by base domain 2025-02-12 23:04:08 -06:00
Alex Gleason
d991464810
Fix domain feeds 2025-02-12 22:45:53 -06:00
P. Reis
795c83ee88 refactor: remove unused imports and get rid of useless await 2025-02-12 20:28:34 -03:00
P. Reis
7d2258ff50 refactor: delete old controllers code: swapNutzapsToWalletController, createNutzapInformationController and createCashuWalletController 2025-02-12 20:11:02 -03:00
Alex Gleason
9ca0b2d21c Merge branch 'search-perf' into 'main'
Improve performance of account search

See merge request soapbox-pub/ditto!653
2025-02-12 22:21:24 +00:00
Alex Gleason
db793a3c6c
Refresh materialized view in search test 2025-02-12 16:20:04 -06:00
Alex Gleason
1482ee148e
Add missing indexes, fix order of results 2025-02-12 16:13:44 -06:00
Alex Gleason
ab7a0e06c7
Add a top_authors materialized view 2025-02-12 15:21:09 -06:00
Alex Gleason
510ad647be
Fix type errors 2025-02-12 15:02:19 -06:00
Alex Gleason
5969d9b3fa
getPubkeysBySearch: reduce limit of second query if applicable 2025-02-12 14:47:20 -06:00
Alex Gleason
379953a8cb
Improve performance of account search 2025-02-12 14:40:00 -06:00
P. Reis
96a16a9fd0 feat: create GET '/api/v1/ditto/cashu/mints' endpoint 2025-02-12 16:33:56 -03:00
Alex Gleason
7d8e5e676c Merge branch 'legacy-media' into 'main'
Enable media:true for legacy (non-imeta) URL attachments

See merge request soapbox-pub/ditto!652
2025-02-12 19:23:15 +00:00
Alex Gleason
112081e4bb
Enable media:true for legacy (non-imeta) URL attachments 2025-02-12 13:21:04 -06:00
P. Reis
7095519198 chore: remove done comments 2025-02-12 13:46:37 -03:00
Alex Gleason
8d76ab2906 Merge branch 'rm-pubkey-domains' into 'main'
Remove pubkey_domains table

See merge request soapbox-pub/ditto!651
2025-02-12 03:51:52 +00:00
Alex Gleason
f6fe777e78
Remove pubkey_domains table 2025-02-11 21:49:58 -06:00
Alex Gleason
efbefd918a
Speed up db:populate-extensions task 2025-02-11 21:20:16 -06:00
P. Reis
03946fabc8 test: GET /wallet must be successful 2025-02-11 22:11:06 -03:00
P. Reis
5e86844c12 feat: craete GET '/api/v1/ditto/cashu/wallet' endpoint
refactor: remove old swap controller and create swapNutzapsMiddleware
2025-02-11 22:10:33 -03:00
Alex Gleason
173aea6458
Update deno.lock 2025-02-11 18:17:22 -06:00
Alex Gleason
266fa08004 Merge branch 'negative-search' into 'main'
Upgrade Nostrify to support negative search queries, remove getIdsBySearch function

See merge request soapbox-pub/ditto!650
2025-02-12 00:16:19 +00:00
Alex Gleason
c379c11b25
search: support pagination via Link header 2025-02-11 18:13:47 -06:00
Alex Gleason
eb94da6cca
Upgrade Nostrify to support negative search queries, remove getIdsBySearch function 2025-02-11 17:40:28 -06:00
Alex Gleason
f8777b9e09 Merge branch 'fix-language' into 'main'
Ensure `.language` property gets added to DittoEvent when it's queried

See merge request soapbox-pub/ditto!649
2025-02-11 19:23:51 +00:00
Alex Gleason
43d675b837
Ensure .language property gets added to DittoEvent when it's queried 2025-02-11 13:19:32 -06:00
P. Reis
76f91687bd test: split test into 2 test functions 2025-02-11 12:58:02 -03:00
P. Reis
edd9512b01 test: PUT '/api/v1/ditto/cashu/wallet' endpoint must NOT be successful 2025-02-11 11:45:14 -03:00
P. Reis
89840eb279 refactor: create walletSchema and use it where required 2025-02-11 11:29:58 -03:00
P. Reis
1ff6511b39 test: PUT '/api/v1/ditto/cashu/wallet' endpoint 2025-02-11 11:07:46 -03:00
P. Reis
b74a0ffac0 refactor: create NIP-60 wallet and NIP-61 nutzap information event in the same endpoint 2025-02-11 10:59:04 -03:00
Alex Gleason
207e04ef08
Prewarm card cache in pipeline 2025-02-10 22:19:35 -06:00
Alex Gleason
756a9d9607
favicon: image/x-icon is an acceptable mime type 2025-02-10 21:21:18 -06:00
Alex Gleason
e468072b55 Merge branch 'home-filters' into 'main'
homeTimelineController: support exclude_replies, only_media params

See merge request soapbox-pub/ditto!648
2025-02-10 23:10:22 +00:00
Alex Gleason
11adaef243
homeTimelineController: support exclude_replies, only_media params 2025-02-10 17:08:29 -06:00
Alex Gleason
9e7aeda364 Merge branch 'exclude-replies-index' into 'main'
accountStatusesController: handle exclude_replies with NIP-50 search ext

See merge request soapbox-pub/ditto!647
2025-02-10 23:01:39 +00:00
Alex Gleason
8a9928696b
accountStatusesController: handle exclude_replies with NIP-50 search ext 2025-02-10 16:56:59 -06:00
Alex Gleason
c9dc3b198c Merge branch 'nip11-cors' into 'main'
Set CORS header on NIP-11 response

See merge request soapbox-pub/ditto!646
2025-02-10 22:24:27 +00:00
Alex Gleason
8e3976ea78 Merge branch 'hydrate-batch' into 'main'
hydrate: batch queries together

See merge request soapbox-pub/ditto!645
2025-02-10 22:23:49 +00:00
Alex Gleason
56c782e6e5
Set CORS header on NIP-11 response 2025-02-10 16:22:56 -06:00
Alex Gleason
4e86b6bf3f
hydrate: quotes must be gathered in a separate step 2025-02-10 16:21:01 -06:00
Alex Gleason
8267c170f0
hydrate: batch queries together 2025-02-10 16:15:13 -06:00
P. Reis
5076cdcc33 Merge branch 'main' into cashu 2025-02-10 17:18:40 -03:00
Alex Gleason
425edf2174
Add controller test, refactor some middlewares 2025-02-10 12:41:41 -06:00
Alex Gleason
1368304d25
Add cashuApp (rough draft) 2025-02-10 12:04:46 -06:00
P. Reis
00d10c7f9b refactor: TODO comments 2025-02-10 13:24:19 -03:00
Alex Gleason
1c4f0a87d5 Merge branch 'index-u-tags' into 'main'
feat: index 'u' tags

See merge request soapbox-pub/ditto!644
2025-02-10 16:04:50 +00:00
P. Reis
5a19a35847 Merge branch 'main' into cashu
Conflicts:
	deno.json
2025-02-10 12:48:55 -03:00
P. Reis
f85e7f7c33 feat: index 'u' tags 2025-02-09 21:27:38 -03:00
Alex Gleason
424272c97b Merge branch 'nip05-db' into 'main'
Store NIP-05 in the database

See merge request soapbox-pub/ditto!641
2025-02-09 23:46:50 +00:00
Alex Gleason
16f3a13364
SimpleLRU: respect AbortSignal 2025-02-09 17:22:53 -06:00
Alex Gleason
a597eae674
Merge remote-tracking branch 'origin/main' into nip05-db 2025-02-09 15:11:20 -06:00
Alex Gleason
8db0238c9a Merge branch 'asyncutil' into 'main'
@lambdalisue/async -> @core/asyncutil

See merge request soapbox-pub/ditto!643
2025-02-09 21:07:40 +00:00
Alex Gleason
433c2a4347
@lambdalisue/async -> @core/asyncutil 2025-02-09 15:06:13 -06:00
Alex Gleason
56e478a65d Merge branch 'rm-fetch-worker' into 'main'
Remove fetchWorker

See merge request soapbox-pub/ditto!642
2025-02-09 21:04:24 +00:00
Alex Gleason
838f773b84
Remove fetchWorker 2025-02-09 15:02:15 -06:00
Alex Gleason
a98bfdd0c6
fetchWorker: try throwing a preemptive AbortError inside the worker itself 2025-02-09 14:52:43 -06:00
Alex Gleason
93874df063
fetchWorker: log responses 2025-02-09 14:50:28 -06:00
Alex Gleason
576a66460f
fetchWorker: preemptively throw if signal is aborted 2025-02-09 14:45:26 -06:00
Alex Gleason
ebbde66824
Add @core/asyncutil 2025-02-09 14:34:12 -06:00
Alex Gleason
41419e84dc
Refetch nip05 if last_verified is null 2025-02-09 14:20:32 -06:00
Alex Gleason
8386fe7609
try-catch favicon fetch 2025-02-09 13:32:47 -06:00
Alex Gleason
dd009de5be
Wrap nip05 updates in a try-catch 2025-02-09 13:31:17 -06:00
Alex Gleason
8c60a4842b
Fix NIP05 verification 2025-02-09 13:27:05 -06:00
Alex Gleason
48507b7505
faviconCache: check favicon.ico explicitly 2025-02-09 11:57:09 -06:00
P. Reis
cde091132e fix: remove comment 2025-02-09 11:54:12 -03:00
Alex Gleason
e7027af1ae
Fix hydrating mentions 2025-02-08 10:13:38 -06:00
Alex Gleason
b8c67a85d0
hydrate: move gatherMentions down 2025-02-08 09:58:02 -06:00
P. Reis
efceee505a fix: pass privkey to cashuWallet.receive 2025-02-07 23:49:57 -03:00
P. Reis
361ef9a600 fix: stop trying to decrypt kind 7376 content (lol), log errors 2025-02-07 23:32:21 -03:00
P. Reis
55cc109376 Merge branch 'main' into cashu 2025-02-07 22:53:34 -03:00
P. Reis
f9da100936 refactor(swapNutzapsToWalletController): change to POST method 2025-02-07 22:41:39 -03:00
Alex Gleason
5811a19151
Fix mentions in statuses? 2025-02-07 19:05:37 -06:00
Alex Gleason
7780507a15
Add semaphore to nip05 script 2025-02-07 18:17:50 -06:00
Alex Gleason
b902abc7cc
Use an even simpler query for db:populate:nip05 script 2025-02-07 18:13:39 -06:00
Alex Gleason
b0dc7faaff
Simplify db:populate:nip05 script 2025-02-07 17:49:39 -06:00
Alex Gleason
a4a26d7575
Make db:populate:nip05 script more efficient 2025-02-07 17:48:07 -06:00
Alex Gleason
5157a90b63
Add populate:nip05 script 2025-02-07 16:03:22 -06:00
Alex Gleason
93141c1db1
Hook everything up? (In a messy way) 2025-02-07 15:39:25 -06:00
P. Reis
a6c7bbd751 createNutzapInformationController: add TODO message 2025-02-07 18:11:39 -03:00
Alex Gleason
d9b0bc1437
Add nip05 and favicon results to the database, make renderAccount synchronous 2025-02-07 13:39:56 -06:00
Alex Gleason
af262b5d52
Whoops, fix streak days calculation 2025-02-07 12:06:34 -06:00
Alex Gleason
c476596d0a Merge branch 'streak-window' into 'main'
Make STREAK_WINDOW configurable

See merge request soapbox-pub/ditto!640
2025-02-07 17:56:36 +00:00
Alex Gleason
ea8ef09045
Change default streak window to 36 hours 2025-02-07 11:54:47 -06:00
Alex Gleason
46558a97e4
Make STREAK_WINDOW configurable 2025-02-07 11:50:37 -06:00
Alex Gleason
00e10eb19f
detectLanguage: strip numbers from text before matching language patterns 2025-02-06 18:42:29 -06:00
Alex Gleason
86ffa7f0cc
Don't display broken streak through the API 2025-02-06 16:33:14 -06:00
Alex Gleason
30559ba043
streak-recompute: only update changed rows 2025-02-06 16:04:25 -06:00
Alex Gleason
08a5e7f761 Merge branch 'streak2' into 'main'
Streak API: Version 2

See merge request soapbox-pub/ditto!639
2025-02-06 22:01:47 +00:00
Alex Gleason
b480947c4d
Add a script to recompute the streak of all authors 2025-02-06 15:56:49 -06:00
Alex Gleason
080c34d13f
Fix streak broken logic 2025-02-06 14:53:42 -06:00
Alex Gleason
abea4f17b3
Streak: report a 1 day streak after the first post 2025-02-06 14:44:01 -06:00
Alex Gleason
084df2b59d
Streaks API 2025-02-06 14:26:08 -06:00
Alex Gleason
c2aab97018
indexExtensions: ensure kind 6 has reply:false to test the performance difference between -reply:true 2025-02-06 11:24:49 -06:00
Alex Gleason
f412a0ae50
detectLanguage: test that a Japanese text with Han-only characters is ambiguous 2025-02-06 10:40:41 -06:00
P. Reis
f5ebb5d82a Merge branch 'main' into cashu 2025-02-06 13:19:35 -03:00
P. Reis
f7e49cd5ec checkpoint: implement nutzap redemption history (kind 7376) 2025-02-06 12:28:09 -03:00
Alex Gleason
c5680150e6
Copy the code into getIdsBySearch >:( 2025-02-05 22:56:02 -06:00
Alex Gleason
6cf5d42a5b
Upgrade Nostrify to enable negative search tokens 2025-02-05 22:53:26 -06:00
P. Reis
d61f0d1d4b checkpoint: swap tokens into user controlled wallet
TODO: create the 7376 history kind, reemded marker, etc
2025-02-05 23:34:56 -03:00
P. Reis
df1a3fe842 dependency: add cashu-ts 2025-02-05 19:32:48 -03:00
P. Reis
2012ef5192 Merge branch 'main' into cashu 2025-02-04 21:35:41 -03:00
P. Reis
870847127b checkpoint: implement swapNutzapsToWalletController 2025-02-04 21:33:02 -03:00
P. Reis
236a9284ca refactor(createNutzapInformationController): implement new NIP 60 cashu wallet 2025-02-04 15:12:56 -03:00
P. Reis
e9696b8a2a refactor(createCashuWalletController): implement new NIP 60 cashu wallet 2025-02-04 12:48:56 -03:00
P. Reis
daedf24ca8 fix: add missing endpoint createNutzapInformationController 2025-02-04 12:34:41 -03:00
464 changed files with 14173 additions and 7460 deletions

View file

@ -1,4 +1,4 @@
image: denoland/deno:2.1.1
image: denoland/deno:2.2.2
default:
interruptible: true
@ -8,11 +8,12 @@ stages:
test:
stage: test
timeout: 2 minutes
script:
- deno fmt --check
- deno lint
- deno task lint
- deno task check
- deno task test --coverage=cov_profile
- deno task test --ignore=packages/transcode --coverage=cov_profile
- deno coverage cov_profile
coverage: /All files[^\|]*\|[^\|]*\s+([\d\.]+)/
services:

45
.goosehints Normal file
View file

@ -0,0 +1,45 @@
# Ditto
This project is called Ditto, a self-hosted social media server written in TypeScript with Deno. It implements the [Nostr Protocol](https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/README.md), and parts of the [Mastodon API](https://docs.joinmastodon.org/methods/) and [Pleroma API](https://git.pleroma.social/pleroma/pleroma/-/raw/develop/docs/development/API/pleroma_api.md).
## Project Structure
Ditto is a monorepo with a `packages` directory. The main package is `packages/ditto`, and the main API definition is in `packages/ditto/app.ts`.
## Deno, npm, and jsr
Ditto uses Deno 2.x
Dependencies are managed in `deno.json`, which are added with the `deno add` command. This command also updates the `deno.lock` file. npm packages can be added by using `deno add` and prefixing the package name with an `npm:` protocol. For example, `deno add npm:kysely` would add the `kysely` package from npm.
[jsr](https://jsr.io/) is a modern alternative to npm. It's a completely different registry with different packages available. jsr packages can be added by using `deno add` and prefixing the package name with a `jsr:` protocol. For example, `deno add jsr:@std/assert` would add the `@std/assert` package from jsr.
## Nostr
Nostr is a decentralized social media protocol involving clients, relays, keys, and a unified Nostr event format.
Specifications on Nostr are called "NIPs". NIP stands for "Nostr Implementation Possibilities". NIPs are numbered like `NIP-XX` where `XX` are two capitalized hexadecimal digits, eg `NIP-01` and `NIP-C7`.
To learn about Nostr, use the fetch tool to read [NIP-01](https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/01.md).
To read a specific NIP, construct the NIP URL following this template: `https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/{nip}.md` (replace `{nip}` in the URL template with the relevant NIP name, eg `07` for NIP-07, or `C7` for NIP-C7). Then use the fetch tool to read the URL.
To read the definition of a specific kind, construct a URL following this template: `https://nostrbook.dev/kinds/{kind}.md` (replace `{kind}` in the template with the kind number, eg `https://nostrbook.dev/kinds/0.md` for kind 0).
To discover the full list of NIPs, use the fetch tool to read the [NIPs README](https://raw.githubusercontent.com/nostr-protocol/nips/refs/heads/master/README.md).
It's important that Ditto conforms to Nostr standards. Please read as much of the NIPs as you need to have a full understanding before adding or modifying Nostr events and filters. It is possible to add new ideas to Nostr that don't exist yet in the NIPs, but only after other options have been explored. Care must be taken when adding new Nostr ideas, to ensure they fit seamlessly within the existing Nostr ecosystem.
## How Ditto uses Nostr and Mastodon API
Ditto implements a full Nostr relay, available at `/relay` of the Ditto server.
Mastodon API functionality, available at `/api/*`, is built around the Nostr relay's storage implementation.
Ditto's goal is to enable Mastodon API clients to interact directly with Nostr. It achieves this by implementing most of Mastodon's API, and "pretending" to be a Mastodon server to client applications, while in actuality it uses Nostr as its decentralized protocol layer.
## Testing Changes
After making changes, please run `deno task check` to check for type errors. If there are any type errors, please try to fix them.
Afterwards, run `deno fmt` to format the code, and then you are done. Please do not try to run the server, or run any other tests.

View file

@ -1 +1 @@
deno 2.1.1
deno 2.2.2

2
.vscode/launch.json vendored
View file

@ -8,7 +8,7 @@
"request": "launch",
"name": "Launch Program",
"type": "node",
"program": "${workspaceFolder}/src/server.ts",
"program": "${workspaceFolder}/packages/ditto/server.ts",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "deno",
"runtimeArgs": [

View file

@ -2,5 +2,8 @@
"deno.enable": true,
"deno.lint": true,
"editor.defaultFormatter": "denoland.vscode-deno",
"path-intellisense.extensionOnImport": true
"path-intellisense.extensionOnImport": true,
"files.associations": {
".goosehints": "markdown"
}
}

View file

@ -1,10 +1,10 @@
FROM denoland/deno:2.1.1
FROM denoland/deno:2.2.2
ENV PORT 5000
WORKDIR /app
RUN mkdir -p data && chown -R deno data
COPY . .
RUN deno cache --allow-import src/server.ts
RUN deno cache --allow-import packages/ditto/server.ts
RUN apt-get update && apt-get install -y unzip curl
RUN deno task soapbox
CMD deno task start

View file

@ -1,30 +0,0 @@
---
- name: Update Ditto
hosts: all
become: true
tasks:
- name: Update Deno
shell:
cmd: curl -fsSL https://deno.land/x/install/install.sh | sh
environment:
DENO_INSTALL: /usr/local
become_user: root
- name: Update Soapbox
shell:
cmd: deno task soapbox
chdir: /opt/ditto
become_user: ditto
- name: Update ditto from the main branch
git:
repo: 'https://gitlab.com/soapbox-pub/ditto.git'
dest: '/opt/ditto'
version: main
become_user: ditto
- name: Restart ditto service
systemd:
name: ditto
state: restarted
become_user: root

View file

@ -1,17 +1,34 @@
{
"version": "1.1.0",
"workspace": [
"./packages/captcha",
"./packages/conf",
"./packages/db",
"./packages/ditto",
"./packages/lang",
"./packages/mastoapi",
"./packages/metrics",
"./packages/nip98",
"./packages/policies",
"./packages/ratelimiter",
"./packages/transcode",
"./packages/translators",
"./packages/uploaders",
"./packages/cashu"
],
"tasks": {
"start": "deno run -A --env-file --deny-read=.env src/server.ts",
"dev": "deno run -A --env-file --deny-read=.env --watch src/server.ts",
"start": "deno run -A --env-file --deny-read=.env packages/ditto/server.ts",
"dev": "deno run -A --env-file --deny-read=.env --watch packages/ditto/server.ts",
"hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts",
"db:export": "deno run -A --env-file --deny-read=.env scripts/db-export.ts",
"db:import": "deno run -A --env-file --deny-read=.env scripts/db-import.ts",
"db:cleanup": "deno run -A --env-file --deny-read=.env scripts/db-policy.ts",
"db:migrate": "deno run -A --env-file --deny-read=.env scripts/db-migrate.ts",
"nostr:pull": "deno run -A --env-file --deny-read=.env scripts/nostr-pull.ts",
"debug": "deno run -A --env-file --deny-read=.env --inspect src/server.ts",
"debug": "deno run -A --env-file --deny-read=.env --inspect packages/ditto/server.ts",
"test": "deno test -A --env-file=.env.test --deny-read=.env --junit-path=./deno-test.xml",
"check": "deno check --allow-import .",
"lint": "deno lint --allow-import",
"nsec": "deno run scripts/nsec.ts",
"admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts",
"admin:role": "deno run -A --env-file --deny-read=.env scripts/admin-role.ts",
@ -20,9 +37,11 @@
"stats:recompute": "deno run -A --env-file --deny-read=.env scripts/stats-recompute.ts",
"soapbox": "curl -O https://dl.soapbox.pub/main/soapbox.zip && mkdir -p public && mv soapbox.zip public/ && cd public/ && unzip -o soapbox.zip && rm soapbox.zip",
"trends": "deno run -A --env-file --deny-read=.env scripts/trends.ts",
"clean:deps": "deno cache --reload src/app.ts",
"clean:deps": "deno cache --reload packages/ditto/app.ts",
"db:populate:nip05": "deno run -A --env-file --deny-read=.env scripts/db-populate-nip05.ts",
"db:populate-search": "deno run -A --env-file --deny-read=.env scripts/db-populate-search.ts",
"db:populate-extensions": "deno run -A --env-file --deny-read=.env scripts/db-populate-extensions.ts",
"db:streak:recompute": "deno run -A --env-file --deny-read=.env scripts/db-streak-recompute.ts",
"vapid": "deno run scripts/vapid.ts"
},
"unstable": [
@ -35,19 +54,19 @@
"./public"
],
"imports": {
"@/": "./src/",
"@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47",
"@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
"@cashu/cashu-ts": "npm:@cashu/cashu-ts@^2.2.0",
"@core/asyncutil": "jsr:@core/asyncutil@^1.2.0",
"@electric-sql/pglite": "npm:@electric-sql/pglite@^0.2.8",
"@esroyo/scoped-performance": "jsr:@esroyo/scoped-performance@^3.1.0",
"@gfx/canvas-wasm": "jsr:@gfx/canvas-wasm@^0.4.2",
"@hono/hono": "jsr:@hono/hono@^4.4.6",
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
"@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1",
"@negrel/webpush": "jsr:@negrel/webpush@^0.3.0",
"@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0",
"@nostrify/db": "jsr:@nostrify/db@^0.37.3",
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.38.0",
"@nostrify/db": "jsr:@nostrify/db@^0.39.4",
"@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.39.1",
"@nostrify/policies": "jsr:@nostrify/policies@^0.36.1",
"@nostrify/types": "jsr:@nostrify/types@^0.36.0",
"@scure/base": "npm:@scure/base@^1.1.6",
@ -56,6 +75,7 @@
"@soapbox/logi": "jsr:@soapbox/logi@^0.3.0",
"@soapbox/safe-fetch": "jsr:@soapbox/safe-fetch@^2.0.0",
"@std/assert": "jsr:@std/assert@^0.225.1",
"@std/async": "jsr:@std/async@^1.0.10",
"@std/cli": "jsr:@std/cli@^0.223.0",
"@std/crypto": "jsr:@std/crypto@^0.224.0",
"@std/encoding": "jsr:@std/encoding@^0.224.0",
@ -63,11 +83,11 @@
"@std/json": "jsr:@std/json@^0.223.0",
"@std/media-types": "jsr:@std/media-types@^0.224.1",
"@std/streams": "jsr:@std/streams@^0.223.0",
"@std/testing": "jsr:@std/testing@^1.0.9",
"blurhash": "npm:blurhash@2.0.5",
"comlink": "npm:comlink@^4.4.1",
"comlink-async-generator": "npm:comlink-async-generator@^0.0.1",
"commander": "npm:commander@12.1.0",
"deno.json": "./deno.json",
"entities": "npm:entities@^4.5.0",
"fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0",
"formdata-helper": "npm:formdata-helper@^0.3.0",
@ -85,7 +105,6 @@
"nostr-tools": "npm:nostr-tools@2.5.1",
"nostr-wasm": "npm:nostr-wasm@^0.1.0",
"path-to-regexp": "npm:path-to-regexp@^7.1.0",
"png-to-ico": "npm:png-to-ico@^2.1.8",
"postgres": "https://gitlab.com/soapbox-pub/postgres.js/-/raw/e79d7d2039446fbf7a37d4eca0d17e94a94b8b53/deno/mod.js",
"prom-client": "npm:prom-client@^15.1.2",
"question-deno": "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/mod.ts",
@ -97,16 +116,6 @@
"zod": "npm:zod@^3.23.8",
"~/fixtures/": "./fixtures/"
},
"lint": {
"rules": {
"tags": [
"recommended"
],
"exclude": [
"no-explicit-any"
]
}
},
"fmt": {
"useTabs": false,
"lineWidth": 120,

272
deno.lock generated
View file

@ -3,6 +3,7 @@
"specifiers": {
"jsr:@b-fuze/deno-dom@~0.1.47": "0.1.48",
"jsr:@bradenmacdonald/s3-lite-client@~0.7.4": "0.7.6",
"jsr:@core/asyncutil@^1.2.0": "1.2.0",
"jsr:@denosaurs/plug@1.0.3": "1.0.3",
"jsr:@esroyo/scoped-performance@^3.1.0": "3.1.0",
"jsr:@gfx/canvas-wasm@~0.4.2": "0.4.2",
@ -27,24 +28,30 @@
"jsr:@gleasonator/policy@0.9.2": "0.9.2",
"jsr:@gleasonator/policy@0.9.3": "0.9.3",
"jsr:@gleasonator/policy@0.9.4": "0.9.4",
"jsr:@gleasonator/policy@0.9.5": "0.9.5",
"jsr:@gleasonator/policy@0.9.6": "0.9.6",
"jsr:@gleasonator/policy@0.9.7": "0.9.7",
"jsr:@gleasonator/policy@0.9.8": "0.9.8",
"jsr:@hono/hono@^4.4.6": "4.6.15",
"jsr:@lambdalisue/async@^2.1.1": "2.1.1",
"jsr:@negrel/http-ece@0.6.0": "0.6.0",
"jsr:@negrel/webpush@0.3": "0.3.0",
"jsr:@nostrify/db@~0.37.3": "0.37.3",
"jsr:@nostrify/db@~0.39.4": "0.39.4",
"jsr:@nostrify/nostrify@0.31": "0.31.0",
"jsr:@nostrify/nostrify@0.32": "0.32.0",
"jsr:@nostrify/nostrify@0.36": "0.36.2",
"jsr:@nostrify/nostrify@0.38": "0.38.0",
"jsr:@nostrify/nostrify@0.39": "0.39.1",
"jsr:@nostrify/nostrify@~0.22.1": "0.22.5",
"jsr:@nostrify/nostrify@~0.22.4": "0.22.4",
"jsr:@nostrify/nostrify@~0.22.5": "0.22.5",
"jsr:@nostrify/nostrify@~0.39.1": "0.39.1",
"jsr:@nostrify/nostrify@~0.46.3": "0.46.3",
"jsr:@nostrify/policies@0.33": "0.33.0",
"jsr:@nostrify/policies@0.33.1": "0.33.1",
"jsr:@nostrify/policies@0.34": "0.34.0",
"jsr:@nostrify/policies@0.36": "0.36.0",
"jsr:@nostrify/policies@~0.33.1": "0.33.1",
"jsr:@nostrify/policies@~0.36.1": "0.36.1",
"jsr:@nostrify/policies@~0.36.2": "0.36.2",
"jsr:@nostrify/types@0.30": "0.30.1",
"jsr:@nostrify/types@0.35": "0.35.0",
"jsr:@nostrify/types@0.36": "0.36.0",
@ -54,8 +61,10 @@
"jsr:@soapbox/safe-fetch@2": "2.0.0",
"jsr:@std/assert@0.223": "0.223.0",
"jsr:@std/assert@0.224": "0.224.0",
"jsr:@std/assert@^1.0.10": "1.0.11",
"jsr:@std/assert@~0.213.1": "0.213.1",
"jsr:@std/assert@~0.225.1": "0.225.3",
"jsr:@std/async@^1.0.10": "1.0.10",
"jsr:@std/bytes@0.223": "0.223.0",
"jsr:@std/bytes@0.224": "0.224.0",
"jsr:@std/bytes@0.224.0": "0.224.0",
@ -65,6 +74,7 @@
"jsr:@std/bytes@^1.0.2-rc.3": "1.0.2",
"jsr:@std/cli@0.223": "0.223.0",
"jsr:@std/crypto@0.224": "0.224.0",
"jsr:@std/data-structures@^1.0.6": "1.0.6",
"jsr:@std/encoding@0.213.1": "0.213.1",
"jsr:@std/encoding@0.224": "0.224.3",
"jsr:@std/encoding@0.224.0": "0.224.0",
@ -72,8 +82,10 @@
"jsr:@std/encoding@~0.224.1": "0.224.3",
"jsr:@std/fmt@0.213.1": "0.213.1",
"jsr:@std/fs@0.213.1": "0.213.1",
"jsr:@std/fs@^1.0.9": "1.0.11",
"jsr:@std/fs@~0.229.3": "0.229.3",
"jsr:@std/internal@1": "1.0.5",
"jsr:@std/internal@^1.0.5": "1.0.5",
"jsr:@std/io@0.223": "0.223.0",
"jsr:@std/io@0.224": "0.224.9",
"jsr:@std/json@0.223": "0.223.0",
@ -82,8 +94,11 @@
"jsr:@std/path@0.213.1": "0.213.1",
"jsr:@std/path@0.224.0": "0.224.0",
"jsr:@std/path@1.0.0-rc.1": "1.0.0-rc.1",
"jsr:@std/path@^1.0.8": "1.0.8",
"jsr:@std/path@~0.213.1": "0.213.1",
"jsr:@std/streams@0.223": "0.223.0",
"jsr:@std/testing@^1.0.9": "1.0.9",
"npm:@cashu/cashu-ts@^2.2.0": "2.2.0",
"npm:@electric-sql/pglite@~0.2.8": "0.2.8",
"npm:@isaacs/ttlcache@^1.4.1": "1.4.1",
"npm:@noble/hashes@^1.4.0": "1.4.0",
@ -91,7 +106,7 @@
"npm:@scure/base@^1.1.6": "1.1.6",
"npm:@scure/bip32@^1.4.0": "1.4.0",
"npm:@scure/bip39@^1.3.0": "1.3.0",
"npm:@types/node@*": "18.16.19",
"npm:@types/node@*": "22.5.4",
"npm:blurhash@2.0.5": "2.0.5",
"npm:comlink-async-generator@*": "0.0.1",
"npm:comlink-async-generator@^0.0.1": "0.0.1",
@ -117,11 +132,11 @@
"npm:lru-cache@^10.2.2": "10.2.2",
"npm:nostr-tools@2.5.1": "2.5.1",
"npm:nostr-tools@^2.10.4": "2.10.4",
"npm:nostr-tools@^2.13.0": "2.14.2",
"npm:nostr-tools@^2.5.0": "2.5.1",
"npm:nostr-tools@^2.7.0": "2.7.0",
"npm:nostr-wasm@0.1": "0.1.0",
"npm:path-to-regexp@^7.1.0": "7.1.0",
"npm:png-to-ico@^2.1.8": "2.1.8",
"npm:postgres@3.4.4": "3.4.4",
"npm:prom-client@^15.1.2": "15.1.2",
"npm:sharp@~0.33.5": "0.33.5",
@ -131,6 +146,7 @@
"npm:type-fest@^4.3.0": "4.18.2",
"npm:unfurl.js@^6.4.0": "6.4.0",
"npm:websocket-ts@^2.1.5": "2.1.5",
"npm:websocket-ts@^2.2.1": "2.2.1",
"npm:zod@^3.23.8": "3.23.8"
},
"jsr": {
@ -152,6 +168,9 @@
"jsr:@std/io@0.224"
]
},
"@core/asyncutil@1.2.0": {
"integrity": "9967f15190c60df032c13f72ce5ac73d185c34f31c53dc918d8800025854c118"
},
"@denosaurs/plug@1.0.3": {
"integrity": "b010544e386bea0ff3a1d05e0c88f704ea28cbd4d753439c2f1ee021a85d4640",
"dependencies": [
@ -306,6 +325,34 @@
"jsr:@nostrify/policies@~0.36.1"
]
},
"@gleasonator/policy@0.9.5": {
"integrity": "8ce76ad719b5d002bb1799c60f2deb4d450b32d590e0f4c211919aa68f1ea963",
"dependencies": [
"jsr:@nostrify/nostrify@0.36",
"jsr:@nostrify/policies@~0.36.1"
]
},
"@gleasonator/policy@0.9.6": {
"integrity": "5bbd04f2d986344509547d480b5202e5f42832a4216b5be66c161e638f5e6672",
"dependencies": [
"jsr:@nostrify/nostrify@0.36",
"jsr:@nostrify/policies@~0.36.1"
]
},
"@gleasonator/policy@0.9.7": {
"integrity": "e4f45032683e7433f9b8fb8a38e1ca767bbfb75513dd0600230f85d06d2956d6",
"dependencies": [
"jsr:@nostrify/nostrify@0.36",
"jsr:@nostrify/policies@~0.36.1"
]
},
"@gleasonator/policy@0.9.8": {
"integrity": "a972b1bc797f5a38f2e71458194c37af075c85e941c04048d208b858100efc52",
"dependencies": [
"jsr:@nostrify/nostrify@~0.46.3",
"jsr:@nostrify/policies@~0.36.2"
]
},
"@hono/hono@4.4.6": {
"integrity": "aa557ca9930787ee86b9ca1730691f1ce1c379174c2cb244d5934db2b6314453"
},
@ -336,9 +383,6 @@
"@hono/hono@4.6.15": {
"integrity": "935b3b12e98e4b22bcd1aa4dbe6587321e431c79829eba61f535b4ede39fd8b1"
},
"@lambdalisue/async@2.1.1": {
"integrity": "1fc9bc6f4ed50215cd2f7217842b18cea80f81c25744f88f8c5eb4be5a1c9ab4"
},
"@negrel/http-ece@0.6.0": {
"integrity": "7afdd81b86ea5b21a9677b323c01c3338705e11cc2bfed250870f5349d8f86f7",
"dependencies": [
@ -356,10 +400,10 @@
"jsr:@std/path@0.224.0"
]
},
"@nostrify/db@0.37.3": {
"integrity": "fe7cacd67bb817f10fb44587e832cfb042a3a0d32db29b24a487b7d006438623",
"@nostrify/db@0.39.4": {
"integrity": "53fecea3b67394cf4f52795f89d1d065bdeb0627b8655cc7fc3a89d6b21adf01",
"dependencies": [
"jsr:@nostrify/nostrify@0.38",
"jsr:@nostrify/nostrify@0.39",
"jsr:@nostrify/types@0.36",
"npm:kysely@~0.27.3",
"npm:nostr-tools@^2.10.4"
@ -376,7 +420,7 @@
"npm:kysely@~0.27.3",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.5.0",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
@ -390,7 +434,7 @@
"npm:kysely@~0.27.3",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.7.0",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
@ -405,7 +449,7 @@
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.7.0",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
@ -418,7 +462,7 @@
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.7.0",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
@ -431,7 +475,7 @@
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.7.0",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
@ -446,7 +490,7 @@
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.7.0",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
@ -459,7 +503,7 @@
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.7.0",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
@ -474,7 +518,51 @@
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.10.4",
"npm:websocket-ts",
"npm:websocket-ts@^2.1.5",
"npm:zod"
]
},
"@nostrify/nostrify@0.39.0": {
"integrity": "f7e052c32b8b9bafe0f2517dcf090e7d3df5aed38452a0cf61ade817d34067ee",
"dependencies": [
"jsr:@nostrify/nostrify@0.39",
"jsr:@nostrify/types@0.36",
"jsr:@std/crypto",
"jsr:@std/encoding@~0.224.1",
"npm:@scure/base",
"npm:@scure/bip32",
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.10.4",
"npm:websocket-ts@^2.2.1",
"npm:zod"
]
},
"@nostrify/nostrify@0.39.1": {
"integrity": "84f98c815a07f4151bd02188a3525e438c416e9de632c79c9da9edbfca580d7f",
"dependencies": [
"jsr:@nostrify/nostrify@~0.39.1",
"jsr:@nostrify/types@0.36",
"jsr:@std/crypto",
"jsr:@std/encoding@~0.224.1",
"npm:@scure/base",
"npm:@scure/bip32",
"npm:@scure/bip39",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.10.4",
"npm:websocket-ts@^2.2.1",
"npm:zod"
]
},
"@nostrify/nostrify@0.46.3": {
"integrity": "a809b83219c483dff4c87420f54bef7e0f98a438450283be26a167698114fec5",
"dependencies": [
"jsr:@nostrify/nostrify@~0.46.3",
"jsr:@nostrify/types@0.36",
"jsr:@std/encoding@~0.224.1",
"npm:lru-cache@^10.2.0",
"npm:nostr-tools@^2.13.0",
"npm:websocket-ts@^2.2.1",
"npm:zod"
]
},
@ -516,6 +604,14 @@
"npm:nostr-tools@^2.7.0"
]
},
"@nostrify/policies@0.36.2": {
"integrity": "b62c99fadf2d451e68d24ac1643844b953785c45cc170d3aee62b57c60ab9829",
"dependencies": [
"jsr:@nostrify/nostrify@~0.46.3",
"jsr:@nostrify/types@0.36",
"npm:nostr-tools@^2.13.0"
]
},
"@nostrify/types@0.30.0": {
"integrity": "1f38fa849cff930bd709edbf94ef9ac02f46afb8b851f86c8736517b354616da"
},
@ -555,9 +651,18 @@
"@std/assert@0.225.3": {
"integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f",
"dependencies": [
"jsr:@std/internal"
"jsr:@std/internal@1"
]
},
"@std/assert@1.0.11": {
"integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1",
"dependencies": [
"jsr:@std/internal@^1.0.5"
]
},
"@std/async@1.0.10": {
"integrity": "2ff1b1c7d33d1416159989b0f69e59ec7ee8cb58510df01e454def2108b3dbec"
},
"@std/bytes@0.223.0": {
"integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8"
},
@ -586,6 +691,9 @@
"jsr:@std/encoding@0.224"
]
},
"@std/data-structures@1.0.6": {
"integrity": "76a7fd8080c66604c0496220a791860492ab21a04a63a969c0b9a0609bbbb760"
},
"@std/dotenv@0.224.0": {
"integrity": "d9234cdf551507dcda60abb6c474289843741d8c07ee8ce540c60f5c1b220a1d"
},
@ -617,6 +725,12 @@
"jsr:@std/path@1.0.0-rc.1"
]
},
"@std/fs@1.0.11": {
"integrity": "ba674672693340c5ebdd018b4fe1af46cb08741f42b4c538154e97d217b55bdd",
"dependencies": [
"jsr:@std/path@^1.0.8"
]
},
"@std/internal@1.0.0": {
"integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a"
},
@ -714,6 +828,9 @@
"@std/path@1.0.0-rc.1": {
"integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6"
},
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
},
"@std/streams@0.223.0": {
"integrity": "d6b28e498ced3960b04dc5d251f2dcfc1df244b5ec5a48dc23a8f9b490be3b99",
"dependencies": [
@ -721,9 +838,38 @@
"jsr:@std/bytes@0.223",
"jsr:@std/io@0.223"
]
},
"@std/testing@1.0.9": {
"integrity": "9bdd4ac07cb13e7594ac30e90f6ceef7254ac83a9aeaa089be0008f33aab5cd4",
"dependencies": [
"jsr:@std/assert@^1.0.10",
"jsr:@std/data-structures",
"jsr:@std/fs@^1.0.9",
"jsr:@std/internal@^1.0.5",
"jsr:@std/path@^1.0.8"
]
}
},
"npm": {
"@cashu/cashu-ts@2.2.0": {
"integrity": "sha512-7b6pGyjjpm3uAJvmOL+ztpRxqp1qnmzGpydp+Pu30pOjxj93EhejPTJVrZMDJ0P35y6u5+5jIjHF4k0fpovvmg==",
"dependencies": [
"@cashu/crypto",
"@noble/curves@1.4.0",
"@noble/hashes@1.4.0",
"buffer"
]
},
"@cashu/crypto@0.3.4": {
"integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==",
"dependencies": [
"@noble/curves@1.8.1",
"@noble/hashes@1.7.1",
"@scure/bip32@1.6.2",
"@scure/bip39@1.5.4",
"buffer"
]
},
"@electric-sql/pglite@0.2.8": {
"integrity": "sha512-0wSmQu22euBRzR5ghqyIHnBH4MfwlkL5WstOrrA3KOsjEWEglvoL/gH92JajEUA6Ufei/+qbkB2hVloC/K/RxQ=="
},
@ -841,6 +987,12 @@
"@noble/hashes@1.4.0"
]
},
"@noble/curves@1.8.1": {
"integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
"dependencies": [
"@noble/hashes@1.7.1"
]
},
"@noble/hashes@1.3.1": {
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="
},
@ -850,6 +1002,9 @@
"@noble/hashes@1.4.0": {
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="
},
"@noble/hashes@1.7.1": {
"integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="
},
"@noble/secp256k1@2.1.0": {
"integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw=="
},
@ -862,6 +1017,9 @@
"@scure/base@1.1.6": {
"integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g=="
},
"@scure/base@1.2.4": {
"integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="
},
"@scure/bip32@1.3.1": {
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": [
@ -878,6 +1036,14 @@
"@scure/base@1.1.6"
]
},
"@scure/bip32@1.6.2": {
"integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==",
"dependencies": [
"@noble/curves@1.8.1",
"@noble/hashes@1.7.1",
"@scure/base@1.2.4"
]
},
"@scure/bip39@1.2.1": {
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": [
@ -892,17 +1058,24 @@
"@scure/base@1.1.6"
]
},
"@scure/bip39@1.5.4": {
"integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==",
"dependencies": [
"@noble/hashes@1.7.1",
"@scure/base@1.2.4"
]
},
"@types/dompurify@3.0.5": {
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"dependencies": [
"@types/trusted-types"
]
},
"@types/node@17.0.45": {
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
},
"@types/node@18.16.19": {
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA=="
"@types/node@22.5.4": {
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dependencies": [
"undici-types"
]
},
"@types/trusted-types@2.0.7": {
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
@ -928,6 +1101,9 @@
"asynckit@0.4.0": {
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"base64-js@1.5.1": {
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bintrees@1.0.2": {
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
},
@ -940,6 +1116,13 @@
"fill-range"
]
},
"buffer@6.0.3": {
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"dependencies": [
"base64-js",
"ieee754"
]
},
"chalk@5.3.0": {
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
},
@ -1178,6 +1361,9 @@
"safer-buffer"
]
},
"ieee754@1.2.1": {
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"image-size@1.1.1": {
"integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
"dependencies": [
@ -1382,6 +1568,18 @@
"nostr-wasm"
]
},
"nostr-tools@2.14.2": {
"integrity": "sha512-YOIOn5EdJ2Kq5sQW5Zh4wOcqzR6kUyrCDHG4+mVD2szzthsyOTpiWX0yrwaRZGlHJG6q83vkhg95qc2W201XTQ==",
"dependencies": [
"@noble/ciphers",
"@noble/curves@1.2.0",
"@noble/hashes@1.3.1",
"@scure/base@1.1.1",
"@scure/bip32@1.3.1",
"@scure/bip39@1.2.1",
"nostr-wasm"
]
},
"nostr-tools@2.5.1": {
"integrity": "sha512-bpkhGGAhdiCN0irfV+xoH3YP5CQeOXyXzUq7SYeM6D56xwTXZCPEmBlUGqFVfQidvRsoVeVxeAiOXW2c2HxoRQ==",
"dependencies": [
@ -1451,14 +1649,6 @@
"pidtree@0.6.0": {
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="
},
"png-to-ico@2.1.8": {
"integrity": "sha512-Nf+IIn/cZ/DIZVdGveJp86NG5uNib1ZXMiDd/8x32HCTeKSvgpyg6D/6tUBn1QO/zybzoMK0/mc3QRgAyXdv9w==",
"dependencies": [
"@types/node@17.0.45",
"minimist",
"pngjs"
]
},
"pngjs@6.0.0": {
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="
},
@ -1660,6 +1850,9 @@
"type-fest@4.18.2": {
"integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg=="
},
"undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"unfurl.js@6.4.0": {
"integrity": "sha512-DogJFWPkOWMcu2xPdpmbcsL+diOOJInD3/jXOv6saX1upnWmMK8ndAtDWUfJkuInqNI9yzADud4ID9T+9UeWCw==",
"dependencies": [
@ -1685,6 +1878,9 @@
"websocket-ts@2.1.5": {
"integrity": "sha512-rCNl9w6Hsir1azFm/pbjBEFzLD/gi7Th5ZgOxMifB6STUfTSovYAzryWw0TRvSZ1+Qu1Z5Plw4z42UfTNA9idA=="
},
"websocket-ts@2.2.1": {
"integrity": "sha512-YKPDfxlK5qOheLZ2bTIiktZO1bpfGdNCPJmTEaPW7G9UXI1GKjDdeacOrsULUS000OPNxDVOyAuKLuIWPqWM0Q=="
},
"whatwg-encoding@3.1.1": {
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"dependencies": [
@ -2351,19 +2547,20 @@
"dependencies": [
"jsr:@b-fuze/deno-dom@~0.1.47",
"jsr:@bradenmacdonald/s3-lite-client@~0.7.4",
"jsr:@core/asyncutil@^1.2.0",
"jsr:@esroyo/scoped-performance@^3.1.0",
"jsr:@gfx/canvas-wasm@~0.4.2",
"jsr:@hono/hono@^4.4.6",
"jsr:@lambdalisue/async@^2.1.1",
"jsr:@negrel/webpush@0.3",
"jsr:@nostrify/db@~0.37.3",
"jsr:@nostrify/nostrify@0.38",
"jsr:@nostrify/db@~0.39.4",
"jsr:@nostrify/nostrify@~0.39.1",
"jsr:@nostrify/policies@~0.36.1",
"jsr:@nostrify/types@0.36",
"jsr:@soapbox/kysely-pglite@1",
"jsr:@soapbox/logi@0.3",
"jsr:@soapbox/safe-fetch@2",
"jsr:@std/assert@~0.225.1",
"jsr:@std/async@^1.0.10",
"jsr:@std/cli@0.223",
"jsr:@std/crypto@0.224",
"jsr:@std/encoding@0.224",
@ -2371,6 +2568,8 @@
"jsr:@std/json@0.223",
"jsr:@std/media-types@~0.224.1",
"jsr:@std/streams@0.223",
"jsr:@std/testing@^1.0.9",
"npm:@cashu/cashu-ts@^2.2.0",
"npm:@electric-sql/pglite@~0.2.8",
"npm:@isaacs/ttlcache@^1.4.1",
"npm:@noble/secp256k1@2",
@ -2396,7 +2595,6 @@
"npm:nostr-tools@2.5.1",
"npm:nostr-wasm@0.1",
"npm:path-to-regexp@^7.1.0",
"npm:png-to-ico@^2.1.8",
"npm:prom-client@^15.1.2",
"npm:sharp@~0.33.5",
"npm:tldts@^6.0.14",

View file

@ -1,23 +0,0 @@
# Authentication in Ditto
One of the main benefits of Nostr is that users control their keys. Instead of a username and password, the user has a public key (`npub` or `pubkey`) and private key (`nsec`). The public key is a globally-unique identifier for the user, and the private key can be used to sign events, producing a signature that only the pubkey could have produced.
With keys, users have full control over their identity. They can move between servers freely, and post to multiple servers at once. But with such power comes great responsibilities. Users cannot lose control of their key, or they'll lose control over their account forever.
## Managing Keys
There are several ways to manage keys in Nostr, and they all come with trade-offs. It's new territory, and people are still coming up with new ideas.
The main concerns are how to **conveniently log in on multiple devices**, and **who/what to trust with your key.**
### Current Solutions
1. **Private key text.** Users copy their key between devices/apps, giving apps full control over their key. Users might email the key to themselves, or better yet use a password manager, or apps might even provide a QR code for other apps to scan. This method is convenient, but it's not secure. Keys can get compromised in transit, or by a malicious or vulnerable app.
2. **Browser extension.** For web clients, an extension can expose `getPublicKey` and `signEvent` functions to web-pages without exposing the private key directly. This option is secure, but it only works well for laptop/desktop devices. On mobile, only FireFox can do it, with no support from Safari or Chrome. It also offers no way to share a key across devices on its own.
3. **Remote signer**. Users can run a remote signer program and then connect apps to it. The signer should be running 24/7, so it's best suited for running on a server. This idea has evolved into the creation of "bunker" services. Bunkers allow users to have a traditional username and password and login from anywhere. This method solves a lot of problems, but it also creates some problems. Users have to create an account on a separate website before they can log into your website. This makes it an option for more advanced users. Also, it's concerning that the administrator of the bunker server has full control over your keys. None of this is a problem if you run your own remote signer, but it's not a mainstream option.
4. **Custodial**. Apps which make you log you in with a username/password, and then keep Nostr keys for each user in their database. You might not even be able to export your keys. This option may be easier for users at first, but it puts a whole lot of liability on the server, since leaks can cause permanent damage. It also gives up a lot of the benefits of Nostr.
Each of these ideas could be improved upon greatly with new experiments and technical progress. But to Ditto, user freedom matters the most, so we're focusing on non-custodial solution. Even though there are security risks to copying around keys, the onus is on the user. The user may fall victim to a targeted attack (or make a stupid mistake), whereas custodial servers have the ability to wipe out entire demographics of users at once. Therefore we believe that custodial solutions are actually _less_ secure than users copying around keys. Users must take precautions about which apps to trust with their private key until we improve upon the area to make it more secure (likely with better support of browser extensions, OS key management, and more).

View file

@ -1,27 +0,0 @@
# Debugging Ditto
Running the command `deno task debug` will start the Ditto server in debug mode, making it possible to inspect with Chromium-based browsers by visiting `chrome://inspect`.
From there, go to the "Performance" tab and click "Start profiling". Perform the actions you want to profile, then click "Stop profiling". You can then inspect the call stack and see where the time is being spent.
## Remote debugging
If the Ditto server is on a separate machine, you will first need to put it into debug mode. Edit its systemd file (usually located at `/etc/systemd/system/ditto.service`) and change `deno task start` to `deno task debug` in the `ExecStart` line. Then run `systemctl daemon-reload` and `systemctl restart ditto`.
To access the debugger remotely, you can use SSH port forwarding. Run this command on your local machine, replacing `<user>@<host>` with the SSH login for the remote machine:
```sh
ssh -L 9229:localhost:9229 <user>@<host>
```
Then, in Chromium, go to `chrome://inspect` and the Ditto server should be available.
## SQL performance
To track slow queries, first set `DEBUG=ditto:sql` in the environment so only SQL logs are shown.
Then, grep for any logs above 0.001s:
```sh
journalctl -fu ditto | grep -v '(0.00s)'
```

View file

@ -1,15 +0,0 @@
# Installing Ditto
First, install Deno:
```sh
curl -fsSL https://deno.land/x/install/install.sh | sudo DENO_INSTALL=/usr/local sh
```
Now, run Ditto:
```sh
deno run -A https://gitlab.com/soapbox-pub/ditto/-/raw/main/src/server.ts
```
That's it! Ditto is now running on your machine.

View file

@ -1,9 +0,0 @@
# Mastodon API
Ditto implements Mastodon's client-server API, a REST API used by Mastodon mobile apps and frontends to interact with Mastodon servers. While it was originally designed for Mastodon, it has been adopted by other ActivityPub servers such as Pleroma, Mitra, Friendica, and many others.
Note that Mastodon API is **not** ActivityPub. It is not the API used to federate between servers. Instead, it enables user interfaces, mobile apps, bots, and other clients to interact with Mastodon servers.
Mastodon is built in Ruby on Rails, and its API is inspired by Twitter's legacy REST API. Rails, being an MVC framework, has "models", which it maps directly to "Entities" in its API.
Endpoints return either a single Entity, or an array of Entities. Entities Entities are JSON objects with a specific structure, and are documented in the [Mastodon API documentation](https://docs.joinmastodon.org/api/).

226
log.json

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
import { assert } from '@std/assert';
import { getCaptchaImages } from './assets.ts';
Deno.test('getCaptchaImages', async () => {
// If this function runs at all, it most likely worked.
const { bgImages } = await getCaptchaImages();
assert(bgImages.length);
});

View file

@ -0,0 +1,36 @@
import { type Image, loadImage } from '@gfx/canvas-wasm';
export interface CaptchaImages {
bgImages: Image[];
puzzleMask: Image;
puzzleHole: Image;
}
export async function getCaptchaImages(): Promise<CaptchaImages> {
const bgImages = await getBackgroundImages();
const puzzleMask = await loadImage(
await Deno.readFile(new URL('./assets/puzzle/puzzle-mask.png', import.meta.url)),
);
const puzzleHole = await loadImage(
await Deno.readFile(new URL('./assets/puzzle/puzzle-hole.png', import.meta.url)),
);
return { bgImages, puzzleMask, puzzleHole };
}
async function getBackgroundImages(): Promise<Image[]> {
const path = new URL('./assets/bg/', import.meta.url);
const images: Image[] = [];
for await (const dirEntry of Deno.readDir(path)) {
if (dirEntry.isFile && dirEntry.name.endsWith('.jpg')) {
const file = await Deno.readFile(new URL(dirEntry.name, path));
const image = await loadImage(file);
images.push(image);
}
}
return images;
}

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

Before

Width:  |  Height:  |  Size: 997 B

After

Width:  |  Height:  |  Size: 997 B

View file

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

View file

@ -0,0 +1,22 @@
import { createCanvas } from '@gfx/canvas-wasm';
import { assertNotEquals } from '@std/assert';
import { encodeHex } from '@std/encoding/hex';
import { addNoise } from './canvas.ts';
// This is almost impossible to truly test,
// but we can at least check that the image on the canvas changes.
Deno.test('addNoise', async () => {
const canvas = createCanvas(100, 100);
const ctx = canvas.getContext('2d');
const dataBefore = ctx.getImageData(0, 0, canvas.width, canvas.height);
const hashBefore = await crypto.subtle.digest('SHA-256', dataBefore.data);
addNoise(ctx, canvas.width, canvas.height);
const dataAfter = ctx.getImageData(0, 0, canvas.width, canvas.height);
const hashAfter = await crypto.subtle.digest('SHA-256', dataAfter.data);
assertNotEquals(encodeHex(hashBefore), encodeHex(hashAfter));
});

View file

@ -0,0 +1,21 @@
import type { CanvasRenderingContext2D } from '@gfx/canvas-wasm';
/**
* Add a small amount of noise to the image.
* This protects against an attacker pregenerating every possible solution and then doing a reverse-lookup.
*/
export function addNoise(ctx: CanvasRenderingContext2D, width: number, height: number): void {
const imageData = ctx.getImageData(0, 0, width, height);
// Loop over every pixel.
for (let i = 0; i < imageData.data.length; i += 4) {
// Add/subtract a small amount from each color channel.
// We skip i+3 because that's the alpha channel, which we don't want to modify.
for (let j = 0; j < 3; j++) {
const alteration = Math.floor(Math.random() * 11) - 5; // Vary between -5 and +5
imageData.data[i + j] = Math.min(Math.max(imageData.data[i + j] + alteration, 0), 255);
}
}
ctx.putImageData(imageData, 0, 0);
}

View file

@ -0,0 +1,12 @@
import { getCaptchaImages } from './assets.ts';
import { generateCaptcha, verifyCaptchaSolution } from './captcha.ts';
Deno.test('generateCaptcha', async () => {
const images = await getCaptchaImages();
generateCaptcha(images, { w: 370, h: 400 }, { w: 65, h: 65 });
});
Deno.test('verifyCaptchaSolution', () => {
verifyCaptchaSolution({ w: 65, h: 65 }, { x: 0, y: 0 }, { x: 0, y: 0 });
verifyCaptchaSolution({ w: 65, h: 65 }, { x: 0, y: 0 }, { x: 10, y: 10 });
});

View file

@ -0,0 +1,60 @@
import { createCanvas, type EmulatedCanvas2D } from '@gfx/canvas-wasm';
import { addNoise } from './canvas.ts';
import { areIntersecting, type Dimensions, type Point } from './geometry.ts';
import type { CaptchaImages } from './assets.ts';
/** Generate a puzzle captcha, returning canvases for the board and piece. */
export function generateCaptcha(
{ bgImages, puzzleMask, puzzleHole }: CaptchaImages,
bgSize: Dimensions,
puzzleSize: Dimensions,
): {
bg: EmulatedCanvas2D;
puzzle: EmulatedCanvas2D;
solution: Point;
} {
const bg = createCanvas(bgSize.w, bgSize.h);
const puzzle = createCanvas(puzzleSize.w, puzzleSize.h);
const ctx = bg.getContext('2d');
const pctx = puzzle.getContext('2d');
const solution = generateSolution(bgSize, puzzleSize);
const bgImage = bgImages[Math.floor(Math.random() * bgImages.length)];
// Draw the background image.
ctx.drawImage(bgImage, 0, 0, bg.width, bg.height);
addNoise(ctx, bg.width, bg.height);
// Draw the puzzle piece.
pctx.drawImage(puzzleMask, 0, 0, puzzle.width, puzzle.height);
pctx.globalCompositeOperation = 'source-in';
pctx.drawImage(bg, solution.x, solution.y, puzzle.width, puzzle.height, 0, 0, puzzle.width, puzzle.height);
// Draw the hole.
ctx.globalCompositeOperation = 'source-atop';
ctx.drawImage(puzzleHole, solution.x, solution.y, puzzle.width, puzzle.height);
return {
bg,
puzzle,
solution,
};
}
export function verifyCaptchaSolution(puzzleSize: Dimensions, point: Point, solution: Point): boolean {
return areIntersecting(
{ ...point, ...puzzleSize },
{ ...solution, ...puzzleSize },
);
}
/** Random coordinates such that the piece fits within the canvas. */
function generateSolution(bgSize: Dimensions, puzzleSize: Dimensions): Point {
return {
x: Math.floor(Math.random() * (bgSize.w - puzzleSize.w)),
y: Math.floor(Math.random() * (bgSize.h - puzzleSize.h)),
};
}

View file

@ -0,0 +1,7 @@
{
"name": "@ditto/captcha",
"version": "0.1.0",
"exports": {
".": "./mod.ts"
}
}

View file

@ -0,0 +1,8 @@
import { assertEquals } from '@std/assert';
import { areIntersecting } from './geometry.ts';
Deno.test('areIntersecting', () => {
assertEquals(areIntersecting({ x: 0, y: 0, w: 10, h: 10 }, { x: 5, y: 5, w: 10, h: 10 }), true);
assertEquals(areIntersecting({ x: 0, y: 0, w: 10, h: 10 }, { x: 15, y: 15, w: 10, h: 10 }), false);
});

View file

@ -0,0 +1,27 @@
export interface Point {
x: number;
y: number;
}
export interface Dimensions {
w: number;
h: number;
}
type Rectangle = Point & Dimensions;
/** Check if the two rectangles intersect by at least `threshold` percent. */
export function areIntersecting(rect1: Rectangle, rect2: Rectangle, threshold = 0.5): boolean {
const r1cx = rect1.x + rect1.w / 2;
const r2cx = rect2.x + rect2.w / 2;
const r1cy = rect1.y + rect1.h / 2;
const r2cy = rect2.y + rect2.h / 2;
const dist = Math.sqrt((r2cx - r1cx) ** 2 + (r2cy - r1cy) ** 2);
const e1 = Math.sqrt(rect1.h ** 2 + rect1.w ** 2) / 2;
const e2 = Math.sqrt(rect2.h ** 2 + rect2.w ** 2) / 2;
return dist <= (e1 + e2) * threshold;
}

2
packages/captcha/mod.ts Normal file
View file

@ -0,0 +1,2 @@
export { getCaptchaImages } from './assets.ts';
export { generateCaptcha, verifyCaptchaSolution } from './captcha.ts';

View file

@ -0,0 +1,457 @@
import { type NostrFilter, NSecSigner } from '@nostrify/nostrify';
import { NPostgres } from '@nostrify/db';
import { genEvent } from '@nostrify/nostrify/test';
import { generateSecretKey, getPublicKey } from 'nostr-tools';
import { bytesToString, stringToBytes } from '@scure/base';
import { assertEquals } from '@std/assert';
import { DittoPolyPg, TestDB } from '@ditto/db';
import { DittoConf } from '@ditto/conf';
import { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet } from './cashu.ts';
Deno.test('validateAndParseWallet function returns valid data', async () => {
const conf = new DittoConf(Deno.env);
const orig = new DittoPolyPg(conf.databaseUrl);
await using db = new TestDB(orig);
await db.migrate();
await db.clear();
const store = new NPostgres(orig.kysely);
const sk = generateSecretKey();
const signer = new NSecSigner(sk);
const pubkey = await signer.getPublicKey();
const privkey = bytesToString('hex', sk);
const p2pk = getPublicKey(stringToBytes('hex', privkey));
// Wallet
const wallet = genEvent({
kind: 17375,
content: await signer.nip44.encrypt(
pubkey,
JSON.stringify([
['privkey', privkey],
['mint', 'https://mint.soul.com'],
]),
),
}, sk);
await store.event(wallet);
// Nutzap information
const nutzapInfo = genEvent({
kind: 10019,
tags: [
['pubkey', p2pk],
['mint', 'https://mint.soul.com'],
['relay', conf.relay],
],
}, sk);
await store.event(nutzapInfo);
const { data, error } = await validateAndParseWallet(store, signer, pubkey);
assertEquals(error, null);
assertEquals(data, {
wallet,
nutzapInfo,
privkey,
p2pk,
mints: ['https://mint.soul.com'],
relays: [conf.relay],
});
});
Deno.test('organizeProofs function is working', async () => {
const conf = new DittoConf(Deno.env);
const orig = new DittoPolyPg(conf.databaseUrl);
await using db = new TestDB(orig);
await db.migrate();
await db.clear();
const store = new NPostgres(orig.kysely);
const sk = generateSecretKey();
const signer = new NSecSigner(sk);
const pubkey = await signer.getPublicKey();
const event1 = genEvent({
kind: 7375,
content: await signer.nip44.encrypt(
pubkey,
JSON.stringify({
mint: 'https://mint.soul.com',
proofs: [
{
id: '005c2502034d4f12',
amount: 25,
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
},
{
id: '005c2502034d4f12',
amount: 25,
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
},
{
id: '005c2502034d4f12',
amount: 25,
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
},
{
id: '005c2502034d4f12',
amount: 25,
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
},
],
del: [],
}),
),
}, sk);
await store.event(event1);
const proof1 = {
'id': '004f7adf2a04356c',
'amount': 1,
'secret': '6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0',
'C': '03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3',
'dleq': {
'e': 'bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351',
's': 'a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67',
'r': 'b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df',
},
};
const token1 = JSON.stringify({
mint: 'https://mint-fashion.com',
proofs: [proof1],
del: [],
});
const event2 = genEvent({
kind: 7375,
content: await signer.nip44.encrypt(
pubkey,
token1,
),
}, sk);
await store.event(event2);
const proof2 = {
'id': '004f7adf2a04356c',
'amount': 123,
'secret': '6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0',
'C': '03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3',
'dleq': {
'e': 'bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351',
's': 'a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67',
'r': 'b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df',
},
};
const token2 = JSON.stringify({
mint: 'https://mint-fashion.com',
proofs: [proof2],
del: [],
});
const event3 = genEvent({
kind: 7375,
content: await signer.nip44.encrypt(
pubkey,
token2,
),
}, sk);
await store.event(event3);
const unspentProofs = await store.query([{ kinds: [7375], authors: [pubkey] }]);
const organizedProofs = await organizeProofs(unspentProofs, signer);
assertEquals(organizedProofs, {
'https://mint.soul.com': {
totalBalance: 100,
[event1.id]: { event: event1, balance: 100 },
},
'https://mint-fashion.com': {
totalBalance: 124,
[event2.id]: { event: event2, balance: 1 },
[event3.id]: { event: event3, balance: 123 },
},
});
});
Deno.test('getLastRedeemedNutzap function is working', async () => {
const conf = new DittoConf(Deno.env);
const orig = new DittoPolyPg(conf.databaseUrl);
await using db = new TestDB(orig);
await db.migrate();
await db.clear();
const store = new NPostgres(orig.kysely);
const sk = generateSecretKey();
const signer = new NSecSigner(sk);
const pubkey = await signer.getPublicKey();
const event1 = genEvent({
kind: 7376,
content: '<nip-44-encrypted>',
created_at: Math.floor(Date.now() / 1000), // now
tags: [
['e', '<event-id-of-created-token>', '', 'redeemed'],
],
}, sk);
await store.event(event1);
const event2 = genEvent({
kind: 7376,
content: '<nip-44-encrypted>',
created_at: Math.floor((Date.now() - 86400000) / 1000), // yesterday
tags: [
['e', '<event-id-of-created-token>', '', 'redeemed'],
],
}, sk);
await store.event(event2);
const event3 = genEvent({
kind: 7376,
content: '<nip-44-encrypted>',
created_at: Math.floor((Date.now() - 86400000) / 1000), // yesterday
tags: [
['e', '<event-id-of-created-token>', '', 'redeemed'],
],
}, sk);
await store.event(event3);
const event4 = genEvent({
kind: 7376,
content: '<nip-44-encrypted>',
created_at: Math.floor((Date.now() + 86400000) / 1000), // tomorrow
tags: [
['e', '<event-id-of-created-token>', '', 'redeemed'],
],
}, sk);
await store.event(event4);
const event = await getLastRedeemedNutzap(store, pubkey);
assertEquals(event, event4);
});
Deno.test('getMintsToProofs function is working', async () => {
const conf = new DittoConf(Deno.env);
const orig = new DittoPolyPg(conf.databaseUrl);
await using db = new TestDB(orig);
await db.migrate();
await db.clear();
const store = new NPostgres(orig.kysely);
const sk = generateSecretKey();
const signer = new NSecSigner(sk);
const pubkey = await signer.getPublicKey();
const redeemedNutzap = genEvent({
created_at: Math.floor(Date.now() / 1000), // now
kind: 9321,
content: 'Thanks buddy! Nice idea.',
tags: [
[
'proof',
JSON.stringify({
id: '005c2502034d4f12',
amount: 25,
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
}),
],
['u', 'https://mint.soul.com'],
['e', 'nutzapped-post'],
['p', '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4'],
],
}, sk);
await store.event(redeemedNutzap);
await new Promise((r) => setTimeout(r, 1000));
const history = genEvent({
created_at: Math.floor(Date.now() / 1000), // now
kind: 7376,
content: 'nip-44-encrypted',
tags: [
['e', redeemedNutzap.id, conf.relay, 'redeemed'],
['p', redeemedNutzap.pubkey],
],
}, sk);
await store.event(history);
const nutzap = genEvent({
created_at: Math.floor(Date.now() / 1000), // now
kind: 9321,
content: 'Thanks buddy! Nice idea.',
tags: [
[
'proof',
JSON.stringify({
id: '005c2502034d4f12',
amount: 50,
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
}),
],
['u', 'https://mint.soul.com'],
['e', 'nutzapped-post'],
['p', '47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4'],
],
}, sk);
await store.event(nutzap);
const nutzapsFilter: NostrFilter = {
kinds: [9321],
'#p': ['47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4'],
'#u': ['https://mint.soul.com'],
};
const lastRedeemedNutzap = await getLastRedeemedNutzap(store, pubkey);
if (lastRedeemedNutzap) {
nutzapsFilter.since = lastRedeemedNutzap.created_at;
}
const mintsToProofs = await getMintsToProofs(store, nutzapsFilter, conf.relay);
assertEquals(mintsToProofs, {
'https://mint.soul.com': {
proofs: [{
id: '005c2502034d4f12',
amount: 50,
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
}],
toBeRedeemed: [
['e', nutzap.id, conf.relay, 'redeemed'],
['p', nutzap.pubkey],
],
},
});
});
Deno.test('getWallet function is working', async () => {
const conf = new DittoConf(Deno.env);
const orig = new DittoPolyPg(conf.databaseUrl);
await using db = new TestDB(orig);
await db.migrate();
await db.clear();
const sk = generateSecretKey();
const signer = new NSecSigner(sk);
const pubkey = await signer.getPublicKey();
const privkey = bytesToString('hex', sk);
const p2pk = getPublicKey(stringToBytes('hex', privkey));
const relay = new NPostgres(orig.kysely);
const proofs = genEvent({
kind: 7375,
content: await signer.nip44.encrypt(
pubkey,
JSON.stringify({
mint: 'https://cuiaba.mint.com',
proofs: [
{
'id': '004f7adf2a04356c',
'amount': 2,
'secret': '700312ccba84cb15d6a008c1d01b0dbf00025d3f2cb01f030a756553aca52de3',
'C': '02f0ff21fdd19a547d66d9ca09df5573ad88d28e4951825130708ba53cbed19561',
'dleq': {
'e': '9c44a58cb429be619c474b97216009bd96ff1b7dd145b35828a14f180c03a86f',
's': 'a11b8f616dfee5157a2c7c36da0ee181fe71b28729bee56b789e472c027ceb3b',
'r': 'c51b9ade8cfd3939b78d509c9723f86b43b432680f55a6791e3e252b53d4b465',
},
},
{
'id': '004f7adf2a04356c',
'amount': 4,
'secret': '5936f22d486734c03bd50b89aaa34be8e99f20d199bcebc09da8716890e95fb3',
'C': '039b55f92c02243e31b04e964f2ad0bcd2ed3229e334f4c7a81037392b8411d6e7',
'dleq': {
'e': '7b7be700f2515f1978ca27bc1045d50b9d146bb30d1fe0c0f48827c086412b9e',
's': 'cf44b08c7e64fd2bd9199667327b10a29b7c699b10cb7437be518203b25fe3fa',
'r': 'ec0cf54ce2d17fae5db1c6e5e5fd5f34d7c7df18798b8d92bcb7cb005ec2f93b',
},
},
{
'id': '004f7adf2a04356c',
'amount': 16,
'secret': '89e2315c058f3a010972dc6d546b1a2e81142614d715c28d169c6afdba5326bd',
'C': '02bc1c3756e77563fe6c7769fc9d9bc578ea0b84bf4bf045cf31c7e2d3f3ad0818',
'dleq': {
'e': '8dfa000c9e2a43d35d2a0b1c7f36a96904aed35457ca308c6e7d10f334f84e72',
's': '9270a914b1a53e32682b1277f34c5cfa931a6fab701a5dbee5855b68ddf621ab',
'r': 'ae71e572839a3273b0141ea2f626915592b4b3f5f91b37bbeacce0d3396332c9',
},
},
{
'id': '004f7adf2a04356c',
'amount': 16,
'secret': '06f2209f313d92505ae5c72087263f711b7a97b1b29a71886870e672a1b180ac',
'C': '02fa2ad933b62449e2765255d39593c48293f10b287cf7036b23570c8f01c27fae',
'dleq': {
'e': 'e696d61f6259ae97f8fe13a5af55d47f526eea62a7998bf888626fd1ae35e720',
's': 'b9f1ef2a8aec0e73c1a4aaff67e28b3ca3bc4628a532113e0733643c697ed7ce',
'r': 'b66ed62852811d14e9bf822baebfda92ba47c5c4babc4f2499d9ce81fbbbd3f2',
},
},
],
del: [],
}),
),
created_at: Math.floor(Date.now() / 1000), // now
}, sk);
await relay.event(proofs);
await relay.event(genEvent({
kind: 10019,
tags: [
['pubkey', p2pk],
['mint', 'https://mint.soul.com'],
['mint', 'https://cuiaba.mint.com'],
['relay', conf.relay],
],
}, sk));
const wallet = genEvent({
kind: 17375,
content: await signer.nip44.encrypt(
pubkey,
JSON.stringify([
['privkey', privkey],
['mint', 'https://mint.soul.com'],
]),
),
}, sk);
await relay.event(wallet);
const { wallet: walletEntity } = await getWallet(relay, pubkey, signer);
assertEquals(walletEntity, {
balance: 38,
mints: ['https://mint.soul.com', 'https://cuiaba.mint.com'],
relays: [conf.relay],
pubkey_p2pk: p2pk,
});
});

302
packages/cashu/cashu.ts Normal file
View file

@ -0,0 +1,302 @@
import type { Proof } from '@cashu/cashu-ts';
import { type NostrEvent, type NostrFilter, type NostrSigner, NSchema as n, type NStore } from '@nostrify/nostrify';
import { getPublicKey } from 'nostr-tools';
import { stringToBytes } from '@scure/base';
import { logi } from '@soapbox/logi';
import type { SetRequired } from 'type-fest';
import { z } from 'zod';
import { proofSchema, tokenEventSchema, type Wallet } from './schemas.ts';
type Data = {
wallet: NostrEvent;
nutzapInfo: NostrEvent;
privkey: string;
p2pk: string;
mints: string[];
relays: string[];
};
type CustomError =
| { message: 'Wallet not found'; code: 'wallet-not-found' }
| { message: 'Could not decrypt wallet content'; code: 'fail-decrypt-wallet' }
| { message: 'Could not parse wallet content'; code: 'fail-parse-wallet' }
| { message: 'Wallet does not contain privkey or privkey is not a valid nostr id'; code: 'privkey-missing' }
| { message: 'Nutzap information event not found'; code: 'nutzap-info-not-found' }
| {
message:
"You do not have a 'pubkey' tag in your nutzap information event or the one you have does not match the one derivated from the wallet.";
code: 'pubkey-mismatch';
}
| { message: 'You do not have any mints in your nutzap information event.'; code: 'mints-missing' };
/** Ensures that the wallet event and nutzap information event are correct. */
async function validateAndParseWallet(
store: NStore,
signer: SetRequired<NostrSigner, 'nip44'>,
pubkey: string,
opts?: { signal?: AbortSignal },
): Promise<{ data: Data; error: null } | { data: null; error: CustomError }> {
const [wallet] = await store.query([{ authors: [pubkey], kinds: [17375] }], { signal: opts?.signal });
if (!wallet) {
return { error: { message: 'Wallet not found', code: 'wallet-not-found' }, data: null };
}
let decryptedContent: string;
try {
decryptedContent = await signer.nip44.decrypt(pubkey, wallet.content);
} catch (e) {
logi({
level: 'error',
ns: 'ditto.api.cashu.wallet',
id: wallet.id,
kind: wallet.kind,
error: errorJson(e),
});
return { data: null, error: { message: 'Could not decrypt wallet content', code: 'fail-decrypt-wallet' } };
}
let contentTags: string[][];
try {
contentTags = n.json().pipe(z.string().array().array()).parse(decryptedContent);
} catch {
return { data: null, error: { message: 'Could not parse wallet content', code: 'fail-parse-wallet' } };
}
const privkey = contentTags.find(([value]) => value === 'privkey')?.[1];
if (!privkey || !isNostrId(privkey)) {
return {
data: null,
error: { message: 'Wallet does not contain privkey or privkey is not a valid nostr id', code: 'privkey-missing' },
};
}
const p2pk = getPublicKey(stringToBytes('hex', privkey));
const [nutzapInfo] = await store.query([{ authors: [pubkey], kinds: [10019] }], { signal: opts?.signal });
if (!nutzapInfo) {
return { data: null, error: { message: 'Nutzap information event not found', code: 'nutzap-info-not-found' } };
}
const nutzapInformationPubkey = nutzapInfo.tags.find(([name]) => name === 'pubkey')?.[1];
if (!nutzapInformationPubkey || (nutzapInformationPubkey !== p2pk)) {
return {
data: null,
error: {
message:
"You do not have a 'pubkey' tag in your nutzap information event or the one you have does not match the one derivated from the wallet.",
code: 'pubkey-mismatch',
},
};
}
const mints = [...new Set(nutzapInfo.tags.filter(([name]) => name === 'mint').map(([_, value]) => value))];
if (mints.length < 1) {
return {
data: null,
error: { message: 'You do not have any mints in your nutzap information event.', code: 'mints-missing' },
};
}
const relays = [...new Set(nutzapInfo.tags.filter(([name]) => name === 'relay').map(([_, value]) => value))];
return { data: { wallet, nutzapInfo, privkey, p2pk, mints, relays }, error: null };
}
type OrganizedProofs = {
[mintUrl: string]: {
/** Total balance in this mint */
totalBalance: number;
/** Event id */
[eventId: string]: {
event: NostrEvent;
/** Total balance in this event */
balance: number;
} | number;
};
};
async function organizeProofs(
events: NostrEvent[],
signer: SetRequired<NostrSigner, 'nip44'>,
): Promise<OrganizedProofs> {
const organizedProofs: OrganizedProofs = {};
const pubkey = await signer.getPublicKey();
for (const event of events) {
const decryptedContent = await signer.nip44.decrypt(pubkey, event.content);
const { data: token, success } = n.json().pipe(tokenEventSchema).safeParse(decryptedContent);
if (!success) {
continue;
}
const { mint, proofs } = token;
const balance = proofs.reduce((prev, current) => prev + current.amount, 0);
if (!organizedProofs[mint]) {
organizedProofs[mint] = { totalBalance: 0 };
}
organizedProofs[mint] = { ...organizedProofs[mint], [event.id]: { event, balance } };
organizedProofs[mint].totalBalance += balance;
}
return organizedProofs;
}
/** Returns a spending history event that contains the last redeemed nutzap. */
async function getLastRedeemedNutzap(
store: NStore,
pubkey: string,
opts?: { signal?: AbortSignal },
): Promise<NostrEvent | undefined> {
const events = await store.query([{ kinds: [7376], authors: [pubkey] }], { signal: opts?.signal });
for (const event of events) {
const nutzap = event.tags.find(([name]) => name === 'e');
const redeemed = nutzap?.[3];
if (redeemed === 'redeemed') {
return event;
}
}
}
/**
* toBeRedeemed are the nutzaps that will be redeemed into a kind 7375 and saved in the kind 7376 tags
* The tags format is: [
* [ "e", "<9321-event-id>", "<relay-hint>", "redeemed" ], // nutzap event that has been redeemed
* [ "p", "<sender-pubkey>" ] // pubkey of the author of the 9321 event (nutzap sender)
* ]
* https://github.com/nostr-protocol/nips/blob/master/61.md#updating-nutzap-redemption-history
*/
type MintsToProofs = { [key: string]: { proofs: Proof[]; toBeRedeemed: string[][] } };
/**
* Gets proofs from nutzaps that have not been redeemed yet.
* Each proof is associated with a specific mint.
* @param store Store used to query for the nutzaps
* @param nutzapsFilter Filter used to query for the nutzaps, most useful when
* it contains a 'since' field so it saves time and resources
* @param relay Relay hint where the new kind 7376 will be saved
* @returns MintsToProofs An object where each key is a mint url and the values are an array of proofs
* and an array of redeemed tags in this format:
* ```
* [
* ...,
* [ "e", "<9321-event-id>", "<relay-hint>", "redeemed" ], // nutzap event that has been redeemed
* [ "p", "<sender-pubkey>" ] // pubkey of the author of the 9321 event (nutzap sender)
* ]
* ```
*/
async function getMintsToProofs(
store: NStore,
nutzapsFilter: NostrFilter,
relay: string,
opts?: { signal?: AbortSignal },
): Promise<MintsToProofs> {
const mintsToProofs: MintsToProofs = {};
const nutzaps = await store.query([nutzapsFilter], { signal: opts?.signal });
for (const event of nutzaps) {
try {
const mint = event.tags.find(([name]) => name === 'u')?.[1];
if (!mint) {
continue;
}
const proofs = event.tags.filter(([name]) => name === 'proof').map((tag) => tag[1]).filter(Boolean);
if (proofs.length < 1) {
continue;
}
if (!mintsToProofs[mint]) {
mintsToProofs[mint] = { proofs: [], toBeRedeemed: [] };
}
const parsed = n.json().pipe(
proofSchema,
).array().safeParse(proofs);
if (!parsed.success) {
continue;
}
mintsToProofs[mint].proofs = [...mintsToProofs[mint].proofs, ...parsed.data];
mintsToProofs[mint].toBeRedeemed = [
...mintsToProofs[mint].toBeRedeemed,
[
'e', // nutzap event that has been redeemed
event.id,
relay,
'redeemed',
],
['p', event.pubkey], // pubkey of the author of the 9321 event (nutzap sender)
];
} catch (e) {
logi({ level: 'error', ns: 'ditto.api.cashu.wallet.swap', error: errorJson(e) });
}
}
return mintsToProofs;
}
/** Returns a wallet entity with the latest balance. */
async function getWallet(
store: NStore,
pubkey: string,
signer: SetRequired<NostrSigner, 'nip44'>,
opts?: { signal?: AbortSignal },
): Promise<{ wallet: Wallet; error: null } | { wallet: null; error: CustomError }> {
const { data, error } = await validateAndParseWallet(store, signer, pubkey, { signal: opts?.signal });
if (error) {
logi({ level: 'error', ns: 'ditto.cashu.get_wallet', error: errorJson(error) });
return { wallet: null, error };
}
const { p2pk, mints, relays } = data;
let balance = 0;
const tokens = await store.query([{ authors: [pubkey], kinds: [7375] }], { signal: opts?.signal });
for (const token of tokens) {
try {
const decryptedContent: { mint: string; proofs: Proof[] } = JSON.parse(
await signer.nip44.decrypt(pubkey, token.content),
);
if (!mints.includes(decryptedContent.mint)) {
mints.push(decryptedContent.mint);
}
balance += decryptedContent.proofs.reduce((accumulator, current) => {
return accumulator + current.amount;
}, 0);
} catch (e) {
logi({ level: 'error', ns: 'dtto.cashu.get_wallet', error: errorJson(e) });
}
}
// TODO: maybe change the 'Wallet' type data structure so each mint is a key and the value are the tokens associated with a given mint
const walletEntity: Wallet = {
pubkey_p2pk: p2pk,
mints,
relays,
balance,
};
return { wallet: walletEntity, error: null };
}
/** Serialize an error into JSON for JSON logging. */
export function errorJson(error: unknown): Error | null {
if (error instanceof Error) {
return error;
} else {
return null;
}
}
function isNostrId(value: unknown): boolean {
return n.id().safeParse(value).success;
}
export { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet };

7
packages/cashu/deno.json Normal file
View file

@ -0,0 +1,7 @@
{
"name": "@ditto/cashu",
"version": "0.1.0",
"exports": {
".": "./mod.ts"
}
}

3
packages/cashu/mod.ts Normal file
View file

@ -0,0 +1,3 @@
export { getLastRedeemedNutzap, getMintsToProofs, getWallet, organizeProofs, validateAndParseWallet } from './cashu.ts';
export { proofSchema, tokenEventSchema, type Wallet, walletSchema } from './schemas.ts';
export { renderTransaction, type Transaction } from './views.ts';

View file

@ -0,0 +1,39 @@
import { NSchema as n } from '@nostrify/nostrify';
import { assertEquals } from '@std/assert';
import { proofSchema } from './schemas.ts';
import { tokenEventSchema } from './schemas.ts';
Deno.test('Parse proof', () => {
const proof =
'{"id":"004f7adf2a04356c","amount":1,"secret":"6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0","C":"03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3","dleq":{"e":"bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351","s":"a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67","r":"b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df"}}';
assertEquals(n.json().pipe(proofSchema).safeParse(proof).success, true);
assertEquals(n.json().pipe(proofSchema).safeParse(JSON.parse(proof)).success, false);
assertEquals(proofSchema.safeParse(JSON.parse(proof)).success, true);
assertEquals(proofSchema.safeParse(proof).success, false);
});
Deno.test('Parse token', () => {
const proof = {
'id': '004f7adf2a04356c',
'amount': 1,
'secret': '6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0',
'C': '03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3',
'dleq': {
'e': 'bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351',
's': 'a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67',
'r': 'b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df',
},
};
const token = JSON.stringify({
mint: 'https://mint-fashion.com',
proofs: [proof],
del: [],
});
assertEquals(n.json().pipe(tokenEventSchema).safeParse(token).success, true);
assertEquals(n.json().pipe(tokenEventSchema).safeParse(JSON.parse(token)).success, false);
assertEquals(tokenEventSchema.safeParse(JSON.parse(token)).success, true);
assertEquals(tokenEventSchema.safeParse(tokenEventSchema).success, false);
});

50
packages/cashu/schemas.ts Normal file
View file

@ -0,0 +1,50 @@
import { NSchema as n } from '@nostrify/nostrify';
import { z } from 'zod';
export const proofSchema: z.ZodType<{
id: string;
amount: number;
secret: string;
C: string;
dleq?: { s: string; e: string; r?: string };
dleqValid?: boolean;
}> = z.object({
id: z.string(),
amount: z.number(),
secret: z.string(),
C: z.string(),
dleq: z.object({ s: z.string(), e: z.string(), r: z.string().optional() })
.optional(),
dleqValid: z.boolean().optional(),
});
/** Decrypted content of a kind 7375 */
export const tokenEventSchema: z.ZodType<{
mint: string;
proofs: Array<z.infer<typeof proofSchema>>;
del?: string[];
}> = z.object({
mint: z.string().url(),
proofs: proofSchema.array(),
del: z.string().array().optional(),
});
/** Ditto Cashu wallet */
export const walletSchema: z.ZodType<{
pubkey_p2pk: string;
mints: string[];
relays: string[];
balance: number;
}> = z.object({
pubkey_p2pk: n.id(),
mints: z.array(z.string().url()).nonempty().transform((val) => {
return [...new Set(val)];
}),
relays: z.array(z.string()).nonempty().transform((val) => {
return [...new Set(val)];
}),
/** Unit in sats */
balance: z.number(),
});
export type Wallet = z.infer<typeof walletSchema>;

View file

@ -0,0 +1,85 @@
import { NSecSigner } from '@nostrify/nostrify';
import { NPostgres } from '@nostrify/db';
import { genEvent } from '@nostrify/nostrify/test';
import { generateSecretKey } from 'nostr-tools';
import { assertEquals } from '@std/assert';
import { DittoPolyPg, TestDB } from '@ditto/db';
import { DittoConf } from '@ditto/conf';
import { renderTransaction } from './views.ts';
Deno.test('renderTransaction function is working', async () => {
const conf = new DittoConf(Deno.env);
const orig = new DittoPolyPg(conf.databaseUrl);
await using db = new TestDB(orig);
await db.migrate();
await db.clear();
const sk = generateSecretKey();
const signer = new NSecSigner(sk);
const pubkey = await signer.getPublicKey();
const relay = new NPostgres(orig.kysely);
const history1 = genEvent({
kind: 7376,
content: await signer.nip44.encrypt(
pubkey,
JSON.stringify([
['direction', 'in'],
['amount', '33'],
]),
),
created_at: Math.floor(Date.now() / 1000), // now
}, sk);
await relay.event(history1);
const history2 = genEvent({
kind: 7376,
content: await signer.nip44.encrypt(
pubkey,
JSON.stringify([
['direction', 'out'],
['amount', '29'],
]),
),
created_at: Math.floor(Date.now() / 1000) - 1, // now - 1 second
}, sk);
await relay.event(history2);
const history3 = genEvent({
kind: 7376,
content: await signer.nip44.encrypt(
pubkey,
JSON.stringify([
['direction', 'ouch'],
['amount', 'yolo'],
]),
),
created_at: Math.floor(Date.now() / 1000) - 2, // now - 2 second
}, sk);
await relay.event(history3);
const events = await relay.query([{ kinds: [7376], authors: [pubkey], since: history2.created_at }]);
const transactions = await Promise.all(
events.map((event) => {
return renderTransaction(event, pubkey, signer);
}),
);
assertEquals(transactions, [
{
direction: 'in',
amount: 33,
created_at: history1.created_at,
},
{
direction: 'out',
amount: 29,
created_at: history2.created_at,
},
]);
});

44
packages/cashu/views.ts Normal file
View file

@ -0,0 +1,44 @@
import { type NostrEvent, type NostrSigner, NSchema as n } from '@nostrify/nostrify';
import type { SetRequired } from 'type-fest';
import { z } from 'zod';
type Transaction = {
amount: number;
created_at: number;
direction: 'in' | 'out';
};
/** Renders one history of transaction. */
async function renderTransaction(
event: NostrEvent,
viewerPubkey: string,
signer: SetRequired<NostrSigner, 'nip44'>,
): Promise<Transaction | undefined> {
if (event.kind !== 7376) return;
const { data: contentTags, success } = n.json().pipe(z.coerce.string().array().min(2).array()).safeParse(
await signer.nip44.decrypt(viewerPubkey, event.content),
);
if (!success) {
return;
}
const direction = contentTags.find(([name]) => name === 'direction')?.[1];
if (direction !== 'out' && direction !== 'in') {
return;
}
const amount = parseInt(contentTags.find(([name]) => name === 'amount')?.[1] ?? '', 10);
if (isNaN(amount)) {
return;
}
return {
created_at: event.created_at,
direction,
amount,
};
}
export { renderTransaction, type Transaction };

View file

@ -0,0 +1,54 @@
import { assertEquals, assertThrows } from '@std/assert';
import { DittoConf } from './DittoConf.ts';
Deno.test('DittoConfig', async (t) => {
const env = new Map<string, string>([
['DITTO_NSEC', 'nsec19shyxpuzd0cq2p5078fwnws7tyykypud6z205fzhlmlrs2vpz6hs83zwkw'],
]);
const config = new DittoConf(env);
await t.step('signer', async () => {
assertEquals(
await config.signer.getPublicKey(),
'1ba0c5ed1bbbf3b7eb0d7843ba16836a0201ea68a76bafcba507358c45911ff6',
);
});
});
Deno.test('DittoConfig defaults', async (t) => {
const env = new Map<string, string>();
const config = new DittoConf(env);
await t.step('signer throws', () => {
assertThrows(() => config.signer);
});
await t.step('port', () => {
assertEquals(config.port, 4036);
});
});
Deno.test('DittoConfig with insecure media host', () => {
const env = new Map<string, string>([
['LOCAL_DOMAIN', 'https://ditto.test'],
['MEDIA_DOMAIN', 'https://ditto.test'],
]);
assertThrows(
() => new DittoConf(env),
Error,
'For security reasons, MEDIA_DOMAIN cannot be on the same host as LOCAL_DOMAIN',
);
});
Deno.test('DittoConfig with insecure media host and precheck disabled', () => {
const env = new Map<string, string>([
['LOCAL_DOMAIN', 'https://ditto.test'],
['MEDIA_DOMAIN', 'https://ditto.test'],
['DITTO_PRECHECK', 'false'],
]);
new DittoConf(env);
});

516
packages/conf/DittoConf.ts Normal file
View file

@ -0,0 +1,516 @@
import os from 'node:os';
import path from 'node:path';
import { NSecSigner } from '@nostrify/nostrify';
import { decodeBase64 } from '@std/encoding/base64';
import { encodeBase64Url } from '@std/encoding/base64url';
import ISO6391, { type LanguageCode } from 'iso-639-1';
import { nip19 } from 'nostr-tools';
import { getEcdsaPublicKey } from './utils/crypto.ts';
import { optionalBooleanSchema, optionalNumberSchema } from './utils/schema.ts';
import { mergeURLPath } from './utils/url.ts';
/** Ditto application-wide configuration. */
export class DittoConf {
constructor(private env: { get(key: string): string | undefined }) {
if (this.precheck) {
const mediaUrl = new URL(this.mediaDomain);
if (this.url.host === mediaUrl.host) {
throw new Error(
'For security reasons, MEDIA_DOMAIN cannot be on the same host as LOCAL_DOMAIN.\n\nTo disable this check, set DITTO_PRECHECK="false"',
);
}
}
}
/** Cached parsed admin signer. */
private _signer: NSecSigner | undefined;
/** Cached parsed VAPID public key value. */
private _vapidPublicKey: Promise<string | undefined> | undefined;
/**
* Ditto admin secret key in hex format.
* @deprecated Use `signer` instead. TODO: handle auth tokens.
*/
get seckey(): Uint8Array {
const nsec = this.env.get('DITTO_NSEC');
if (!nsec) {
throw new Error('Missing DITTO_NSEC');
}
if (!nsec.startsWith('nsec1')) {
throw new Error('Invalid DITTO_NSEC');
}
return nip19.decode(nsec as `nsec1${string}`).data;
}
/** Ditto admin signer. */
get signer(): NSecSigner {
if (!this._signer) {
this._signer = new NSecSigner(this.seckey);
}
return this._signer;
}
/** Port to use when serving the HTTP server. */
get port(): number {
return parseInt(this.env.get('PORT') || '4036');
}
/** IP addresses not affected by rate limiting. */
get ipWhitelist(): string[] {
return this.env.get('IP_WHITELIST')?.split(',') || [];
}
/** Relay URL to the Ditto server's relay. */
get relay(): `wss://${string}` | `ws://${string}` {
const { protocol, host } = this.url;
return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`;
}
/** Relay to use for NIP-50 `search` queries. */
get searchRelay(): string | undefined {
return this.env.get('SEARCH_RELAY');
}
/** Origin of the Ditto server, including the protocol and port. */
get localDomain(): string {
return this.env.get('LOCAL_DOMAIN') || `http://localhost:${this.port}`;
}
/** Link to an external nostr viewer. */
get externalDomain(): string {
return this.env.get('NOSTR_EXTERNAL') || 'https://njump.me';
}
/** Get a link to a nip19-encoded entity in the configured external viewer. */
external(path: string): string {
return new URL(path, this.externalDomain).toString();
}
/**
* Heroku-style database URL. This is used in production to connect to the
* database.
*
* Follows the format:
*
* ```txt
* protocol://username:password@host:port/database_name
* ```
*/
get databaseUrl(): string {
return this.env.get('DATABASE_URL') ?? 'file://data/pgdata';
}
/** PGlite debug level. 0 disables logging. */
get pgliteDebug(): 0 | 1 | 2 | 3 | 4 | 5 {
return Number(this.env.get('PGLITE_DEBUG') || 0) as 0 | 1 | 2 | 3 | 4 | 5;
}
get vapidPublicKey(): Promise<string | undefined> {
if (!this._vapidPublicKey) {
this._vapidPublicKey = (async () => {
const keys = await this.vapidKeys;
if (keys) {
const { publicKey } = keys;
const bytes = await crypto.subtle.exportKey('raw', publicKey);
return encodeBase64Url(bytes);
}
})();
}
return this._vapidPublicKey;
}
get vapidKeys(): Promise<CryptoKeyPair | undefined> {
return (async () => {
const encoded = this.env.get('VAPID_PRIVATE_KEY');
if (!encoded) {
return;
}
const keyData = decodeBase64(encoded);
const privateKey = await crypto.subtle.importKey(
'pkcs8',
keyData,
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign'],
);
const publicKey = await getEcdsaPublicKey(privateKey, true);
return { privateKey, publicKey };
})();
}
get db(): { timeouts: { default: number; relay: number; timelines: number } } {
const env = this.env;
return {
/** Database query timeout configurations. */
timeouts: {
/** Default query timeout when another setting isn't more specific. */
get default(): number {
return Number(env.get('DB_TIMEOUT_DEFAULT') || 5_000);
},
/** Timeout used for queries made through the Nostr relay. */
get relay(): number {
return Number(env.get('DB_TIMEOUT_RELAY') || 1_000);
},
/** Timeout used for timelines such as home, notifications, hashtag, etc. */
get timelines(): number {
return Number(env.get('DB_TIMEOUT_TIMELINES') || 15_000);
},
},
};
}
/** Time-to-live for captchas in milliseconds. */
get captchaTTL(): number {
return Number(this.env.get('CAPTCHA_TTL') || 5 * 60 * 1000);
}
/** Character limit to enforce for posts made through Mastodon API. */
get postCharLimit(): number {
return Number(this.env.get('POST_CHAR_LIMIT') || 5000);
}
/** S3 media storage configuration. */
get s3(): {
endPoint?: string;
region?: string;
accessKey?: string;
secretKey?: string;
bucket?: string;
pathStyle?: boolean;
port?: number;
sessionToken?: string;
useSSL?: boolean;
} {
const env = this.env;
return {
get endPoint(): string | undefined {
return env.get('S3_ENDPOINT');
},
get region(): string | undefined {
return env.get('S3_REGION');
},
get accessKey(): string | undefined {
return env.get('S3_ACCESS_KEY');
},
get secretKey(): string | undefined {
return env.get('S3_SECRET_KEY');
},
get bucket(): string | undefined {
return env.get('S3_BUCKET');
},
get pathStyle(): boolean | undefined {
return optionalBooleanSchema.parse(env.get('S3_PATH_STYLE'));
},
get port(): number | undefined {
return optionalNumberSchema.parse(env.get('S3_PORT'));
},
get sessionToken(): string | undefined {
return env.get('S3_SESSION_TOKEN');
},
get useSSL(): boolean | undefined {
return optionalBooleanSchema.parse(env.get('S3_USE_SSL'));
},
};
}
/** IPFS uploader configuration. */
get ipfs(): { apiUrl: string } {
const env = this.env;
return {
/** Base URL for private IPFS API calls. */
get apiUrl(): string {
return env.get('IPFS_API_URL') || 'http://localhost:5001';
},
};
}
/**
* The logging configuration for the Ditto server. The config is derived from
* the DEBUG environment variable and it is parsed as follows:
*
* `DEBUG='<jsonl|pretty>:<minimum log level to show>:comma-separated scopes to show'`.
* If the scopes are empty (e.g. in 'pretty:warn:', then all scopes are shown.)
*/
get logConfig(): {
fmt: 'jsonl' | 'pretty';
level: string;
scopes: string[];
} {
let [fmt, level, scopes] = (this.env.get('LOG_CONFIG') || '').split(':');
fmt ||= 'jsonl';
level ||= 'debug';
scopes ||= '';
if (fmt !== 'jsonl' && fmt !== 'pretty') fmt = 'jsonl';
return {
fmt: fmt as 'jsonl' | 'pretty',
level,
scopes: scopes.split(',').filter(Boolean),
};
}
/** nostr.build API endpoint when the `nostrbuild` uploader is used. */
get nostrbuildEndpoint(): string {
return this.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files';
}
/** Default Blossom servers to use when the `blossom` uploader is set. */
get blossomServers(): string[] {
return this.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/'];
}
/** Module to upload files with. */
get uploader(): string | undefined {
return this.env.get('DITTO_UPLOADER');
}
/** Location to use for local uploads. */
get uploadsDir(): string {
return this.env.get('UPLOADS_DIR') || 'data/uploads';
}
/** Media base URL for uploads. */
get mediaDomain(): string {
const value = this.env.get('MEDIA_DOMAIN');
if (!value) {
const url = this.url;
url.host = `media.${url.host}`;
return url.toString();
}
return value;
}
/**
* Whether to analyze media metadata with [blurhash](https://www.npmjs.com/package/blurhash) and [sharp](https://www.npmjs.com/package/sharp).
* This is prone to security vulnerabilities, which is why it's not enabled by default.
*/
get mediaAnalyze(): boolean {
return optionalBooleanSchema.parse(this.env.get('MEDIA_ANALYZE')) ?? false;
}
/** Whether to transcode uploaded video files with ffmpeg. */
get mediaTranscode(): boolean {
return optionalBooleanSchema.parse(this.env.get('MEDIA_TRANSCODE')) ?? false;
}
/** Max upload size for files in number of bytes. Default 100MiB. */
get maxUploadSize(): number {
return Number(this.env.get('MAX_UPLOAD_SIZE') || 100 * 1024 * 1024);
}
/** Usernames that regular users cannot sign up with. */
get forbiddenUsernames(): string[] {
return this.env.get('FORBIDDEN_USERNAMES')?.split(',') || [
'_',
'admin',
'administrator',
'root',
'sysadmin',
'system',
];
}
/** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */
get url(): URL {
return new URL(this.localDomain);
}
/** Merges the path with the localDomain. */
local(path: string): string {
return mergeURLPath(this.localDomain, path);
}
/** URL to send Sentry errors to. */
get sentryDsn(): string | undefined {
return this.env.get('SENTRY_DSN');
}
/** Postgres settings. */
get pg(): { poolSize: number } {
const env = this.env;
return {
/** Number of connections to use in the pool. */
get poolSize(): number {
return Number(env.get('PG_POOL_SIZE') ?? 20);
},
};
}
/** Whether to enable requesting events from known relays. */
get firehoseEnabled(): boolean {
return optionalBooleanSchema.parse(this.env.get('FIREHOSE_ENABLED')) ?? true;
}
/** Number of events the firehose is allowed to process at one time before they have to wait in a queue. */
get firehoseConcurrency(): number {
return Math.ceil(Number(this.env.get('FIREHOSE_CONCURRENCY') ?? 1));
}
/** Nostr event kinds of events to listen for on the firehose. */
get firehoseKinds(): number[] {
return (this.env.get('FIREHOSE_KINDS') ?? '0, 1, 3, 5, 6, 7, 20, 9735, 10002')
.split(/[, ]+/g)
.map(Number);
}
/**
* Whether Ditto should subscribe to Nostr events from the Postgres database itself.
* This would make Nostr events inserted directly into Postgres available to the streaming API and relay.
*/
get notifyEnabled(): boolean {
return optionalBooleanSchema.parse(this.env.get('NOTIFY_ENABLED')) ?? true;
}
/** Whether to enable Ditto cron jobs. */
get cronEnabled(): boolean {
return optionalBooleanSchema.parse(this.env.get('CRON_ENABLED')) ?? true;
}
/** User-Agent to use when fetching link previews. Pretend to be Facebook by default. */
get fetchUserAgent(): string {
return this.env.get('DITTO_FETCH_USER_AGENT') ?? 'facebookexternalhit';
}
/** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */
get policy(): string {
return this.env.get('DITTO_POLICY') || path.join(this.dataDir, 'policy.ts');
}
/** Absolute path to the data directory used by Ditto. */
get dataDir(): string {
return this.env.get('DITTO_DATA_DIR') || path.join(Deno.cwd(), 'data');
}
/** Absolute path of the Deno directory. */
get denoDir(): string {
return this.env.get('DENO_DIR') || `${os.userInfo().homedir}/.cache/deno`;
}
/** Whether zap splits should be enabled. */
get zapSplitsEnabled(): boolean {
return optionalBooleanSchema.parse(this.env.get('ZAP_SPLITS_ENABLED')) ?? false;
}
/** Languages this server wishes to highlight. Used when querying trends.*/
get preferredLanguages(): LanguageCode[] | undefined {
return this.env.get('DITTO_LANGUAGES')?.split(',')?.filter(ISO6391.validate);
}
/** Mints to be displayed in the UI when the user decides to create a wallet.*/
get cashuMints(): string[] {
return this.env.get('CASHU_MINTS')?.split(',') ?? [];
}
/** Translation provider used to translate posts. */
get translationProvider(): string | undefined {
return this.env.get('TRANSLATION_PROVIDER');
}
/** DeepL URL endpoint. */
get deeplBaseUrl(): string | undefined {
return this.env.get('DEEPL_BASE_URL');
}
/** DeepL API KEY. */
get deeplApiKey(): string | undefined {
return this.env.get('DEEPL_API_KEY');
}
/** LibreTranslate URL endpoint. */
get libretranslateBaseUrl(): string | undefined {
return this.env.get('LIBRETRANSLATE_BASE_URL');
}
/** LibreTranslate API KEY. */
get libretranslateApiKey(): string | undefined {
return this.env.get('LIBRETRANSLATE_API_KEY');
}
/** Cache settings. */
get caches(): {
nip05: { max: number; ttl: number };
favicon: { max: number; ttl: number };
translation: { max: number; ttl: number };
} {
const env = this.env;
return {
/** NIP-05 cache settings. */
get nip05(): { max: number; ttl: number } {
return {
max: Number(env.get('DITTO_CACHE_NIP05_MAX') || 3000),
ttl: Number(env.get('DITTO_CACHE_NIP05_TTL') || 1 * 60 * 60 * 1000),
};
},
/** Favicon cache settings. */
get favicon(): { max: number; ttl: number } {
return {
max: Number(env.get('DITTO_CACHE_FAVICON_MAX') || 500),
ttl: Number(env.get('DITTO_CACHE_FAVICON_TTL') || 1 * 60 * 60 * 1000),
};
},
/** Translation cache settings. */
get translation(): { max: number; ttl: number } {
return {
max: Number(env.get('DITTO_CACHE_TRANSLATION_MAX') || 1000),
ttl: Number(env.get('DITTO_CACHE_TRANSLATION_TTL') || 6 * 60 * 60 * 1000),
};
},
};
}
/** Custom profile fields configuration. */
get profileFields(): { maxFields: number; nameLength: number; valueLength: number } {
const env = this.env;
return {
get maxFields(): number {
return Number(env.get('PROFILE_FIELDS_MAX_FIELDS') || 10);
},
get nameLength(): number {
return Number(env.get('PROFILE_FIELDS_NAME_LENGTH') || 255);
},
get valueLength(): number {
return Number(env.get('PROFILE_FIELDS_VALUE_LENGTH') || 2047);
},
};
}
/** Maximum time between events before a streak is broken, *in seconds*. */
get streakWindow(): number {
return Number(this.env.get('STREAK_WINDOW') || 129600);
}
/** Whether to perform security/configuration checks on startup. */
get precheck(): boolean {
return optionalBooleanSchema.parse(this.env.get('DITTO_PRECHECK')) ?? true;
}
/** Path to `ffmpeg` executable. */
get ffmpegPath(): string {
return this.env.get('FFMPEG_PATH') || 'ffmpeg';
}
/** Path to `ffprobe` executable. */
get ffprobePath(): string {
return this.env.get('FFPROBE_PATH') || 'ffprobe';
}
}

7
packages/conf/deno.json Normal file
View file

@ -0,0 +1,7 @@
{
"name": "@ditto/conf",
"version": "0.1.0",
"exports": {
".": "./mod.ts"
}
}

1
packages/conf/mod.ts Normal file
View file

@ -0,0 +1 @@
export { DittoConf } from './DittoConf.ts';

View file

@ -1,6 +1,6 @@
import { assertEquals } from '@std/assert';
import { getEcdsaPublicKey } from '@/utils/crypto.ts';
import { getEcdsaPublicKey } from './crypto.ts';
Deno.test('getEcdsaPublicKey', async () => {
const { publicKey, privateKey } = await crypto.subtle.generateKey(

View file

@ -0,0 +1,17 @@
import { assertEquals, assertThrows } from '@std/assert';
import { optionalBooleanSchema, optionalNumberSchema } from './schema.ts';
Deno.test('optionalBooleanSchema', () => {
assertEquals(optionalBooleanSchema.parse('true'), true);
assertEquals(optionalBooleanSchema.parse('false'), false);
assertEquals(optionalBooleanSchema.parse(undefined), undefined);
assertThrows(() => optionalBooleanSchema.parse('invalid'));
});
Deno.test('optionalNumberSchema', () => {
assertEquals(optionalNumberSchema.parse('123'), 123);
assertEquals(optionalNumberSchema.parse('invalid'), NaN); // maybe this should throw?
assertEquals(optionalNumberSchema.parse(undefined), undefined);
});

View file

@ -0,0 +1,11 @@
import { z } from 'zod';
export const optionalBooleanSchema = z
.enum(['true', 'false'])
.optional()
.transform((value) => value !== undefined ? value === 'true' : undefined);
export const optionalNumberSchema = z
.string()
.optional()
.transform((value) => value !== undefined ? Number(value) : undefined);

View file

@ -0,0 +1,9 @@
import { assertEquals } from '@std/assert';
import { mergeURLPath } from './url.ts';
Deno.test('mergeURLPath', () => {
assertEquals(mergeURLPath('https://mario.com', '/path'), 'https://mario.com/path');
assertEquals(mergeURLPath('https://mario.com', 'https://luigi.com/path'), 'https://mario.com/path');
assertEquals(mergeURLPath('https://mario.com', 'https://luigi.com/path?q=1'), 'https://mario.com/path?q=1');
});

View file

@ -0,0 +1,23 @@
/**
* Produce a URL whose origin is guaranteed to be the same as the base URL.
* The path is either an absolute path (starting with `/`), or a full URL. In either case, only its path is used.
*/
export function mergeURLPath(
/** Base URL. Result is guaranteed to use this URL's origin. */
base: string,
/** Either an absolute path (starting with `/`), or a full URL. If a full URL, its path */
path: string,
): string {
const url = new URL(
path.startsWith('/') ? path : new URL(path).pathname,
base,
);
if (!path.startsWith('/')) {
// Copy query parameters from the original URL to the new URL
const originalUrl = new URL(path);
url.search = originalUrl.search;
}
return url.toString();
}

View file

@ -1,15 +1,16 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
import { DittoTables } from '@/db/DittoTables.ts';
import type { DittoTables } from './DittoTables.ts';
export interface DittoDatabase {
export interface DittoDB extends AsyncDisposable {
readonly kysely: Kysely<DittoTables>;
readonly poolSize: number;
readonly availableConnections: number;
migrate(): Promise<void>;
listen(channel: string, callback: (payload: string) => void): void;
}
export interface DittoDatabaseOpts {
export interface DittoDBOpts {
poolSize?: number;
debug?: 0 | 1 | 2 | 3 | 4 | 5;
}

View file

@ -0,0 +1,52 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { logi } from '@soapbox/logi';
import { FileMigrationProvider, type Kysely, Migrator } from 'kysely';
import type { JsonValue } from '@std/json';
export class DittoPgMigrator {
private migrator: Migrator;
// deno-lint-ignore no-explicit-any
constructor(private kysely: Kysely<any>) {
this.migrator = new Migrator({
db: this.kysely,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: new URL(import.meta.resolve('./migrations')).pathname,
}),
});
}
async migrate(): Promise<void> {
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Running migrations...', state: 'started' });
const { results, error } = await this.migrator.migrateToLatest();
if (error) {
logi({
level: 'fatal',
ns: 'ditto.db.migration',
msg: 'Migration failed.',
state: 'failed',
results: results as unknown as JsonValue,
error: error instanceof Error ? error : null,
});
throw new Error('Migration failed.');
} else {
if (!results?.length) {
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Everything up-to-date.', state: 'skipped' });
} else {
logi({
level: 'info',
ns: 'ditto.db.migration',
msg: 'Migrations finished!',
state: 'migrated',
results: results as unknown as JsonValue,
});
}
}
}
}

View file

@ -1,14 +1,17 @@
import { Generated } from 'kysely';
import type { NPostgresSchema } from '@nostrify/db';
import type { Generated } from 'kysely';
import { NPostgresSchema } from '@nostrify/db';
import type { MastodonPreviewCard } from '@ditto/mastoapi/types';
export interface DittoTables extends NPostgresSchema {
auth_tokens: AuthTokenRow;
author_stats: AuthorStatsRow;
domain_favicons: DomainFaviconRow;
event_stats: EventStatsRow;
pubkey_domains: PubkeyDomainRow;
event_zaps: EventZapRow;
push_subscriptions: PushSubscriptionRow;
/** This is a materialized view of `author_stats` pre-sorted by followers_count. */
top_authors: Pick<AuthorStatsRow, 'pubkey' | 'followers_count' | 'search'>;
}
interface AuthorStatsRow {
@ -17,6 +20,12 @@ interface AuthorStatsRow {
following_count: number;
notes_count: number;
search: string;
streak_start: number | null;
streak_end: number | null;
nip05: string | null;
nip05_domain: string | null;
nip05_hostname: string | null;
nip05_last_verified_at: number | null;
}
interface EventStatsRow {
@ -27,6 +36,8 @@ interface EventStatsRow {
quotes_count: number;
reactions: string;
zaps_amount: number;
zaps_amount_cashu: number;
link_preview?: MastodonPreviewCard;
}
interface AuthTokenRow {
@ -38,9 +49,9 @@ interface AuthTokenRow {
created_at: Date;
}
interface PubkeyDomainRow {
pubkey: string;
interface DomainFaviconRow {
domain: string;
favicon: string;
last_updated_at: number;
}

View file

@ -0,0 +1,41 @@
import { dbQueriesCounter, dbQueryDurationHistogram } from '@ditto/metrics';
import { logi, type LogiValue } from '@soapbox/logi';
import type { Logger } from 'kysely';
/** Log the SQL for queries. */
export const KyselyLogger: Logger = (event) => {
const { query, queryDurationMillis } = event;
const { parameters, sql } = query;
const duration = queryDurationMillis / 1000;
dbQueriesCounter.inc();
dbQueryDurationHistogram.observe(duration);
if (event.level === 'query') {
logi({ level: 'trace', ns: 'ditto.sql', sql, parameters: parameters as LogiValue, duration });
}
if (event.level === 'error') {
if (event.error instanceof Error) {
switch (event.error.message) {
case 'duplicate key value violates unique constraint "nostr_events_pkey"':
case 'duplicate key value violates unique constraint "author_stats_pkey"':
case 'duplicate key value violates unique constraint "event_stats_pkey"':
case 'duplicate key value violates unique constraint "event_zaps_pkey"':
case 'insert or update on table "event_stats" violates foreign key constraint "event_stats_event_id_fkey"':
return; // Don't log expected errors
}
}
logi({
level: 'error',
ns: 'ditto.sql',
sql,
parameters: parameters as LogiValue,
error: event.error instanceof Error ? event.error : null,
duration,
});
}
};

View file

@ -0,0 +1,22 @@
import { assertEquals, assertRejects } from '@std/assert';
import { DittoPglite } from './DittoPglite.ts';
Deno.test('DittoPglite', async () => {
await using db = new DittoPglite('memory://');
await db.migrate();
assertEquals(db.poolSize, 1);
assertEquals(db.availableConnections, 1);
});
Deno.test('DittoPglite query after closing', async () => {
const db = new DittoPglite('memory://');
await db[Symbol.asyncDispose]();
await assertRejects(
() => db.kysely.selectFrom('nostr_events').selectAll().execute(),
Error,
'PGlite is closed',
);
});

View file

@ -0,0 +1,62 @@
import { PGlite } from '@electric-sql/pglite';
import { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm';
import { PgliteDialect } from '@soapbox/kysely-pglite';
import { Kysely } from 'kysely';
import { KyselyLogger } from '../KyselyLogger.ts';
import { DittoPgMigrator } from '../DittoPgMigrator.ts';
import { isWorker } from '../utils/worker.ts';
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
import type { DittoTables } from '../DittoTables.ts';
export class DittoPglite implements DittoDB {
readonly poolSize = 1;
readonly availableConnections = 1;
readonly kysely: Kysely<DittoTables>;
private pglite: PGlite;
private migrator: DittoPgMigrator;
constructor(databaseUrl: string, opts?: DittoDBOpts) {
const url = new URL(databaseUrl);
if (url.protocol === 'file:' && isWorker()) {
throw new Error('PGlite is not supported in worker threads.');
}
this.pglite = new PGlite(databaseUrl, {
extensions: { pg_trgm },
debug: opts?.debug,
});
this.kysely = new Kysely<DittoTables>({
dialect: new PgliteDialect({ database: this.pglite }),
log: KyselyLogger,
});
this.migrator = new DittoPgMigrator(this.kysely);
}
listen(channel: string, callback: (payload: string) => void): void {
this.pglite.listen(channel, callback);
}
async migrate(): Promise<void> {
await this.migrator.migrate();
}
async [Symbol.asyncDispose](): Promise<void> {
try {
// FIXME: `kysely.destroy()` calls `pglite.close()` internally, but it doesn't work.
await this.pglite.close();
await this.kysely.destroy();
} catch (e) {
if (e instanceof Error && e.message === 'PGlite is closed') {
// Make dispose idempotent.
} else {
throw e;
}
}
}
}

View file

@ -0,0 +1,6 @@
import { DittoPolyPg } from './DittoPolyPg.ts';
Deno.test('DittoPolyPg', async () => {
const db = new DittoPolyPg('memory://');
await db.migrate();
});

View file

@ -0,0 +1,53 @@
import { DittoPglite } from './DittoPglite.ts';
import { DittoPostgres } from './DittoPostgres.ts';
import type { Kysely } from 'kysely';
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
import type { DittoTables } from '../DittoTables.ts';
/** Creates either a PGlite or Postgres connection depending on the databaseUrl. */
export class DittoPolyPg implements DittoDB {
private adapter: DittoDB;
/** Open a new database connection. */
constructor(databaseUrl: string, opts?: DittoDBOpts) {
const { protocol } = new URL(databaseUrl);
switch (protocol) {
case 'file:':
case 'memory:':
this.adapter = new DittoPglite(databaseUrl, opts);
break;
case 'postgres:':
case 'postgresql:':
this.adapter = new DittoPostgres(databaseUrl, opts);
break;
default:
throw new Error('Unsupported database URL.');
}
}
get kysely(): Kysely<DittoTables> {
return this.adapter.kysely;
}
async migrate(): Promise<void> {
await this.adapter.migrate();
}
listen(channel: string, callback: (payload: string) => void): void {
this.adapter.listen(channel, callback);
}
get poolSize(): number {
return this.adapter.poolSize;
}
get availableConnections(): number {
return this.adapter.availableConnections;
}
async [Symbol.asyncDispose](): Promise<void> {
await this.adapter[Symbol.asyncDispose]();
}
}

View file

@ -0,0 +1,22 @@
import { DittoConf } from '@ditto/conf';
import { DittoPostgres } from './DittoPostgres.ts';
const conf = new DittoConf(Deno.env);
const isPostgres = /^postgres(?:ql)?:/.test(conf.databaseUrl);
Deno.test('DittoPostgres', { ignore: !isPostgres }, async () => {
await using db = new DittoPostgres(conf.databaseUrl);
await db.migrate();
});
// FIXME: There is a problem with postgres-js where queries just hang after the database is closed.
// Deno.test('DittoPostgres query after closing', { ignore: !isPostgres }, async () => {
// const db = new DittoPostgres(conf.databaseUrl);
// await db[Symbol.asyncDispose]();
//
// await assertRejects(
// () => db.kysely.selectFrom('nostr_events').selectAll().execute(),
// );
// });

View file

@ -0,0 +1,79 @@
import {
type BinaryOperationNode,
FunctionNode,
Kysely,
OperatorNode,
PostgresAdapter,
PostgresIntrospector,
PostgresQueryCompiler,
PrimitiveValueListNode,
ValueNode,
} from 'kysely';
import { type PostgresJSDialectConfig, PostgresJSDriver } from 'kysely-postgres-js';
import postgres from 'postgres';
import { DittoPgMigrator } from '../DittoPgMigrator.ts';
import { KyselyLogger } from '../KyselyLogger.ts';
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
import type { DittoTables } from '../DittoTables.ts';
export class DittoPostgres implements DittoDB {
private pg: ReturnType<typeof postgres>;
private migrator: DittoPgMigrator;
readonly kysely: Kysely<DittoTables>;
constructor(databaseUrl: string, opts?: DittoDBOpts) {
this.pg = postgres(databaseUrl, { max: opts?.poolSize });
this.kysely = new Kysely<DittoTables>({
dialect: {
createAdapter: () => new PostgresAdapter(),
createDriver: () =>
new PostgresJSDriver({ postgres: this.pg as unknown as PostgresJSDialectConfig['postgres'] }),
createIntrospector: (db) => new PostgresIntrospector(db),
createQueryCompiler: () => new DittoPostgresQueryCompiler(),
},
log: KyselyLogger,
});
this.migrator = new DittoPgMigrator(this.kysely);
}
listen(channel: string, callback: (payload: string) => void): void {
this.pg.listen(channel, callback);
}
async migrate(): Promise<void> {
await this.migrator.migrate();
}
get poolSize(): number {
return this.pg.connections.open;
}
get availableConnections(): number {
return this.pg.connections.idle;
}
async [Symbol.asyncDispose](): Promise<void> {
await this.pg.end({ timeout: 0 }); // force-close the connections
await this.kysely.destroy();
}
}
/** Converts `in` queries to `any` to improve prepared statements on Postgres. */
class DittoPostgresQueryCompiler extends PostgresQueryCompiler {
protected override visitBinaryOperation(node: BinaryOperationNode): void {
if (
OperatorNode.is(node.operator) && node.operator.operator === 'in' && PrimitiveValueListNode.is(node.rightOperand)
) {
this.visitNode(node.leftOperand);
this.append(' = ');
this.visitNode(FunctionNode.create('any', [ValueNode.create(node.rightOperand.values)]));
} else {
super.visitBinaryOperation(node);
}
}
}

View file

@ -0,0 +1,11 @@
import { assertEquals } from '@std/assert';
import { DummyDB } from './DummyDB.ts';
Deno.test('DummyDB', async () => {
const db = new DummyDB();
await db.migrate();
const rows = await db.kysely.selectFrom('nostr_events').selectAll().execute();
assertEquals(rows, []);
});

View file

@ -0,0 +1,33 @@
import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from 'kysely';
import type { DittoDB } from '../DittoDB.ts';
import type { DittoTables } from '../DittoTables.ts';
export class DummyDB implements DittoDB {
readonly kysely: Kysely<DittoTables>;
readonly poolSize = 0;
readonly availableConnections = 0;
constructor() {
this.kysely = new Kysely<DittoTables>({
dialect: {
createAdapter: () => new PostgresAdapter(),
createDriver: () => new DummyDriver(),
createIntrospector: (db) => new PostgresIntrospector(db),
createQueryCompiler: () => new PostgresQueryCompiler(),
},
});
}
listen(): void {
// noop
}
migrate(): Promise<void> {
return Promise.resolve();
}
[Symbol.asyncDispose](): Promise<void> {
return Promise.resolve();
}
}

View file

@ -0,0 +1,25 @@
import { DittoConf } from '@ditto/conf';
import { NPostgres } from '@nostrify/db';
import { genEvent } from '@nostrify/nostrify/test';
import { assertEquals } from '@std/assert';
import { DittoPolyPg } from './DittoPolyPg.ts';
import { TestDB } from './TestDB.ts';
Deno.test('TestDB', async () => {
const conf = new DittoConf(Deno.env);
const orig = new DittoPolyPg(conf.databaseUrl);
await using db = new TestDB(orig);
await db.migrate();
await db.clear();
const store = new NPostgres(orig.kysely);
await store.event(genEvent());
assertEquals((await store.count([{}])).count, 1);
await db.clear();
assertEquals((await store.count([{}])).count, 0);
});

View file

@ -0,0 +1,49 @@
import { type Kysely, sql } from 'kysely';
import type { DittoDB } from '../DittoDB.ts';
import type { DittoTables } from '../DittoTables.ts';
/** Wraps another DittoDB implementation to clear all data when disposed. */
export class TestDB implements DittoDB {
constructor(private db: DittoDB) {}
get kysely(): Kysely<DittoTables> {
return this.db.kysely;
}
get poolSize(): number {
return this.db.poolSize;
}
get availableConnections(): number {
return this.db.availableConnections;
}
migrate(): Promise<void> {
return this.db.migrate();
}
listen(channel: string, callback: (payload: string) => void): void {
return this.db.listen(channel, callback);
}
/** Truncate all tables. */
async clear(): Promise<void> {
const query = sql<{ tablename: string }>`select tablename from pg_tables where schemaname = current_schema()`;
const { rows } = await query.execute(this.db.kysely);
for (const { tablename } of rows) {
if (tablename.startsWith('kysely_')) {
continue; // Skip Kysely's internal tables
} else {
await sql`truncate table ${sql.ref(tablename)} cascade`.execute(this.db.kysely);
}
}
}
async [Symbol.asyncDispose](): Promise<void> {
await this.clear();
await this.db[Symbol.asyncDispose]();
}
}

7
packages/db/deno.json Normal file
View file

@ -0,0 +1,7 @@
{
"name": "@ditto/db",
"version": "0.1.0",
"exports": {
".": "./mod.ts"
}
}

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('events')
.addColumn('id', 'text', (col) => col.primaryKey())
@ -52,7 +52,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('events').execute();
await db.schema.dropTable('tags').execute();
await db.schema.dropTable('users').execute();

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('relays')
.addColumn('url', 'text', (col) => col.primaryKey())
@ -9,6 +9,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('relays').execute();
}

View file

@ -0,0 +1,8 @@
import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> {
// This migration used to create an FTS table for SQLite, but SQLite support was removed.
}
export async function down(_db: Kysely<unknown>): Promise<void> {
}

View file

@ -0,0 +1,8 @@
import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> {
}
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('users').dropColumn('admin').execute();
}

View file

@ -0,0 +1,9 @@
import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> {
}
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('idx_users_pubkey').execute();
await db.schema.dropIndex('idx_users_username').execute();
}

View file

@ -1,6 +1,6 @@
import { Kysely, sql } from 'kysely';
import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('tags_new')
.addColumn('tag', 'text', (col) => col.notNull())
@ -42,7 +42,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('tags').execute();
await db.schema

View file

@ -0,0 +1,7 @@
import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> {
}
export async function down(_db: Kysely<unknown>): Promise<void> {
}

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('unattached_media')
.addColumn('id', 'text', (c) => c.primaryKey())
@ -29,6 +29,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('unattached_media').execute();
}

View file

@ -0,0 +1,7 @@
import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> {
}
export async function down(_db: Kysely<unknown>): Promise<void> {
}

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('author_stats')
.addColumn('pubkey', 'text', (col) => col.primaryKey())
@ -18,7 +18,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('author_stats').execute();
await db.schema.dropTable('event_stats').execute();
}

View file

@ -0,0 +1,8 @@
import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('users').ifExists().execute();
}
export async function down(_db: Kysely<unknown>): Promise<void> {
}

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createIndex('idx_events_kind_pubkey_created_at')
.on('events')
@ -8,6 +8,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('idx_events_kind_pubkey_created_at').execute();
}

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('idx_tags_tag').execute();
await db.schema.dropIndex('idx_tags_value').execute();
@ -11,7 +11,7 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('idx_tags_tag_value').execute();
await db.schema

View file

@ -1,9 +1,9 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('events').addColumn('deleted_at', 'integer').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('events').dropColumn('deleted_at').execute();
}

View file

@ -1,11 +1,11 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.createIndex('idx_author_stats_pubkey').on('author_stats').column('pubkey').execute();
await db.schema.createIndex('idx_event_stats_event_id').on('event_stats').column('event_id').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('idx_author_stats_pubkey').on('author_stats').execute();
await db.schema.dropIndex('idx_event_stats_event_id').on('event_stats').execute();
}

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('pubkey_domains')
.ifNotExists()
@ -16,6 +16,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('pubkey_domains').execute();
}

View file

@ -1,12 +1,12 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable('pubkey_domains')
.addColumn('last_updated_at', 'integer', (col) => col.notNull().defaultTo(0))
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('pubkey_domains').dropColumn('last_updated_at').execute();
}

View file

@ -1,10 +1,10 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('relays').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('relays')
.addColumn('url', 'text', (col) => col.primaryKey())

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createIndex('idx_events_created_at_kind')
.on('events')
@ -9,6 +9,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('idx_events_created_at_kind').ifExists().execute();
}

View file

@ -1,12 +1,12 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('events').renameTo('nostr_events').execute();
await db.schema.alterTable('tags').renameTo('nostr_tags').execute();
await db.schema.alterTable('nostr_tags').renameColumn('tag', 'name').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('nostr_events').renameTo('events').execute();
await db.schema.alterTable('nostr_tags').renameTo('tags').execute();
await db.schema.alterTable('tags').renameColumn('name', 'tag').execute();

View file

@ -1,10 +1,11 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
// deno-lint-ignore no-explicit-any
export async function up(db: Kysely<any>): Promise<void> {
await db.deleteFrom('nostr_events').where('deleted_at', 'is not', null).execute();
await db.schema.alterTable('nostr_events').dropColumn('deleted_at').execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('nostr_events').addColumn('deleted_at', 'integer').execute();
}

View file

@ -1,6 +1,6 @@
import { Kysely, sql } from 'kysely';
import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.createTable('nostr_pgfts')
.ifNotExists()
.addColumn('event_id', 'text', (c) => c.primaryKey().references('nostr_events.id').onDelete('cascade'))
@ -8,6 +8,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('nostr_pgfts').ifExists().execute();
}

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createIndex('nostr_pgfts_gin_search_vec')
.ifNotExists()
@ -10,6 +10,6 @@ export async function up(db: Kysely<any>): Promise<void> {
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('nostr_pgfts_gin_search_vec').ifExists().execute();
}

View file

@ -1,12 +1,12 @@
import { Kysely } from 'kysely';
import type { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable('event_stats')
.addColumn('reactions', 'text', (col) => col.defaultTo('{}'))
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('event_stats').dropColumn('reactions').execute();
}

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