Compare commits
486 Commits
lightning-
...
zap-amount
| Author | SHA1 | Date | |
|---|---|---|---|
|
0b288c921e
|
|||
| f73c0ec1c4 | |||
|
|
05b62c5860 | ||
|
|
fae061cec0 | ||
|
|
4570ba797c | ||
|
|
d1ea081018 | ||
|
|
682704b2cb | ||
|
|
176f1a338a | ||
|
|
fc1eb326e8 | ||
|
|
5e420187e0 | ||
|
|
4815c8a6f7 | ||
|
|
f42ae0673d | ||
|
|
474e2d8d57 | ||
|
|
95a91bed7e | ||
|
|
ff12d8bd7e | ||
|
|
f8245a7b0e | ||
|
|
4036995348 | ||
|
|
5b6534fd56 | ||
|
|
bdd10cccaa | ||
|
|
e9f4cbe881 | ||
|
|
91abd187d3 | ||
|
|
b9d8b1dbf3 | ||
|
|
12a7b483a0 | ||
|
|
caa7802bce | ||
|
|
9c47d2e0bd | ||
|
|
5cd5a249ce | ||
|
|
c86b3a999d | ||
|
|
b5afa3c0b4 | ||
|
|
8f32c81b6c | ||
|
|
f8185d0ca5 | ||
|
|
eb99584501 | ||
|
|
919f644cba | ||
|
|
690e1347e0 | ||
|
|
744bf4bb07 | ||
|
|
475940aa01 | ||
|
|
28a06af534 | ||
|
|
208b3331ca | ||
|
|
5b1f0c4714 | ||
|
|
249e765642 | ||
|
|
712624f515 | ||
|
|
6e7b3b94d7 | ||
|
|
969a2b656e | ||
|
|
d8e7b4707e | ||
|
|
a51618cfd3 | ||
|
|
82da5da4d3 | ||
|
|
37f9c93705 | ||
|
|
094cf5e8cc | ||
|
|
46541694a0 | ||
|
|
04d4ff4e99 | ||
|
|
2d02766461 | ||
|
|
1e6873c879 | ||
|
|
d3496af5cc | ||
|
|
ec798bdeb2 | ||
|
|
fa9b952295 | ||
|
|
27f55bc09f | ||
|
|
52845a52bb | ||
|
|
4e27cca12b | ||
|
|
98e9ba25da | ||
|
|
e6cb6c938b | ||
|
|
af5961ce26 | ||
|
|
58de0025aa | ||
|
|
c931108741 | ||
|
|
20255198fd | ||
|
|
289a8e262a | ||
|
|
05baba9c03 | ||
|
|
e0461d3458 | ||
|
|
62aa72c215 | ||
|
|
287b35a8fb | ||
|
|
478d7b4060 | ||
|
|
2c4728508b | ||
|
|
d24a3f0ce5 | ||
|
|
efba599779 | ||
|
|
19243d49e1 | ||
|
|
6845d0df47 | ||
|
|
8e79ad582a | ||
|
|
282c02eed4 | ||
|
|
155ac27bb5 | ||
|
|
be1d149f4b | ||
|
|
9e0dc47e98 | ||
|
|
0916b14b32 | ||
|
|
6818d001f2 | ||
|
|
4bf9160502 | ||
|
|
02df1e209b | ||
|
|
3186b0e1d3 | ||
|
|
de0935582c | ||
|
|
573de6b881 | ||
|
|
44ab702792 | ||
|
|
1fdf234c46 | ||
|
|
3018200e95 | ||
|
|
47b79fc02e | ||
|
|
0c483bb55a | ||
|
|
ddd30054e8 | ||
|
|
30c5225ed0 | ||
|
|
8c446f804c | ||
|
|
e92018aee5 | ||
|
|
cfb140472d | ||
|
|
2f5fd54297 | ||
|
|
02e970eb9b | ||
|
|
b4b84e6895 | ||
|
|
7831ede057 | ||
|
|
a8d7d971b1 | ||
|
|
201cdd7edc | ||
|
|
e3ca6ca5b4 | ||
|
|
494386d211 | ||
|
|
6c53bc75f2 | ||
|
|
6001063754 | ||
|
|
eb0a1ee807 | ||
|
|
827731b9cb | ||
|
|
56d44d0004 | ||
|
|
7742c8fb3c | ||
|
|
7f2ee78512 | ||
|
|
4d75894bc4 | ||
|
|
bbed448ccb | ||
|
|
3fb4d81d48 | ||
|
|
fc30b68c40 | ||
|
|
0ac25b7aa3 | ||
|
|
b326f007f2 | ||
|
|
a86d8416fc | ||
|
|
b5c57dc935 | ||
|
|
7d6814a481 | ||
|
|
8dd048681b | ||
|
|
2d02a17af6 | ||
|
|
3171959d85 | ||
|
|
bca3716e33 | ||
|
|
57db252783 | ||
|
|
319579f912 | ||
|
|
92e1e4b08f | ||
|
|
ffc50bb2c1 | ||
|
|
a562be009d | ||
|
|
30c9bc7db7 | ||
|
|
0ac03df841 | ||
|
|
db99b4f4d4 | ||
|
|
cc9585b6e3 | ||
|
|
bd17dcfac6 | ||
|
|
25e91b386c | ||
|
|
560e9e53cd | ||
|
|
1c1e5fa2a0 | ||
|
|
2d5f86b142 | ||
|
|
89686d758a | ||
|
|
6c26add1da | ||
|
|
3c5a83392e | ||
|
|
1c63c3b9bb | ||
|
|
0bd4717e01 | ||
|
|
bebd531b58 | ||
|
|
5788c077c4 | ||
|
|
1b77b4f0e0 | ||
|
|
62625c6ff3 | ||
|
|
c8d88058d4 | ||
|
|
b8bef86ea1 | ||
|
|
b128330b2a | ||
|
|
934ea80f85 | ||
|
|
588cebd18d | ||
|
|
ccca6e58ec | ||
|
|
c1befa5221 | ||
|
|
8b3c86c5de | ||
|
|
05c5a6dacb | ||
|
|
1a6568deca | ||
|
|
1b2f4c41df | ||
|
|
25bcf9c243 | ||
|
|
3993679cc0 | ||
|
|
e302bf37fa | ||
|
|
a45f4d3087 | ||
|
|
d598e178c1 | ||
|
|
77601e77ee | ||
|
|
206efba58a | ||
|
|
a84749cd07 | ||
|
|
099b588be2 | ||
|
|
75c7adddb8 | ||
|
|
9f1b9ab945 | ||
|
|
b2080a946e | ||
|
|
942e47a720 | ||
|
|
6dbf3416b9 | ||
|
|
2b14acd62f | ||
|
|
267a9ac54b | ||
|
|
8b03ed6175 | ||
|
|
6cd7b945ca | ||
|
|
e5e6735129 | ||
|
|
9c2f7a931c | ||
|
|
b1bbf355de | ||
|
|
d7a2064786 | ||
|
|
4d14ca8d0a | ||
|
|
81d65cd5bf | ||
|
|
f03d8a5ac9 | ||
|
|
0df18ae1a4 | ||
|
|
8c5ec32eaa | ||
|
|
bdedf8bd8c | ||
|
|
c2383060aa | ||
|
|
432cdb96d9 | ||
|
|
f580c7dd93 | ||
|
|
c677233dcb | ||
|
|
d063362bd7 | ||
|
|
088683696a | ||
|
|
f2795aa71c | ||
|
|
c831976078 | ||
|
|
c2c73c3af6 | ||
|
|
971fa3e4ef | ||
|
|
dfa145dd4a | ||
|
|
4cfe28d802 | ||
|
|
034f2cc02f | ||
|
|
6f9bd6c4f4 | ||
|
|
d73422db38 | ||
|
|
c3b06d281e | ||
|
|
1b09e9458c | ||
|
|
e0a2dcf3db | ||
|
|
9ff1f69a82 | ||
|
|
623b8603c2 | ||
|
|
d8b083010d | ||
|
|
887eb4e1e2 | ||
|
|
b5ad3ed1a5 | ||
|
|
371e9fb406 | ||
|
|
aa5809d792 | ||
|
|
30ba0d72cc | ||
|
|
373cd71f69 | ||
|
|
acaf327a07 | ||
|
|
9f0bf7dff5 | ||
|
|
88d7eb8a86 | ||
|
|
76862776b8 | ||
|
|
4c55459c1f | ||
|
|
f7cdc7bc31 | ||
|
|
1bc4971111 | ||
|
|
6ce6c79160 | ||
|
|
1ffbd80c67 | ||
|
|
1fb88a912a | ||
|
|
954f48b23d | ||
|
|
cc75a8450a | ||
|
|
389c2c9695 | ||
|
|
4a6121ba13 | ||
|
|
a469f2e127 | ||
|
|
2f8f18b846 | ||
|
|
3a7cf4d08d | ||
|
|
e3001cc240 | ||
|
|
d1ef113a8b | ||
|
|
f187f4f8f2 | ||
|
|
4e9583ef54 | ||
|
|
cc95d5df6e | ||
|
|
4ca156fd83 | ||
|
|
9f6da8eb79 | ||
|
|
65a22813a3 | ||
| fdbf271432 | |||
| b26eedc633 | |||
| 793970beaf | |||
|
|
049d9170be | ||
|
|
fd10c5672a | ||
|
|
37bd9447f0 | ||
|
|
e8457d7486 | ||
|
|
280297ad35 | ||
|
|
7da3ead01e | ||
| 3ddb2625e9 | |||
|
|
f53ffae767 | ||
|
|
b9168f9914 | ||
|
|
63ff2b6f9e | ||
|
|
7d9468388b | ||
|
|
66b555e0ff
|
||
|
|
8df332472c
|
||
|
|
6072668438
|
||
|
|
6f26ddf7ac
|
||
|
df156df6d9
|
|||
|
|
11c367b541
|
||
|
|
4e1b23d1cb
|
||
|
|
2de3083dad
|
||
|
93149642db
|
|||
|
|
0b0d422b7a
|
||
|
|
036ea50a3a
|
||
|
|
073feccbbf | ||
|
|
eeea9d3266 | ||
|
|
b8bf5df7bc | ||
|
|
e9e68422d4 | ||
|
|
6f9a00d728 | ||
|
|
51e07df1b5 | ||
|
|
2a42723b81 | ||
| 839ef6a80d | |||
| c073dd8fea | |||
|
|
8d9f728cf0 | ||
| 2c62741e25 | |||
|
|
1f612f7fde | ||
|
|
0e9e102d0f | ||
|
|
b94e8765a1 | ||
|
|
53964f5c1a | ||
| bd574d93c3 | |||
| 47514ace79 | |||
|
|
298b43733f | ||
|
|
02116c0af5 | ||
| 92121e3b2d | |||
|
|
c92094823e | ||
|
|
f4b1a504a5 | ||
| 99ae7de5eb | |||
| b3d9ee3fc0 | |||
| e65219ee3e | |||
|
|
414c67a919 | ||
| f436291209 | |||
| a9196a39df | |||
|
|
6a8ee9c360 | ||
|
947e24864e
|
|||
|
b9198d6bd7
|
|||
|
|
14bf187a6e | ||
|
|
c996e5f8b3 | ||
|
b6dad349c9
|
|||
|
|
56dde30cf6
|
||
|
|
95bfbae131
|
||
|
|
3da0ff7ecc
|
||
|
|
b8f846ded8
|
||
|
|
e74c45ad39
|
||
|
|
e6a03522c6
|
||
|
|
dbc7d79ecd
|
||
|
|
d2b5a65eca
|
||
|
|
16b19d3a96
|
||
|
|
70edb8d7c5
|
||
|
ea04ebe95c
|
|||
|
|
44cf47faa4
|
||
|
612abfd862
|
|||
|
20af086273
|
|||
|
|
e9c1671d06 | ||
|
|
d02847d466 | ||
|
|
580fa954b2 | ||
|
|
aef516ae9f | ||
|
|
eb4e3b692b | ||
|
|
fe52381d63 | ||
|
|
ab8d52e685 | ||
|
|
1d32200ae3 | ||
|
|
309b00380d | ||
|
|
7fa2118480 | ||
|
|
1a6c17e308 | ||
|
|
82a6046620 | ||
|
|
241755c8c4 | ||
|
|
b26f66f15c | ||
|
|
28bd0c81e8 | ||
| 0bd1814877 | |||
| ee94f67b94 | |||
|
|
3a25075473 | ||
|
|
d16ff8f78f | ||
|
|
38dc90cb33 | ||
|
|
52bbc698b2 | ||
|
|
496a11f597 | ||
| 4a8a0ea1bd | |||
|
|
c424d4da99 | ||
|
|
69d5fc1553 | ||
| bcb59896db | |||
| e1e6d9eb3d | |||
| f1fdae5957 | |||
|
|
f96647fa40 | ||
|
|
5ea522d306 | ||
|
|
54d6161acd | ||
|
|
b1fd84fd75 | ||
|
|
9dbdf7928a | ||
|
|
67f0e3d296 | ||
|
|
e498418c2d | ||
| 33150a42c5 | |||
| e7fe4ab9b4 | |||
| c146bab08a | |||
|
|
d1cced8d54 | ||
|
|
8849b6105c | ||
|
|
3a0acfaba1 | ||
|
|
0ec2b05070 | ||
|
|
130bbfafb4 | ||
|
|
ffc75772f9 | ||
|
|
5b3fac70ed | ||
|
|
53e3f6d86b | ||
|
|
c28ab7a57c | ||
|
|
09ce3af11e | ||
| e42c09883a | |||
| 77e3924809 | |||
| 3511b1ee91 | |||
| 78a62c8ef0 | |||
|
|
8b96b9f4e6 | ||
|
|
649a857c3a | ||
|
|
cdae2c7558 | ||
|
|
3639110c51 | ||
|
186668512e
|
|||
|
f63666fae2
|
|||
|
|
68d25059b1
|
||
|
|
9aef6b7f5b
|
||
|
|
d2e712575f
|
||
|
|
bf9674e6e4
|
||
|
|
4815390cbe
|
||
|
|
6ce903f1f6
|
||
|
|
b2c91ffce4
|
||
|
|
ae335b18bf
|
||
|
|
6391819fb2
|
||
|
|
5d0e56b7c7
|
||
|
|
50ccc7bd7f
|
||
|
|
b3a6bcf3b2
|
||
|
|
38b2988bbe
|
||
|
|
446c541dcb
|
||
|
|
31fd48ee52
|
||
| b35cc33c32 | |||
|
|
9510290c29 | ||
|
|
3b1238b9c7 | ||
|
|
3bec23ecac | ||
|
|
7b678228b6 | ||
|
|
b1292d4562 | ||
|
|
a62d782fe5 | ||
|
|
81b07eb339 | ||
|
|
02f88398b9 | ||
|
|
e80961cc09 | ||
|
|
bd7721dc26 | ||
|
|
6d974bf71c | ||
|
|
aeeb817735 | ||
|
|
b5e7033958 | ||
|
|
bdc843f30f | ||
|
|
a823fa8e14 | ||
|
|
9232386c15 | ||
|
|
3c1547718c | ||
|
|
b67a7f3e9e | ||
|
|
92850d4f64 | ||
|
|
6323eafd7e | ||
|
|
b8fe826b58 | ||
|
|
841c49238f | ||
|
|
7ab612e3d9 | ||
|
|
6d8a27688f | ||
|
|
765385319a | ||
|
|
342c49a3e5 | ||
|
|
25860e7bb2 | ||
|
|
6962f2b462 | ||
|
e48ce4c6c5
|
|||
|
1cb311cc2c
|
|||
|
|
401846abe4
|
||
|
|
16ef393350
|
||
|
|
d5742f8e4c
|
||
|
|
319063f823
|
||
|
|
5b13cf5634
|
||
|
|
da10b908b3
|
||
|
|
4568935bc5
|
||
|
|
467404a55e
|
||
|
|
fcfe1e4558
|
||
|
|
c9d87a1b9a
|
||
|
|
35ebf4dfc2
|
||
|
|
bc3c256d22
|
||
|
|
0e10e74496
|
||
|
|
ebe9097f73
|
||
|
|
d61a11b647
|
||
|
|
d980cc1f8e
|
||
|
|
bd6056ce2e
|
||
|
|
cf48fda8d0
|
||
|
|
dc344cd28c
|
||
|
|
358610575f
|
||
|
|
a7869fccbb
|
||
|
|
a50903f90a
|
||
|
|
9243705995
|
||
|
|
db4dd9eee9
|
||
|
|
22f2aba969 | ||
|
|
98f2777fda | ||
|
|
102ce43216 | ||
|
|
0c148c8a1f | ||
|
|
3cccb2eb6b | ||
|
|
af4949e26a | ||
|
|
5bb7e95624 | ||
| 814bcf694f | |||
|
|
b0382c61b1 | ||
| e2650a8bfc | |||
|
|
ac39a53b33 | ||
|
|
fb356cdf0b | ||
|
|
238e89ce16 | ||
|
|
6e041c79f7 | ||
|
|
6ef4b60d14 | ||
|
|
054bec2d9a
|
||
|
|
943a46a343
|
||
|
|
17381f6b94
|
||
|
|
18c88de407
|
||
|
|
99d21fc89b
|
||
|
|
db5c86a0d1
|
||
|
|
736ec6fb9e
|
||
|
|
fa2327325a
|
||
|
|
4fdf048040
|
||
|
|
273538bd36
|
||
|
|
0980c8c040
|
||
|
|
f0bfdeaa5a
|
||
|
|
ab7c5c18e3
|
||
|
|
6ae95ab5ec
|
||
|
eec630b2b0
|
|||
|
|
2b3d86968d | ||
|
|
935a6cae7a | ||
|
|
d4940d8386 | ||
|
71ec18f6c6
|
|||
|
caa4bfe864
|
|||
|
a87ba73160
|
|||
|
|
4324b185fe | ||
|
|
1ab9b30b85 | ||
|
|
81cf6ad297 | ||
|
|
1b3be3a13b | ||
|
|
3a2ce04d6b | ||
|
|
981821a6bc | ||
|
|
98f83769bd | ||
|
|
7684f53281 | ||
|
|
15af686a58 | ||
|
|
aad8f9e8d4 | ||
| b2ee44c0ab |
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -6,6 +6,7 @@ _[Please provide a summary of the changes in this PR.]_
|
||||
|
||||
- [ ] I have read (or I am familiar with) the [Contribution Guidelines](../docs/CONTRIBUTING.md)
|
||||
- [ ] I have tested the changes in this PR
|
||||
- [ ] I have opened or referred to an existing github issue related to this change.
|
||||
- [ ] My PR is either small, or I have split it into smaller logical commits that are easier to review
|
||||
- [ ] I have added the signoff line to all my commits. See [Signing off your work](../docs/CONTRIBUTING.md#sign-your-work---the-developers-certificate-of-origin)
|
||||
- [ ] I have added appropriate changelog entries for the changes in this PR. See [Adding changelog entries](../docs/CONTRIBUTING.md#add-changelog-changed-changelog-fixed-etc)
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ damus.xcodeproj/xcshareddata/xcbaselines
|
||||
TODO.bak
|
||||
tags
|
||||
build-git-hash.txt
|
||||
.build
|
||||
|
||||
107
CHANGELOG.md
107
CHANGELOG.md
@@ -1,3 +1,110 @@
|
||||
## [1.14] - 2025-05-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added safety reminder to wallets with higher balance (Daniel D’Aquino)
|
||||
- Added one-click Coinos wallet setup (Daniel D’Aquino)
|
||||
- Add notification setting to hide hellthreads (Terry Yiu)
|
||||
- Added separated first aid option for relay lists that does not need a contact list reset (Daniel D’Aquino)
|
||||
- Added NIP-65 relay list support (Daniel D’Aquino)
|
||||
- Added Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker (Terry Yiu)
|
||||
- Added a search interface to the settings screen (SanjaySiddharth)
|
||||
- Added view introducing users to Zaps (ericholguin)
|
||||
- Added new wallet view with balance and transactions list (ericholguin)
|
||||
- Added copy technical info button to user visible errors, so that users can more easily share errors with developers (Daniel D’Aquino)
|
||||
- Add dismiss button to wallet high balance reminders (Daniel D’Aquino)
|
||||
- Zap receiver information now included for outgoing zaps (Daniel D’Aquino)
|
||||
- Added inline note rendering of invoices to pull up wallet selector sheet (Terry Yiu)
|
||||
- Added route to profile page from wallet tx list (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Added additional information on top of blurred images (SanjaySiddharth)
|
||||
- Improved robustness of relay list handling (Daniel D’Aquino)
|
||||
- Updated image cache for better stability (Daniel D’Aquino)
|
||||
- Improved integration with Nostr Wallet Connect wallets (ericholguin)
|
||||
- Added relay connectivity information to NWC settings (Daniel D’Aquino)
|
||||
- Improved handling around NWC responses (Daniel D’Aquino)
|
||||
- Added more human visible errors on NWC wallets to aid with troubleshooting (Daniel D’Aquino)
|
||||
- Re-enabled note zaps as permitted by the new App Store guidelines (Daniel D’Aquino)
|
||||
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Hide future notes from timeline (Terry Yiu)
|
||||
- Fixed issue where profiles with a NIP-65 relay list would not display on Damus (Daniel D’Aquino)
|
||||
- Fix quote notes to include missing q tag (Terry Yiu)
|
||||
- Fixed issue where the side menu would close when copying the npub (SanjaySiddharth)
|
||||
- Fixed issue where cached images would be backed up to iCloud (Daniel D’Aquino)
|
||||
- Optimized classify_url function (Terry Yiu)
|
||||
- Fixed note rendering for those that contain previewable items or leading and trailing whitespaces (Terry Yiu)
|
||||
- Fixed issue where some videos would become unplayable after some time using the app (Daniel D’Aquino)
|
||||
|
||||
|
||||
[1.14]: https://github.com/damus-io/damus/releases/tag/v1.14
|
||||
|
||||
|
||||
## [1.13.1] - 2025-03-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where threads would not load properly (Daniel D’Aquino)
|
||||
|
||||
|
||||
[1.13.1]: https://github.com/damus-io/damus/releases/tag/v1.13.1
|
||||
|
||||
|
||||
## [1.13] - 2025-03-14
|
||||
|
||||
### Added
|
||||
|
||||
- Added local persistence of note drafts (Daniel D’Aquino)
|
||||
- Added user-friendly error view for errors around the app that would not fit in other places (Daniel D’Aquino)
|
||||
- Coinos connection button in Wallet view (ericholguin)
|
||||
- Added Alby Go to mobile wallets selection menu (Tomek ⚡ K)
|
||||
- Minor accessibility improvements around picture editing and onboarding (Daniel D’Aquino)
|
||||
- Profile image cropping tools (Daniel D’Aquino)
|
||||
- Added Conversations tab to profiles (Terry Yiu)
|
||||
- Added profile pictures to push notifications (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Don't show reposts for the same note more than once in your home feed (William Casarin)
|
||||
- Improved profile image bandwidth optimization (Daniel D’Aquino)
|
||||
- Improved reliability of picture selector (Daniel D’Aquino)
|
||||
- Changed spaces to newlines in new posts to provide cleaner separation between text, uploaded media, and quoted notes (Terry Yiu)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where some push notifications would not open in the app and leave users confused (Daniel D’Aquino)
|
||||
- Fixed issue where app would need a restart for new NWC wallets to work (Daniel D’Aquino)
|
||||
- Fixed overly sensitive horizontal swipe on thread chat view (Daniel D’Aquino)
|
||||
- Trim whitespaces from Lightning addresses (Terry Yiu)
|
||||
- Fixed translation export script by upgrading nostr-sdk-swift dependency to support Mac Catalyst (Terry Yiu)
|
||||
- Fixed issue where users continue to receive push notifications after logout (Daniel D’Aquino)
|
||||
- Fixed an issue where events on a thread view would occasionally disappear (Daniel D’Aquino)
|
||||
- Improved robustness of the URL handler (Daniel D’Aquino)
|
||||
- Translate notes even if they are in a preferred language but not the current language as that is what users expect (Terry Yiu)
|
||||
- Cancel ongoing uploading operations after the user cancels the post (Swift Coder)
|
||||
- Fixed link and photo sharing support on macOS (Swift Coder)
|
||||
- Fix bug where profile view was showing more than just the notes and replies on the notes / notes & replies tabs (Terry Yiu)
|
||||
- Fixed reposts banner to be localizable (Terry Yiu)
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed language filtering from Universe feed because language detection can be inaccurate (Terry Yiu)
|
||||
- Removed mystery tabs meant to fix tab switching bug that no longer exists (Terry Yiu)
|
||||
|
||||
|
||||
|
||||
[1.13](https://github.com/damus-io/damus/releases/tag/v1.13): https://github.com/damus-io/damus/releases/tag/v1.13
|
||||
|
||||
|
||||
## [1.12.3] - 2025-02-06
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
|
||||
@@ -18,7 +18,7 @@ struct NotificationExtensionState: HeadlessDamusState {
|
||||
let lnurls: LNUrls
|
||||
|
||||
init?() {
|
||||
guard let ndb = try? Ndb(owns_db_file: false) else { return nil }
|
||||
guard let ndb = Ndb(owns_db_file: false) else { return nil }
|
||||
self.ndb = ndb
|
||||
|
||||
guard let keypair = get_saved_keypair() else { return nil }
|
||||
|
||||
@@ -103,7 +103,7 @@ struct NotificationFormatter {
|
||||
content.title = Self.zap_notification_title(zap)
|
||||
content.body = Self.zap_notification_body(profiles: state.profiles, zap: zap)
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = LossyLocalNotification(type: .zap, mention: .note(notify.event.id)).to_user_info()
|
||||
content.userInfo = LossyLocalNotification(type: .zap, mention: .init(nip19: .note(notify.event.id))).to_user_info()
|
||||
return (content, "myZapNotification")
|
||||
default:
|
||||
// The sync method should have taken care of this.
|
||||
|
||||
@@ -5,15 +5,32 @@
|
||||
// Created by Daniel D’Aquino on 2023-11-10.
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import ImageIO
|
||||
import UserNotifications
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
import Intents
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
private func configureKingfisherCache() {
|
||||
guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else {
|
||||
return
|
||||
}
|
||||
|
||||
let cachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME)
|
||||
if let cache = try? ImageCache(name: "sharedCache", cacheDirectoryURL: cachePath) {
|
||||
KingfisherManager.shared.cache = cache
|
||||
}
|
||||
}
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
configureKingfisherCache()
|
||||
|
||||
self.contentHandler = contentHandler
|
||||
|
||||
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
|
||||
@@ -40,9 +57,16 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
return
|
||||
}
|
||||
|
||||
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
|
||||
let profile = txn?.unsafeUnownedValue?.profile
|
||||
let name = Profile.displayName(profile: profile, pubkey: nostr_event.pubkey).displayName
|
||||
let sender_profile = {
|
||||
let txn = state.ndb.lookup_profile(nostr_event.pubkey)
|
||||
let profile = txn?.unsafeUnownedValue?.profile
|
||||
let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))!
|
||||
return ProfileBuf(picture: picture,
|
||||
name: profile?.name,
|
||||
display_name: profile?.display_name,
|
||||
nip05: profile?.nip05)
|
||||
}()
|
||||
let sender_pubkey = nostr_event.pubkey
|
||||
|
||||
// Don't show notification details that match mute list.
|
||||
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
||||
@@ -56,7 +80,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
||||
Log.debug("should_display_notification failed", for: .push_notifications)
|
||||
// We should not display notification for this event. Suppress notification.
|
||||
@@ -65,8 +89,8 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
|
||||
|
||||
guard let notification_object = generate_local_notification_object(ndb: state.ndb, from: nostr_event, state: state) else {
|
||||
Log.debug("generate_local_notification_object failed", for: .push_notifications)
|
||||
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
||||
// contentHandler(UNNotificationContent())
|
||||
@@ -74,15 +98,58 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
Task {
|
||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: name, notify: notification_object, state: state) else {
|
||||
let sender_dn = DisplayName(name: sender_profile.name, display_name: sender_profile.display_name, pubkey: sender_pubkey)
|
||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: sender_dn.displayName, notify: notification_object, state: state) else {
|
||||
|
||||
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
|
||||
contentHandler(improvedContent)
|
||||
do {
|
||||
var options: [AnyHashable: Any] = [:]
|
||||
if let imageSource = CGImageSourceCreateWithURL(sender_profile.picture as CFURL, nil),
|
||||
let uti = CGImageSourceGetType(imageSource) {
|
||||
options[UNNotificationAttachmentOptionsTypeHintKey] = uti
|
||||
}
|
||||
|
||||
let attachment = try UNNotificationAttachment(identifier: sender_profile.picture.absoluteString, url: sender_profile.picture, options: options)
|
||||
improvedContent.attachments = [attachment]
|
||||
} catch {
|
||||
Log.error("failed to get notification attachment: %s", for: .push_notifications, error.localizedDescription)
|
||||
}
|
||||
|
||||
let kind = nostr_event.known_kind
|
||||
|
||||
// these aren't supported yet
|
||||
if !(kind == .text || kind == .dm) {
|
||||
contentHandler(improvedContent)
|
||||
return
|
||||
}
|
||||
|
||||
// rich communication notifications for kind1, dms, etc
|
||||
|
||||
let message_intent = await message_intent_from_note(ndb: state.ndb,
|
||||
sender_profile: sender_profile,
|
||||
content: improvedContent.body,
|
||||
note: nostr_event,
|
||||
our_pubkey: state.keypair.pubkey)
|
||||
|
||||
improvedContent.threadIdentifier = nostr_event.thread_id().hex()
|
||||
improvedContent.categoryIdentifier = "COMMUNICATION"
|
||||
|
||||
let interaction = INInteraction(intent: message_intent, response: nil)
|
||||
interaction.direction = .incoming
|
||||
do {
|
||||
try await interaction.donate()
|
||||
let updated = try improvedContent.updating(from: message_intent)
|
||||
contentHandler(updated)
|
||||
} catch {
|
||||
Log.error("failed to donate interaction: %s", for: .push_notifications, error.localizedDescription)
|
||||
contentHandler(improvedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,3 +162,162 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ProfileBuf {
|
||||
let picture: URL
|
||||
let name: String?
|
||||
let display_name: String?
|
||||
let nip05: String?
|
||||
}
|
||||
|
||||
func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: String, note: NdbNote, our_pubkey: Pubkey) async -> INSendMessageIntent {
|
||||
let sender_pk = note.pubkey
|
||||
let sender = await profile_to_inperson(name: sender_profile.name,
|
||||
display_name: sender_profile.display_name,
|
||||
picture: sender_profile.picture.absoluteString,
|
||||
nip05: sender_profile.nip05,
|
||||
pubkey: sender_pk,
|
||||
our_pubkey: our_pubkey)
|
||||
|
||||
let conversationIdentifier = note.thread_id().hex()
|
||||
var recipients: [INPerson] = []
|
||||
var pks: [Pubkey] = []
|
||||
let meta = INSendMessageIntentDonationMetadata()
|
||||
|
||||
// gather recipients
|
||||
if let recipient_note_id = note.direct_replies() {
|
||||
let replying_to = ndb.lookup_note(recipient_note_id)
|
||||
if let replying_to_pk = replying_to?.unsafeUnownedValue?.pubkey {
|
||||
meta.isReplyToCurrentUser = replying_to_pk == our_pubkey
|
||||
|
||||
if replying_to_pk != sender_pk {
|
||||
// we push the actual person being replied to first
|
||||
pks.append(replying_to_pk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pubkeys = Array(note.referenced_pubkeys)
|
||||
meta.recipientCount = pubkeys.count
|
||||
if pubkeys.contains(sender_pk) {
|
||||
meta.recipientCount -= 1
|
||||
}
|
||||
|
||||
for pk in pubkeys.prefix(3) {
|
||||
if pk == sender_pk || pks.contains(pk) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !meta.isReplyToCurrentUser && pk == our_pubkey {
|
||||
meta.mentionsCurrentUser = true
|
||||
}
|
||||
|
||||
pks.append(pk)
|
||||
}
|
||||
|
||||
for pk in pks {
|
||||
let recipient = await pubkey_to_inperson(ndb: ndb, pubkey: pk, our_pubkey: our_pubkey)
|
||||
recipients.append(recipient)
|
||||
}
|
||||
|
||||
// we enable default formatting this way
|
||||
var groupName = INSpeakableString(spokenPhrase: "")
|
||||
|
||||
// otherwise we just say its a DM
|
||||
if note.known_kind == .dm {
|
||||
groupName = INSpeakableString(spokenPhrase: "DM")
|
||||
}
|
||||
|
||||
let intent = INSendMessageIntent(recipients: recipients,
|
||||
outgoingMessageType: .outgoingMessageText,
|
||||
content: content,
|
||||
speakableGroupName: groupName,
|
||||
conversationIdentifier: conversationIdentifier,
|
||||
serviceName: "kind\(note.kind)",
|
||||
sender: sender,
|
||||
attachments: nil)
|
||||
intent.donationMetadata = meta
|
||||
|
||||
// this is needed for recipients > 0
|
||||
if let img = sender.image {
|
||||
intent.setImage(img, forParameterNamed: \.speakableGroupName)
|
||||
}
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||
let profile_txn = ndb.lookup_profile(pubkey)
|
||||
let profile = profile_txn?.unsafeUnownedValue?.profile
|
||||
let name = profile?.name
|
||||
let display_name = profile?.display_name
|
||||
let nip05 = profile?.nip05
|
||||
let picture = profile?.picture
|
||||
|
||||
return await profile_to_inperson(name: name,
|
||||
display_name: display_name,
|
||||
picture: picture,
|
||||
nip05: nip05,
|
||||
pubkey: pubkey,
|
||||
our_pubkey: our_pubkey)
|
||||
}
|
||||
|
||||
func fetch_pfp(picture: URL) async throws -> RetrieveImageResult {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: picture)) { result in
|
||||
switch result {
|
||||
case .success(let img):
|
||||
continuation.resume(returning: img)
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func profile_to_inperson(name: String?, display_name: String?, picture: String?, nip05: String?, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
|
||||
let npub = pubkey.npub
|
||||
let handle = INPersonHandle(value: npub, type: .unknown)
|
||||
var aliases: [INPersonHandle] = []
|
||||
|
||||
if let nip05 {
|
||||
aliases.append(INPersonHandle(value: nip05, type: .emailAddress))
|
||||
}
|
||||
|
||||
let nostrName = DisplayName(name: name, display_name: display_name, pubkey: pubkey)
|
||||
let nameComponents = nostrName.nameComponents()
|
||||
let displayName = nostrName.displayName
|
||||
let contactIdentifier = npub
|
||||
let customIdentifier = npub
|
||||
let suggestionType = INPersonSuggestionType.socialProfile
|
||||
|
||||
var image: INImage? = nil
|
||||
|
||||
if let picture,
|
||||
let url = URL(string: picture),
|
||||
let img = try? await fetch_pfp(picture: url),
|
||||
let imgdata = img.data()
|
||||
{
|
||||
image = INImage(imageData: imgdata)
|
||||
} else {
|
||||
Log.error("Failed to fetch pfp (%s) for %s", for: .push_notifications, picture ?? "nil", displayName)
|
||||
}
|
||||
|
||||
let person = INPerson(personHandle: handle,
|
||||
nameComponents: nameComponents,
|
||||
displayName: displayName,
|
||||
image: image,
|
||||
contactIdentifier: contactIdentifier,
|
||||
customIdentifier: customIdentifier,
|
||||
isMe: pubkey == our_pubkey,
|
||||
suggestionType: suggestionType
|
||||
)
|
||||
|
||||
return person
|
||||
}
|
||||
|
||||
func robohash(_ pk: Pubkey) -> String {
|
||||
return "https://robohash.org/" + pk.hex()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
||||
]
|
||||
// swift-tools-version: 6.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "damus",
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "damus",
|
||||
targets: ["damus"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "damus",
|
||||
dependencies: [
|
||||
.product(name: "secp256k1", package: "secp256k1.swift")
|
||||
],
|
||||
path: "damus"),
|
||||
.testTarget(
|
||||
name: "damusTests",
|
||||
dependencies: ["damus"],
|
||||
path: "damusTests"),
|
||||
]
|
||||
)
|
||||
|
||||
22
README.md
22
README.md
@@ -1,10 +1,26 @@
|
||||
[](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml)
|
||||
<div align="center">
|
||||
|
||||
# damus
|
||||
<img src="./damus/Assets.xcassets/damus-home.imageset/damus-home@2x.png" alt="Damus Logo" title="Damus logo" width=""/>
|
||||
|
||||
# Damus
|
||||
|
||||
The social network you control
|
||||
|
||||
A twitter-like [nostr][nostr] client for iPhone, iPad and MacOS.
|
||||
|
||||
<img src="./ss.png" width="50%" height="50%" />
|
||||
[](/LICENSE)
|
||||
|
||||
## Download and Install
|
||||
|
||||
[](https://apps.apple.com/us/app/damus/id1628663131)
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
iOS 16.0+ • macOS 13.0+
|
||||
|
||||
<img src="./demo1.png" width="70%" height="50%" />
|
||||
|
||||
</div>
|
||||
|
||||
[nostr]: https://github.com/fiatjaf/nostr
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// block.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef block_h
|
||||
#define block_h
|
||||
|
||||
#include "nostr_bech32.h"
|
||||
#include "str_block.h"
|
||||
|
||||
#define MAX_BLOCKS 1024
|
||||
|
||||
enum block_type {
|
||||
BLOCK_HASHTAG = 1,
|
||||
BLOCK_TEXT = 2,
|
||||
BLOCK_MENTION_INDEX = 3,
|
||||
BLOCK_MENTION_BECH32 = 4,
|
||||
BLOCK_URL = 5,
|
||||
BLOCK_INVOICE = 6,
|
||||
};
|
||||
|
||||
|
||||
typedef struct invoice_block {
|
||||
struct str_block invstr;
|
||||
union {
|
||||
struct bolt11 *bolt11;
|
||||
};
|
||||
} invoice_block_t;
|
||||
|
||||
typedef struct mention_bech32_block {
|
||||
struct str_block str;
|
||||
struct nostr_bech32 bech32;
|
||||
} mention_bech32_block_t;
|
||||
|
||||
typedef struct note_block {
|
||||
enum block_type type;
|
||||
union {
|
||||
struct str_block str;
|
||||
struct invoice_block invoice;
|
||||
struct mention_bech32_block mention_bech32;
|
||||
int mention_index;
|
||||
} block;
|
||||
} block_t;
|
||||
|
||||
typedef struct note_blocks {
|
||||
int words;
|
||||
int num_blocks;
|
||||
struct note_block *blocks;
|
||||
} blocks_t;
|
||||
|
||||
void blocks_init(struct note_blocks *blocks);
|
||||
void blocks_free(struct note_blocks *blocks);
|
||||
|
||||
#endif /* block_h */
|
||||
@@ -2,7 +2,6 @@
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#include "damus.h"
|
||||
#include "bolt11.h"
|
||||
#include "amount.h"
|
||||
#include "nostr_bech32.h"
|
||||
|
||||
393
damus-c/damus.c
393
damus-c/damus.c
@@ -1,393 +0,0 @@
|
||||
//
|
||||
// damus.c
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-10-17.
|
||||
//
|
||||
|
||||
#include "damus.h"
|
||||
#include "cursor.h"
|
||||
#include "bolt11.h"
|
||||
#include "bech32.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int parse_digit(struct cursor *cur, int *digit) {
|
||||
int c;
|
||||
if ((c = peek_char(cur, 0)) == -1)
|
||||
return 0;
|
||||
|
||||
c -= '0';
|
||||
|
||||
if (c >= 0 && c <= 9) {
|
||||
*digit = c;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int parse_mention_index(struct cursor *cur, struct note_block *block) {
|
||||
int d1, d2, d3, ind;
|
||||
u8 *start = cur->p;
|
||||
|
||||
if (!parse_str(cur, "#["))
|
||||
return 0;
|
||||
|
||||
if (!parse_digit(cur, &d1)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
ind = d1;
|
||||
|
||||
if (parse_digit(cur, &d2))
|
||||
ind = (d1 * 10) + d2;
|
||||
|
||||
if (parse_digit(cur, &d3))
|
||||
ind = (d1 * 100) + (d2 * 10) + d3;
|
||||
|
||||
if (!parse_char(cur, ']')) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
block->type = BLOCK_MENTION_INDEX;
|
||||
block->block.mention_index = ind;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_hashtag(struct cursor *cur, struct note_block *block) {
|
||||
int c;
|
||||
u8 *start = cur->p;
|
||||
|
||||
if (!parse_char(cur, '#'))
|
||||
return 0;
|
||||
|
||||
c = peek_char(cur, 0);
|
||||
if (c == -1 || is_whitespace(c) || c == '#') {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
consume_until_boundary(cur);
|
||||
|
||||
block->type = BLOCK_HASHTAG;
|
||||
block->block.str.start = (const char*)(start + 1);
|
||||
block->block.str.end = (const char*)cur->p;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_block(struct note_blocks *blocks, struct note_block block)
|
||||
{
|
||||
if (blocks->num_blocks + 1 >= MAX_BLOCKS)
|
||||
return 0;
|
||||
|
||||
blocks->blocks[blocks->num_blocks++] = block;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_text_block(struct note_blocks *blocks, const u8 *start, const u8 *end)
|
||||
{
|
||||
struct note_block b;
|
||||
|
||||
if (start == end)
|
||||
return 1;
|
||||
|
||||
b.type = BLOCK_TEXT;
|
||||
b.block.str.start = (const char*)start;
|
||||
b.block.str.end = (const char*)end;
|
||||
|
||||
return add_block(blocks, b);
|
||||
}
|
||||
|
||||
static int consume_url_fragment(struct cursor *cur)
|
||||
{
|
||||
int c;
|
||||
|
||||
if ((c = peek_char(cur, 0)) < 0)
|
||||
return 1;
|
||||
|
||||
if (c != '#' && c != '?') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
cur->p++;
|
||||
|
||||
return consume_until_end_url(cur, 1);
|
||||
}
|
||||
|
||||
static int consume_url_path(struct cursor *cur)
|
||||
{
|
||||
int c;
|
||||
|
||||
if ((c = peek_char(cur, 0)) < 0)
|
||||
return 1;
|
||||
|
||||
if (c != '/') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (c == '?' || c == '#' || is_final_url_char(cur->p, cur->end)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
cur->p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int consume_url_host(struct cursor *cur)
|
||||
{
|
||||
char c;
|
||||
int count = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
// TODO: handle IDNs
|
||||
if ((is_alphanumeric(c) || c == '.' || c == '-') && !is_final_url_char(cur->p, cur->end))
|
||||
{
|
||||
count++;
|
||||
cur->p++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return count != 0;
|
||||
}
|
||||
|
||||
|
||||
// this means the end of the URL hostname is the end of the buffer and we finished
|
||||
return count != 0;
|
||||
}
|
||||
|
||||
static int parse_url(struct cursor *cur, struct note_block *block) {
|
||||
u8 *start = cur->p;
|
||||
u8 *host;
|
||||
int host_len;
|
||||
struct cursor path_cur;
|
||||
|
||||
if (!parse_str(cur, "http"))
|
||||
return 0;
|
||||
|
||||
if (parse_char(cur, 's') || parse_char(cur, 'S')) {
|
||||
if (!parse_str(cur, "://")) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
if (!parse_str(cur, "://")) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure to save the hostname. We will use this to detect damus.io links
|
||||
host = cur->p;
|
||||
|
||||
if (!consume_url_host(cur)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// get the length of the host string
|
||||
host_len = (int)(cur->p - host);
|
||||
|
||||
// save the current parse state so that we can continue from here when
|
||||
// parsing the bech32 in the damus.io link if we have it
|
||||
copy_cursor(cur, &path_cur);
|
||||
|
||||
// skip leading /
|
||||
cursor_skip(&path_cur, 1);
|
||||
|
||||
if (!consume_url_path(cur)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!consume_url_fragment(cur)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// smart parens
|
||||
if (start - 1 >= 0 &&
|
||||
start < cur->end &&
|
||||
*(start - 1) == '(' &&
|
||||
(cur->p - 1) < cur->end &&
|
||||
*(cur->p - 1) == ')')
|
||||
{
|
||||
cur->p--;
|
||||
}
|
||||
|
||||
// save the bech32 string pos in case we hit a damus.io link
|
||||
block->block.str.start = (const char *)path_cur.p;
|
||||
|
||||
// if we have a damus link, make it a mention
|
||||
if (host_len == 8
|
||||
&& !strncmp((const char *)host, "damus.io", 8)
|
||||
&& parse_nostr_bech32(&path_cur, &block->block.mention_bech32.bech32))
|
||||
{
|
||||
block->block.str.end = (const char *)path_cur.p;
|
||||
block->type = BLOCK_MENTION_BECH32;
|
||||
return 1;
|
||||
}
|
||||
|
||||
block->type = BLOCK_URL;
|
||||
block->block.str.start = (const char *)start;
|
||||
block->block.str.end = (const char *)cur->p;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_invoice(struct cursor *cur, struct note_block *block) {
|
||||
u8 *start, *end;
|
||||
char *fail;
|
||||
struct bolt11 *bolt11;
|
||||
// optional
|
||||
parse_str(cur, "lightning:");
|
||||
|
||||
start = cur->p;
|
||||
|
||||
if (!parse_str(cur, "lnbc"))
|
||||
return 0;
|
||||
|
||||
if (!consume_until_whitespace(cur, 1)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
end = cur->p;
|
||||
|
||||
char str[end - start + 1];
|
||||
str[end - start] = 0;
|
||||
memcpy(str, start, end - start);
|
||||
|
||||
if (!(bolt11 = bolt11_decode(NULL, str, &fail))) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
block->type = BLOCK_INVOICE;
|
||||
|
||||
block->block.invoice.invstr.start = (const char*)start;
|
||||
block->block.invoice.invstr.end = (const char*)end;
|
||||
block->block.invoice.bolt11 = bolt11;
|
||||
|
||||
cur->p = end;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static int parse_mention_bech32(struct cursor *cur, struct note_block *block) {
|
||||
u8 *start = cur->p;
|
||||
|
||||
parse_char(cur, '@');
|
||||
parse_str(cur, "nostr:");
|
||||
|
||||
block->block.str.start = (const char *)cur->p;
|
||||
|
||||
if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
block->block.str.end = (const char *)cur->p;
|
||||
|
||||
block->type = BLOCK_MENTION_BECH32;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_text_then_block(struct cursor *cur, struct note_blocks *blocks, struct note_block block, u8 **start, const u8 *pre_mention)
|
||||
{
|
||||
if (!add_text_block(blocks, *start, pre_mention))
|
||||
return 0;
|
||||
|
||||
*start = (u8*)cur->p;
|
||||
|
||||
if (!add_block(blocks, block))
|
||||
return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int damus_parse_content(struct note_blocks *blocks, const char *content) {
|
||||
int cp, c;
|
||||
struct cursor cur;
|
||||
struct note_block block;
|
||||
u8 *start, *pre_mention;
|
||||
|
||||
blocks->words = 0;
|
||||
blocks->num_blocks = 0;
|
||||
make_cursor((u8*)content, (u8*)content + strlen(content), &cur);
|
||||
|
||||
start = cur.p;
|
||||
while (cur.p < cur.end && blocks->num_blocks < MAX_BLOCKS) {
|
||||
cp = peek_char(&cur, -1);
|
||||
c = peek_char(&cur, 0);
|
||||
|
||||
// new word
|
||||
if (is_whitespace(cp) && !is_whitespace(c)) {
|
||||
blocks->words++;
|
||||
}
|
||||
|
||||
pre_mention = cur.p;
|
||||
if (cp == -1 || is_left_boundary(cp) || c == '#') {
|
||||
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
} else if ((c == 'h' || c == 'H') && parse_url(&cur, &block)) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
} else if ((c == 'l' || c == 'L') && parse_invoice(&cur, &block)) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
} else if ((c == 'n' || c == '@') && parse_mention_bech32(&cur, &block)) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
cur.p++;
|
||||
}
|
||||
|
||||
if (cur.p - start > 0) {
|
||||
if (!add_text_block(blocks, start, cur.p))
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void blocks_init(struct note_blocks *blocks) {
|
||||
blocks->blocks = malloc(sizeof(struct note_block) * MAX_BLOCKS);
|
||||
blocks->num_blocks = 0;
|
||||
}
|
||||
|
||||
void blocks_free(struct note_blocks *blocks) {
|
||||
if (!blocks->blocks) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < blocks->num_blocks; ++i) {
|
||||
if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) {
|
||||
free(blocks->blocks[i].block.mention_bech32.bech32.buffer);
|
||||
blocks->blocks[i].block.mention_bech32.bech32.buffer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
free(blocks->blocks);
|
||||
blocks->num_blocks = 0;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// damus.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-10-17.
|
||||
//
|
||||
|
||||
#ifndef damus_h
|
||||
#define damus_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include "block.h"
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
int damus_parse_content(struct note_blocks *blocks, const char *content);
|
||||
|
||||
#endif /* damus_h */
|
||||
@@ -1,84 +0,0 @@
|
||||
/* CC0 (Public domain) - see LICENSE file for details */
|
||||
#ifndef CCAN_HEX_H
|
||||
#define CCAN_HEX_H
|
||||
#include "config.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/**
|
||||
* hex_decode - Unpack a hex string.
|
||||
* @str: the hexadecimal string
|
||||
* @slen: the length of @str
|
||||
* @buf: the buffer to write the data into
|
||||
* @bufsize: the length of
|
||||
*
|
||||
* Returns false if there are any characters which aren't 0-9, a-f or A-F,
|
||||
* of the string wasn't the right length for @bufsize.
|
||||
*
|
||||
* Example:
|
||||
* unsigned char data[20];
|
||||
*
|
||||
* if (!hex_decode(argv[1], strlen(argv[1]), data, 20))
|
||||
* printf("String is malformed!\n");
|
||||
*/
|
||||
bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize);
|
||||
|
||||
/**
|
||||
* hex_encode - Create a nul-terminated hex string
|
||||
* @buf: the buffer to read the data from
|
||||
* @bufsize: the length of buf
|
||||
* @dest: the string to fill
|
||||
* @destsize: the max size of the string
|
||||
*
|
||||
* Returns true if the string, including terminator, fit in @destsize;
|
||||
*
|
||||
* Example:
|
||||
* unsigned char buf[] = { 0x1F, 0x2F };
|
||||
* char str[5];
|
||||
*
|
||||
* if (!hex_encode(buf, sizeof(buf), str, sizeof(str)))
|
||||
* abort();
|
||||
*/
|
||||
bool hex_encode(const void *buf, size_t bufsize, char *dest, size_t destsize);
|
||||
|
||||
/**
|
||||
* hex_str_size - Calculate how big a nul-terminated hex string is
|
||||
* @bytes: bytes of data to represent
|
||||
*
|
||||
* Example:
|
||||
* unsigned char buf[] = { 0x1F, 0x2F };
|
||||
* char str[hex_str_size(sizeof(buf))];
|
||||
*
|
||||
* hex_encode(buf, sizeof(buf), str, sizeof(str));
|
||||
*/
|
||||
static inline size_t hex_str_size(size_t bytes)
|
||||
{
|
||||
return 2 * bytes + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* hex_data_size - Calculate how many bytes of data in a hex string
|
||||
* @strlen: the length of the string (with or without NUL)
|
||||
*
|
||||
* Example:
|
||||
* const char str[] = "1F2F";
|
||||
* unsigned char buf[hex_data_size(sizeof(str))];
|
||||
*
|
||||
* hex_decode(str, strlen(str), buf, sizeof(buf));
|
||||
*/
|
||||
static inline size_t hex_data_size(size_t strlen)
|
||||
{
|
||||
return strlen / 2;
|
||||
}
|
||||
|
||||
static inline char hexchar(unsigned int val)
|
||||
{
|
||||
if (val < 10)
|
||||
return '0' + val;
|
||||
if (val < 16)
|
||||
return 'a' + val - 10;
|
||||
abort();
|
||||
}
|
||||
|
||||
|
||||
#endif /* CCAN_HEX_H */
|
||||
@@ -1,325 +0,0 @@
|
||||
//
|
||||
// nostr_bech32.c
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#include "nostr_bech32.h"
|
||||
#include <stdlib.h>
|
||||
#include "endian.h"
|
||||
#include "cursor.h"
|
||||
#include "bech32.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
#define MAX_TLVS 16
|
||||
|
||||
#define TLV_SPECIAL 0
|
||||
#define TLV_RELAY 1
|
||||
#define TLV_AUTHOR 2
|
||||
#define TLV_KIND 3
|
||||
#define TLV_KNOWN_TLVS 4
|
||||
|
||||
struct nostr_tlv {
|
||||
u8 type;
|
||||
u8 len;
|
||||
const u8 *value;
|
||||
};
|
||||
|
||||
struct nostr_tlvs {
|
||||
struct nostr_tlv tlvs[MAX_TLVS];
|
||||
int num_tlvs;
|
||||
};
|
||||
|
||||
static int parse_nostr_tlv(struct cursor *cur, struct nostr_tlv *tlv) {
|
||||
// get the tlv tag
|
||||
if (!pull_byte(cur, &tlv->type))
|
||||
return 0;
|
||||
|
||||
// unknown, fail!
|
||||
if (tlv->type >= TLV_KNOWN_TLVS)
|
||||
return 0;
|
||||
|
||||
// get the length
|
||||
if (!pull_byte(cur, &tlv->len))
|
||||
return 0;
|
||||
|
||||
// is the reported length greater then our buffer? if so fail
|
||||
if (cur->p + tlv->len > cur->end)
|
||||
return 0;
|
||||
|
||||
tlv->value = cur->p;
|
||||
cur->p += tlv->len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_nostr_tlvs(struct cursor *cur, struct nostr_tlvs *tlvs) {
|
||||
int i;
|
||||
tlvs->num_tlvs = 0;
|
||||
|
||||
for (i = 0; i < MAX_TLVS; i++) {
|
||||
if (parse_nostr_tlv(cur, &tlvs->tlvs[i])) {
|
||||
tlvs->num_tlvs++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tlvs->num_tlvs == 0)
|
||||
return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int find_tlv(struct nostr_tlvs *tlvs, u8 type, struct nostr_tlv **tlv) {
|
||||
*tlv = NULL;
|
||||
|
||||
for (int i = 0; i < tlvs->num_tlvs; i++) {
|
||||
if (tlvs->tlvs[i].type == type) {
|
||||
*tlv = &tlvs->tlvs[i];
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *type) {
|
||||
// Parse type
|
||||
if (strcmp(prefix, "note") == 0) {
|
||||
*type = NOSTR_BECH32_NOTE;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "npub") == 0) {
|
||||
*type = NOSTR_BECH32_NPUB;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nsec") == 0) {
|
||||
*type = NOSTR_BECH32_NSEC;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nprofile") == 0) {
|
||||
*type = NOSTR_BECH32_NPROFILE;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nevent") == 0) {
|
||||
*type = NOSTR_BECH32_NEVENT;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nrelay") == 0) {
|
||||
*type = NOSTR_BECH32_NRELAY;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "naddr") == 0) {
|
||||
*type = NOSTR_BECH32_NADDR;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_note(struct cursor *cur, struct bech32_note *note) {
|
||||
return pull_bytes(cur, 32, ¬e->event_id);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub) {
|
||||
return pull_bytes(cur, 32, &npub->pubkey);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nsec(struct cursor *cur, struct bech32_nsec *nsec) {
|
||||
return pull_bytes(cur, 32, &nsec->nsec);
|
||||
}
|
||||
|
||||
static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
|
||||
struct nostr_tlv *tlv;
|
||||
struct str_block *str;
|
||||
|
||||
relays->num_relays = 0;
|
||||
|
||||
for (int i = 0; i < tlvs->num_tlvs; i++) {
|
||||
tlv = &tlvs->tlvs[i];
|
||||
if (tlv->type != TLV_RELAY)
|
||||
continue;
|
||||
|
||||
if (relays->num_relays + 1 > MAX_RELAYS)
|
||||
break;
|
||||
|
||||
str = &relays->relays[relays->num_relays++];
|
||||
str->start = (const char*)tlv->value;
|
||||
str->end = (const char*)(tlv->value + tlv->len);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint32_t decode_tlv_u32(const uint8_t *bytes) {
|
||||
beint32_t *be32_bytes = (beint32_t*)bytes;
|
||||
return be32_to_cpu(*be32_bytes);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
if (tlv->len != 32)
|
||||
return 0;
|
||||
|
||||
nevent->event_id = tlv->value;
|
||||
|
||||
if (find_tlv(&tlvs, TLV_AUTHOR, &tlv)) {
|
||||
nevent->pubkey = tlv->value;
|
||||
} else {
|
||||
nevent->pubkey = NULL;
|
||||
}
|
||||
|
||||
if(find_tlv(&tlvs, TLV_KIND, &tlv)) {
|
||||
nevent->kind = decode_tlv_u32(tlv->value);
|
||||
nevent->has_kind = true;
|
||||
} else {
|
||||
nevent->has_kind = false;
|
||||
}
|
||||
|
||||
return tlvs_to_relays(&tlvs, &nevent->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_naddr(struct cursor *cur, struct bech32_naddr *naddr) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
naddr->identifier.start = (const char*)tlv->value;
|
||||
naddr->identifier.end = (const char*)tlv->value + tlv->len;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_AUTHOR, &tlv))
|
||||
return 0;
|
||||
|
||||
naddr->pubkey = tlv->value;
|
||||
|
||||
if(!find_tlv(&tlvs, TLV_KIND, &tlv)) {
|
||||
return 0;
|
||||
}
|
||||
naddr->kind = decode_tlv_u32(tlv->value);
|
||||
|
||||
return tlvs_to_relays(&tlvs, &naddr->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nprofile(struct cursor *cur, struct bech32_nprofile *nprofile) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
if (tlv->len != 32)
|
||||
return 0;
|
||||
|
||||
nprofile->pubkey = tlv->value;
|
||||
|
||||
return tlvs_to_relays(&tlvs, &nprofile->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *nrelay) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
nrelay->relay.start = (const char*)tlv->value;
|
||||
nrelay->relay.end = (const char*)tlv->value + tlv->len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
|
||||
u8 *start, *end;
|
||||
|
||||
start = cur->p;
|
||||
|
||||
if (!consume_until_non_alphanumeric(cur, 1)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
end = cur->p;
|
||||
|
||||
size_t data_len;
|
||||
size_t input_len = end - start;
|
||||
if (input_len < 10 || input_len > 10000) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
obj->buffer = malloc(input_len * 2);
|
||||
if (!obj->buffer)
|
||||
return 0;
|
||||
|
||||
u8 data[input_len];
|
||||
char prefix[input_len];
|
||||
|
||||
if (bech32_decode_len(prefix, data, &data_len, (const char*)start, input_len) == BECH32_ENCODING_NONE) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
obj->buflen = 0;
|
||||
if (!bech32_convert_bits(obj->buffer, &obj->buflen, 8, data, data_len, 5, 0)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (!parse_nostr_bech32_type(prefix, &obj->type)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
struct cursor bcur;
|
||||
make_cursor(obj->buffer, obj->buffer + obj->buflen, &bcur);
|
||||
|
||||
switch (obj->type) {
|
||||
case NOSTR_BECH32_NOTE:
|
||||
if (!parse_nostr_bech32_note(&bcur, &obj->data.note))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NPUB:
|
||||
if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NSEC:
|
||||
if (!parse_nostr_bech32_nsec(&bcur, &obj->data.nsec))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NEVENT:
|
||||
if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NADDR:
|
||||
if (!parse_nostr_bech32_naddr(&bcur, &obj->data.naddr))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NPROFILE:
|
||||
if (!parse_nostr_bech32_nprofile(&bcur, &obj->data.nprofile))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NRELAY:
|
||||
if (!parse_nostr_bech32_nrelay(&bcur, &obj->data.nrelay))
|
||||
goto fail;
|
||||
break;
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
free(obj->buffer);
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//
|
||||
// nostr_bech32.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef nostr_bech32_h
|
||||
#define nostr_bech32_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include "str_block.h"
|
||||
#include "cursor.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef unsigned char u8;
|
||||
#define MAX_RELAYS 10
|
||||
|
||||
struct relays {
|
||||
struct str_block relays[MAX_RELAYS];
|
||||
int num_relays;
|
||||
};
|
||||
|
||||
enum nostr_bech32_type {
|
||||
NOSTR_BECH32_NOTE = 1,
|
||||
NOSTR_BECH32_NPUB = 2,
|
||||
NOSTR_BECH32_NPROFILE = 3,
|
||||
NOSTR_BECH32_NEVENT = 4,
|
||||
NOSTR_BECH32_NRELAY = 5,
|
||||
NOSTR_BECH32_NADDR = 6,
|
||||
NOSTR_BECH32_NSEC = 7,
|
||||
};
|
||||
|
||||
struct bech32_note {
|
||||
const u8 *event_id;
|
||||
};
|
||||
|
||||
struct bech32_npub {
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_nsec {
|
||||
const u8 *nsec;
|
||||
};
|
||||
|
||||
struct bech32_nevent {
|
||||
struct relays relays;
|
||||
const u8 *event_id;
|
||||
const u8 *pubkey; // optional
|
||||
uint32_t kind;
|
||||
bool has_kind;
|
||||
};
|
||||
|
||||
struct bech32_nprofile {
|
||||
struct relays relays;
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_naddr {
|
||||
struct relays relays;
|
||||
struct str_block identifier;
|
||||
const u8 *pubkey;
|
||||
uint32_t kind;
|
||||
};
|
||||
|
||||
struct bech32_nrelay {
|
||||
struct str_block relay;
|
||||
};
|
||||
|
||||
typedef struct nostr_bech32 {
|
||||
enum nostr_bech32_type type;
|
||||
u8 *buffer; // holds strings and tlv stuff
|
||||
size_t buflen;
|
||||
|
||||
union {
|
||||
struct bech32_note note;
|
||||
struct bech32_npub npub;
|
||||
struct bech32_nsec nsec;
|
||||
struct bech32_nevent nevent;
|
||||
struct bech32_nprofile nprofile;
|
||||
struct bech32_naddr naddr;
|
||||
struct bech32_nrelay nrelay;
|
||||
} data;
|
||||
} nostr_bech32_t;
|
||||
|
||||
|
||||
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj);
|
||||
|
||||
#endif /* nostr_bech32_h */
|
||||
308
damus-c/sha256.c
308
damus-c/sha256.c
@@ -1,308 +0,0 @@
|
||||
/* MIT (BSD) license - see LICENSE file for details */
|
||||
/* SHA256 core code translated from the Bitcoin project's C++:
|
||||
*
|
||||
* src/crypto/sha256.cpp commit 417532c8acb93c36c2b6fd052b7c11b6a2906aa2
|
||||
* Copyright (c) 2014 The Bitcoin Core developers
|
||||
* Distributed under the MIT software license, see the accompanying
|
||||
* file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
*/
|
||||
#include "sha256.h"
|
||||
#include "compiler.h"
|
||||
#include "endian.h"
|
||||
#include <stdbool.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
static void invalidate_sha256(struct sha256_ctx *ctx)
|
||||
{
|
||||
#ifdef CCAN_CRYPTO_SHA256_USE_OPENSSL
|
||||
ctx->c.md_len = 0;
|
||||
#else
|
||||
ctx->bytes = (size_t)-1;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void check_sha256(struct sha256_ctx *ctx UNUSED)
|
||||
{
|
||||
#ifdef CCAN_CRYPTO_SHA256_USE_OPENSSL
|
||||
assert(ctx->c.md_len != 0);
|
||||
#else
|
||||
assert(ctx->bytes != (size_t)-1);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef CCAN_CRYPTO_SHA256_USE_OPENSSL
|
||||
void sha256_init(struct sha256_ctx *ctx)
|
||||
{
|
||||
SHA256_Init(&ctx->c);
|
||||
}
|
||||
|
||||
void sha256_update(struct sha256_ctx *ctx, const void *p, size_t size)
|
||||
{
|
||||
check_sha256(ctx);
|
||||
SHA256_Update(&ctx->c, p, size);
|
||||
}
|
||||
|
||||
void sha256_done(struct sha256_ctx *ctx, struct sha256 *res)
|
||||
{
|
||||
SHA256_Final(res->u.u8, &ctx->c);
|
||||
invalidate_sha256(ctx);
|
||||
}
|
||||
#else
|
||||
static uint32_t Ch(uint32_t x, uint32_t y, uint32_t z)
|
||||
{
|
||||
return z ^ (x & (y ^ z));
|
||||
}
|
||||
static uint32_t Maj(uint32_t x, uint32_t y, uint32_t z)
|
||||
{
|
||||
return (x & y) | (z & (x | y));
|
||||
}
|
||||
static uint32_t Sigma0(uint32_t x)
|
||||
{
|
||||
return (x >> 2 | x << 30) ^ (x >> 13 | x << 19) ^ (x >> 22 | x << 10);
|
||||
}
|
||||
static uint32_t Sigma1(uint32_t x)
|
||||
{
|
||||
return (x >> 6 | x << 26) ^ (x >> 11 | x << 21) ^ (x >> 25 | x << 7);
|
||||
}
|
||||
static uint32_t sigma0(uint32_t x)
|
||||
{
|
||||
return (x >> 7 | x << 25) ^ (x >> 18 | x << 14) ^ (x >> 3);
|
||||
}
|
||||
static uint32_t sigma1(uint32_t x)
|
||||
{
|
||||
return (x >> 17 | x << 15) ^ (x >> 19 | x << 13) ^ (x >> 10);
|
||||
}
|
||||
|
||||
/** One round of SHA-256. */
|
||||
static void Round(uint32_t a, uint32_t b, uint32_t c, uint32_t *d, uint32_t e, uint32_t f, uint32_t g, uint32_t *h, uint32_t k, uint32_t w)
|
||||
{
|
||||
uint32_t t1 = *h + Sigma1(e) + Ch(e, f, g) + k + w;
|
||||
uint32_t t2 = Sigma0(a) + Maj(a, b, c);
|
||||
*d += t1;
|
||||
*h = t1 + t2;
|
||||
}
|
||||
|
||||
/** Perform one SHA-256 transformation, processing a 64-byte chunk. */
|
||||
static void Transform(uint32_t *s, const uint32_t *chunk)
|
||||
{
|
||||
uint32_t a = s[0], b = s[1], c = s[2], d = s[3], e = s[4], f = s[5], g = s[6], h = s[7];
|
||||
uint32_t w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15;
|
||||
|
||||
Round(a, b, c, &d, e, f, g, &h, 0x428a2f98, w0 = be32_to_cpu(chunk[0]));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0x71374491, w1 = be32_to_cpu(chunk[1]));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0xb5c0fbcf, w2 = be32_to_cpu(chunk[2]));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0xe9b5dba5, w3 = be32_to_cpu(chunk[3]));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0x3956c25b, w4 = be32_to_cpu(chunk[4]));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0x59f111f1, w5 = be32_to_cpu(chunk[5]));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0x923f82a4, w6 = be32_to_cpu(chunk[6]));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0xab1c5ed5, w7 = be32_to_cpu(chunk[7]));
|
||||
Round(a, b, c, &d, e, f, g, &h, 0xd807aa98, w8 = be32_to_cpu(chunk[8]));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0x12835b01, w9 = be32_to_cpu(chunk[9]));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0x243185be, w10 = be32_to_cpu(chunk[10]));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0x550c7dc3, w11 = be32_to_cpu(chunk[11]));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0x72be5d74, w12 = be32_to_cpu(chunk[12]));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0x80deb1fe, w13 = be32_to_cpu(chunk[13]));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0x9bdc06a7, w14 = be32_to_cpu(chunk[14]));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0xc19bf174, w15 = be32_to_cpu(chunk[15]));
|
||||
|
||||
Round(a, b, c, &d, e, f, g, &h, 0xe49b69c1, w0 += sigma1(w14) + w9 + sigma0(w1));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0xefbe4786, w1 += sigma1(w15) + w10 + sigma0(w2));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0x0fc19dc6, w2 += sigma1(w0) + w11 + sigma0(w3));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0x240ca1cc, w3 += sigma1(w1) + w12 + sigma0(w4));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0x2de92c6f, w4 += sigma1(w2) + w13 + sigma0(w5));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0x4a7484aa, w5 += sigma1(w3) + w14 + sigma0(w6));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0x5cb0a9dc, w6 += sigma1(w4) + w15 + sigma0(w7));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0x76f988da, w7 += sigma1(w5) + w0 + sigma0(w8));
|
||||
Round(a, b, c, &d, e, f, g, &h, 0x983e5152, w8 += sigma1(w6) + w1 + sigma0(w9));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0xa831c66d, w9 += sigma1(w7) + w2 + sigma0(w10));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0xb00327c8, w10 += sigma1(w8) + w3 + sigma0(w11));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0xbf597fc7, w11 += sigma1(w9) + w4 + sigma0(w12));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0xc6e00bf3, w12 += sigma1(w10) + w5 + sigma0(w13));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0xd5a79147, w13 += sigma1(w11) + w6 + sigma0(w14));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0x06ca6351, w14 += sigma1(w12) + w7 + sigma0(w15));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0x14292967, w15 += sigma1(w13) + w8 + sigma0(w0));
|
||||
|
||||
Round(a, b, c, &d, e, f, g, &h, 0x27b70a85, w0 += sigma1(w14) + w9 + sigma0(w1));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0x2e1b2138, w1 += sigma1(w15) + w10 + sigma0(w2));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0x4d2c6dfc, w2 += sigma1(w0) + w11 + sigma0(w3));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0x53380d13, w3 += sigma1(w1) + w12 + sigma0(w4));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0x650a7354, w4 += sigma1(w2) + w13 + sigma0(w5));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0x766a0abb, w5 += sigma1(w3) + w14 + sigma0(w6));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0x81c2c92e, w6 += sigma1(w4) + w15 + sigma0(w7));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0x92722c85, w7 += sigma1(w5) + w0 + sigma0(w8));
|
||||
Round(a, b, c, &d, e, f, g, &h, 0xa2bfe8a1, w8 += sigma1(w6) + w1 + sigma0(w9));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0xa81a664b, w9 += sigma1(w7) + w2 + sigma0(w10));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0xc24b8b70, w10 += sigma1(w8) + w3 + sigma0(w11));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0xc76c51a3, w11 += sigma1(w9) + w4 + sigma0(w12));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0xd192e819, w12 += sigma1(w10) + w5 + sigma0(w13));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0xd6990624, w13 += sigma1(w11) + w6 + sigma0(w14));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0xf40e3585, w14 += sigma1(w12) + w7 + sigma0(w15));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0x106aa070, w15 += sigma1(w13) + w8 + sigma0(w0));
|
||||
|
||||
Round(a, b, c, &d, e, f, g, &h, 0x19a4c116, w0 += sigma1(w14) + w9 + sigma0(w1));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0x1e376c08, w1 += sigma1(w15) + w10 + sigma0(w2));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0x2748774c, w2 += sigma1(w0) + w11 + sigma0(w3));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0x34b0bcb5, w3 += sigma1(w1) + w12 + sigma0(w4));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0x391c0cb3, w4 += sigma1(w2) + w13 + sigma0(w5));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0x4ed8aa4a, w5 += sigma1(w3) + w14 + sigma0(w6));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0x5b9cca4f, w6 += sigma1(w4) + w15 + sigma0(w7));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0x682e6ff3, w7 += sigma1(w5) + w0 + sigma0(w8));
|
||||
Round(a, b, c, &d, e, f, g, &h, 0x748f82ee, w8 += sigma1(w6) + w1 + sigma0(w9));
|
||||
Round(h, a, b, &c, d, e, f, &g, 0x78a5636f, w9 += sigma1(w7) + w2 + sigma0(w10));
|
||||
Round(g, h, a, &b, c, d, e, &f, 0x84c87814, w10 += sigma1(w8) + w3 + sigma0(w11));
|
||||
Round(f, g, h, &a, b, c, d, &e, 0x8cc70208, w11 += sigma1(w9) + w4 + sigma0(w12));
|
||||
Round(e, f, g, &h, a, b, c, &d, 0x90befffa, w12 += sigma1(w10) + w5 + sigma0(w13));
|
||||
Round(d, e, f, &g, h, a, b, &c, 0xa4506ceb, w13 += sigma1(w11) + w6 + sigma0(w14));
|
||||
Round(c, d, e, &f, g, h, a, &b, 0xbef9a3f7, w14 + sigma1(w12) + w7 + sigma0(w15));
|
||||
Round(b, c, d, &e, f, g, h, &a, 0xc67178f2, w15 + sigma1(w13) + w8 + sigma0(w0));
|
||||
|
||||
s[0] += a;
|
||||
s[1] += b;
|
||||
s[2] += c;
|
||||
s[3] += d;
|
||||
s[4] += e;
|
||||
s[5] += f;
|
||||
s[6] += g;
|
||||
s[7] += h;
|
||||
}
|
||||
|
||||
static bool alignment_ok(const void *p UNUSED, size_t n UNUSED)
|
||||
{
|
||||
#if HAVE_UNALIGNED_ACCESS
|
||||
return true;
|
||||
#else
|
||||
return ((size_t)p % n == 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void add(struct sha256_ctx *ctx, const void *p, size_t len)
|
||||
{
|
||||
const unsigned char *data = p;
|
||||
size_t bufsize = ctx->bytes % 64;
|
||||
|
||||
if (bufsize + len >= 64) {
|
||||
/* Fill the buffer, and process it. */
|
||||
memcpy(ctx->buf.u8 + bufsize, data, 64 - bufsize);
|
||||
ctx->bytes += 64 - bufsize;
|
||||
data += 64 - bufsize;
|
||||
len -= 64 - bufsize;
|
||||
Transform(ctx->s, ctx->buf.u32);
|
||||
bufsize = 0;
|
||||
}
|
||||
|
||||
while (len >= 64) {
|
||||
/* Process full chunks directly from the source. */
|
||||
if (alignment_ok(data, sizeof(uint32_t)))
|
||||
Transform(ctx->s, (const uint32_t *)data);
|
||||
else {
|
||||
memcpy(ctx->buf.u8, data, sizeof(ctx->buf));
|
||||
Transform(ctx->s, ctx->buf.u32);
|
||||
}
|
||||
ctx->bytes += 64;
|
||||
data += 64;
|
||||
len -= 64;
|
||||
}
|
||||
|
||||
if (len) {
|
||||
/* Fill the buffer with what remains. */
|
||||
memcpy(ctx->buf.u8 + bufsize, data, len);
|
||||
ctx->bytes += len;
|
||||
}
|
||||
}
|
||||
|
||||
void sha256_init(struct sha256_ctx *ctx)
|
||||
{
|
||||
struct sha256_ctx init = SHA256_INIT;
|
||||
*ctx = init;
|
||||
}
|
||||
|
||||
void sha256_update(struct sha256_ctx *ctx, const void *p, size_t size)
|
||||
{
|
||||
check_sha256(ctx);
|
||||
add(ctx, p, size);
|
||||
}
|
||||
|
||||
void sha256_done(struct sha256_ctx *ctx, struct sha256 *res)
|
||||
{
|
||||
static const unsigned char pad[64] = {0x80};
|
||||
uint64_t sizedesc;
|
||||
size_t i;
|
||||
|
||||
sizedesc = cpu_to_be64((uint64_t)ctx->bytes << 3);
|
||||
/* Add '1' bit to terminate, then all 0 bits, up to next block - 8. */
|
||||
add(ctx, pad, 1 + ((128 - 8 - (ctx->bytes % 64) - 1) % 64));
|
||||
/* Add number of bits of data (big endian) */
|
||||
add(ctx, &sizedesc, 8);
|
||||
for (i = 0; i < sizeof(ctx->s) / sizeof(ctx->s[0]); i++)
|
||||
res->u.u32[i] = cpu_to_be32(ctx->s[i]);
|
||||
invalidate_sha256(ctx);
|
||||
}
|
||||
#endif
|
||||
|
||||
void sha256(struct sha256 *sha, const void *p, size_t size)
|
||||
{
|
||||
struct sha256_ctx ctx;
|
||||
|
||||
sha256_init(&ctx);
|
||||
sha256_update(&ctx, p, size);
|
||||
sha256_done(&ctx, sha);
|
||||
}
|
||||
|
||||
void sha256_u8(struct sha256_ctx *ctx, uint8_t v)
|
||||
{
|
||||
sha256_update(ctx, &v, sizeof(v));
|
||||
}
|
||||
|
||||
void sha256_u16(struct sha256_ctx *ctx, uint16_t v)
|
||||
{
|
||||
sha256_update(ctx, &v, sizeof(v));
|
||||
}
|
||||
|
||||
void sha256_u32(struct sha256_ctx *ctx, uint32_t v)
|
||||
{
|
||||
sha256_update(ctx, &v, sizeof(v));
|
||||
}
|
||||
|
||||
void sha256_u64(struct sha256_ctx *ctx, uint64_t v)
|
||||
{
|
||||
sha256_update(ctx, &v, sizeof(v));
|
||||
}
|
||||
|
||||
/* Add as little-endian */
|
||||
void sha256_le16(struct sha256_ctx *ctx, uint16_t v)
|
||||
{
|
||||
leint16_t lev = cpu_to_le16(v);
|
||||
sha256_update(ctx, &lev, sizeof(lev));
|
||||
}
|
||||
|
||||
void sha256_le32(struct sha256_ctx *ctx, uint32_t v)
|
||||
{
|
||||
leint32_t lev = cpu_to_le32(v);
|
||||
sha256_update(ctx, &lev, sizeof(lev));
|
||||
}
|
||||
|
||||
void sha256_le64(struct sha256_ctx *ctx, uint64_t v)
|
||||
{
|
||||
leint64_t lev = cpu_to_le64(v);
|
||||
sha256_update(ctx, &lev, sizeof(lev));
|
||||
}
|
||||
|
||||
/* Add as big-endian */
|
||||
void sha256_be16(struct sha256_ctx *ctx, uint16_t v)
|
||||
{
|
||||
beint16_t bev = cpu_to_be16(v);
|
||||
sha256_update(ctx, &bev, sizeof(bev));
|
||||
}
|
||||
|
||||
void sha256_be32(struct sha256_ctx *ctx, uint32_t v)
|
||||
{
|
||||
beint32_t bev = cpu_to_be32(v);
|
||||
sha256_update(ctx, &bev, sizeof(bev));
|
||||
}
|
||||
|
||||
void sha256_be64(struct sha256_ctx *ctx, uint64_t v)
|
||||
{
|
||||
beint64_t bev = cpu_to_be64(v);
|
||||
sha256_update(ctx, &bev, sizeof(bev));
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
#ifndef PROTOVERSE_TYPEDEFS_H
|
||||
#define PROTOVERSE_TYPEDEFS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef unsigned char u8;
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned short u16;
|
||||
typedef uint64_t u64;
|
||||
typedef int64_t s64;
|
||||
|
||||
|
||||
#endif /* PROTOVERSE_TYPEDEFS_H */
|
||||
@@ -1179,7 +1179,7 @@ static INLINE int parse_i64(struct cursor *read, uint64_t *val)
|
||||
shift = 0;
|
||||
|
||||
do {
|
||||
if (!pull_byte(read, &byte))
|
||||
if (!cursor_pull_byte(read, &byte))
|
||||
return 0;
|
||||
*val |= (byte & 0x7FULL) << shift;
|
||||
shift += 7;
|
||||
@@ -1199,7 +1199,7 @@ static INLINE int uleb128_read(struct cursor *read, unsigned int *val)
|
||||
*val = 0;
|
||||
|
||||
for (;;) {
|
||||
if (!pull_byte(read, &byte))
|
||||
if (!cursor_pull_byte(read, &byte))
|
||||
return 0;
|
||||
|
||||
*val |= (0x7F & byte) << shift;
|
||||
@@ -1222,7 +1222,7 @@ static INLINE int sleb128_read(struct cursor *read, signed int *val)
|
||||
shift = 0;
|
||||
|
||||
do {
|
||||
if (!pull_byte(read, &byte))
|
||||
if (!cursor_pull_byte(read, &byte))
|
||||
return 0;
|
||||
*val |= ((byte & 0x7F) << shift);
|
||||
shift += 7;
|
||||
@@ -1241,21 +1241,21 @@ static INLINE int uleb128_read(struct cursor *read, unsigned int *val)
|
||||
unsigned char p[6] = {0};
|
||||
*val = 0;
|
||||
|
||||
if (pull_byte(read, &p[0]) && (p[0] & 0x80) == 0) {
|
||||
if (cursor_pull_byte(read, &p[0]) && (p[0] & 0x80) == 0) {
|
||||
*val = LEB128_1(unsigned int);
|
||||
if (p[0] == 0x7F)
|
||||
assert((int)*val == -1);
|
||||
return 1;
|
||||
} else if (pull_byte(read, &p[1]) && (p[1] & 0x80) == 0) {
|
||||
} else if (cursor_pull_byte(read, &p[1]) && (p[1] & 0x80) == 0) {
|
||||
*val = LEB128_2(unsigned int);
|
||||
return 2;
|
||||
} else if (pull_byte(read, &p[2]) && (p[2] & 0x80) == 0) {
|
||||
} else if (cursor_pull_byte(read, &p[2]) && (p[2] & 0x80) == 0) {
|
||||
*val = LEB128_3(unsigned int);
|
||||
return 3;
|
||||
} else if (pull_byte(read, &p[3]) && (p[3] & 0x80) == 0) {
|
||||
} else if (cursor_pull_byte(read, &p[3]) && (p[3] & 0x80) == 0) {
|
||||
*val = LEB128_4(unsigned int);
|
||||
return 4;
|
||||
} else if (pull_byte(read, &p[4]) && (p[4] & 0x80) == 0) {
|
||||
} else if (cursor_pull_byte(read, &p[4]) && (p[4] & 0x80) == 0) {
|
||||
if (!(p[4] & 0xF0)) {
|
||||
*val = LEB128_5(unsigned int);
|
||||
return 5;
|
||||
@@ -1296,7 +1296,7 @@ static int parse_section_tag(struct cursor *cur, enum section_tag *section)
|
||||
|
||||
start = cur->p;
|
||||
|
||||
if (!pull_byte(cur, &byte)) {
|
||||
if (!cursor_pull_byte(cur, &byte)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1315,7 +1315,7 @@ static int parse_valtype(struct wasm_parser *p, enum valtype *valtype)
|
||||
|
||||
start = p->cur.p;
|
||||
|
||||
if (unlikely(!pull_byte(&p->cur, (unsigned char*)valtype))) {
|
||||
if (unlikely(!cursor_pull_byte(&p->cur, (unsigned char*)valtype))) {
|
||||
return parse_err(p, "valtype tag oob");
|
||||
}
|
||||
|
||||
@@ -1416,7 +1416,7 @@ static int parse_export_desc(struct wasm_parser *p, enum exportdesc *desc)
|
||||
{
|
||||
unsigned char byte;
|
||||
|
||||
if (!pull_byte(&p->cur, &byte)) {
|
||||
if (!cursor_pull_byte(&p->cur, &byte)) {
|
||||
parse_err(p, "export desc byte eof");
|
||||
return 0;
|
||||
}
|
||||
@@ -1523,7 +1523,7 @@ static int parse_name_subsection(struct wasm_parser *p, struct namesec *sec, u32
|
||||
u8 tag;
|
||||
u8 *start = p->cur.p;
|
||||
|
||||
if (!pull_byte(&p->cur, &tag))
|
||||
if (!cursor_pull_byte(&p->cur, &tag))
|
||||
return parse_err(p, "name subsection tag oob?");
|
||||
|
||||
if (!is_valid_name_subsection(tag))
|
||||
@@ -1676,7 +1676,7 @@ static int parse_reftype(struct wasm_parser *p, enum reftype *reftype)
|
||||
{
|
||||
u8 tag;
|
||||
|
||||
if (!pull_byte(&p->cur, &tag)) {
|
||||
if (!cursor_pull_byte(&p->cur, &tag)) {
|
||||
parse_err(p, "reftype");
|
||||
return 0;
|
||||
}
|
||||
@@ -1720,7 +1720,7 @@ static int parse_export_section(struct wasm_parser *p,
|
||||
static int parse_limits(struct wasm_parser *p, struct limits *limits)
|
||||
{
|
||||
unsigned char tag;
|
||||
if (!pull_byte(&p->cur, &tag)) {
|
||||
if (!cursor_pull_byte(&p->cur, &tag)) {
|
||||
return parse_err(p, "oob");
|
||||
}
|
||||
|
||||
@@ -1803,7 +1803,7 @@ static void print_code(u8 *code, int code_len)
|
||||
make_cursor(code, code + code_len, &c);
|
||||
|
||||
for (;;) {
|
||||
if (!pull_byte(&c, &tag)) {
|
||||
if (!cursor_pull_byte(&c, &tag)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2169,7 +2169,7 @@ static int parse_const_expr(struct expr_parser *p, struct expr *expr)
|
||||
expr->code = p->code->p;
|
||||
|
||||
while (1) {
|
||||
if (unlikely(!pull_byte(p->code, &tag))) {
|
||||
if (unlikely(!cursor_pull_byte(p->code, &tag))) {
|
||||
return note_error(p->errs, p->code, "oob");
|
||||
}
|
||||
|
||||
@@ -2332,7 +2332,7 @@ static int parse_instrs_until_at(struct expr_parser *p, u8 stop_instr,
|
||||
p->code->p - p->code->start,
|
||||
dbg_inst, instr_name(stop_instr));
|
||||
for (;;) {
|
||||
if (!pull_byte(p->code, &tag))
|
||||
if (!cursor_pull_byte(p->code, &tag))
|
||||
return note_error(p->errs, p->code, "oob");
|
||||
|
||||
if ((tag != i_if && tag == stop_instr) ||
|
||||
@@ -2413,7 +2413,7 @@ static int parse_element(struct wasm_parser *p, struct elem *elem)
|
||||
|
||||
make_expr_parser(&p->errs, &p->cur, &expr_parser);
|
||||
|
||||
if (!pull_byte(&p->cur, &tag))
|
||||
if (!cursor_pull_byte(&p->cur, &tag))
|
||||
return parse_err(p, "tag");
|
||||
|
||||
if (tag > 7)
|
||||
@@ -2545,7 +2545,7 @@ static int parse_wdata(struct wasm_parser *p, struct wdata *data)
|
||||
struct expr_parser parser;
|
||||
u8 tag;
|
||||
|
||||
if (!pull_byte(&p->cur, &tag)) {
|
||||
if (!cursor_pull_byte(&p->cur, &tag)) {
|
||||
return parse_err(p, "tag");
|
||||
}
|
||||
|
||||
@@ -2700,7 +2700,7 @@ static int parse_importdesc(struct wasm_parser *p, struct importdesc *desc)
|
||||
{
|
||||
u8 tag;
|
||||
|
||||
if (!pull_byte(&p->cur, &tag)) {
|
||||
if (!cursor_pull_byte(&p->cur, &tag)) {
|
||||
parse_err(p, "oom");
|
||||
return 0;
|
||||
}
|
||||
@@ -4134,7 +4134,7 @@ static int parse_blocktype(struct cursor *cur, struct errors *errs, struct block
|
||||
{
|
||||
unsigned char byte;
|
||||
|
||||
if (unlikely(!pull_byte(cur, &byte))) {
|
||||
if (unlikely(!cursor_pull_byte(cur, &byte))) {
|
||||
return note_error(errs, cur, "parse_blocktype: oob\n");
|
||||
}
|
||||
|
||||
@@ -4656,7 +4656,7 @@ static int parse_bulk_op(struct cursor *code, struct errors *errs,
|
||||
{
|
||||
u8 tag;
|
||||
|
||||
if (unlikely(!pull_byte(code, &tag)))
|
||||
if (unlikely(!cursor_pull_byte(code, &tag)))
|
||||
return note_error(errs, code, "oob");
|
||||
|
||||
if (unlikely(tag < 10 || tag > 17))
|
||||
@@ -6552,7 +6552,7 @@ static INLINE int interp_parse_instr(struct wasm_interp *interp,
|
||||
{
|
||||
u8 tag;
|
||||
|
||||
if (unlikely(!pull_byte(code, &tag))) {
|
||||
if (unlikely(!cursor_pull_byte(code, &tag))) {
|
||||
return interp_error(interp, "no more instrs to pull");
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ static const unsigned char WASM_MAGIC[] = {0,'a','s','m'};
|
||||
#define interp_error(p, fmt, ...) note_error(&((p)->errors), interp_codeptr(p), fmt, ##__VA_ARGS__)
|
||||
#define parse_err(p, fmt, ...) note_error(&((p)->errs), &(p)->cur, fmt, ##__VA_ARGS__)
|
||||
|
||||
#include "short_types.h"
|
||||
|
||||
enum valtype {
|
||||
val_i32 = 0x7F,
|
||||
val_i64 = 0x7E,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "085cf0f645323bf77edb52886489bf77b309a0a2d2b78a54beaf8520b540d596",
|
||||
"originHash" : "1fc7e0b44329ba72cd285eeb022b5b92582cd01586b920d243cb0485c2e69dcc",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "codescanner",
|
||||
@@ -22,8 +22,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiKit",
|
||||
"state" : {
|
||||
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
|
||||
"version" : "0.1.2"
|
||||
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -31,8 +31,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
||||
"state" : {
|
||||
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
|
||||
"version" : "0.1.1"
|
||||
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "faviconfinder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/will-lumley/FaviconFinder.git",
|
||||
"state" : {
|
||||
"revision" : "9279f4371f4877ca302ba3bf1015f3f58ae4a56c",
|
||||
"version" : "5.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -49,8 +58,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
|
||||
"version" : "7.6.1"
|
||||
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
|
||||
"version" : "8.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -105,6 +114,15 @@
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsoup",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "bba848db50462894e7fc0891d018dfecad4ef11e",
|
||||
"version" : "2.8.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftycrop",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -28,6 +28,15 @@ enum AppAccessibilityIdentifiers: String {
|
||||
// MARK: Onboarding
|
||||
// Prefix: `onboarding`
|
||||
|
||||
/// Any interest option button on the "select your interests" page during onboarding
|
||||
case onboarding_interest_option_button
|
||||
|
||||
/// The "next" button on the onboarding interest page
|
||||
case onboarding_interest_page_next_page
|
||||
|
||||
/// The "next" button on the onboarding content settings page
|
||||
case onboarding_content_settings_page_next_page
|
||||
|
||||
/// The skip button on the onboarding sheet
|
||||
case onboarding_sheet_skip_button
|
||||
|
||||
BIN
damus/Assets.xcassets/Logos/bbw.imageset/bbw.jpg
vendored
BIN
damus/Assets.xcassets/Logos/bbw.imageset/bbw.jpg
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bbw.jpg",
|
||||
"filename" : "blink.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
damus/Assets.xcassets/Logos/blink.imageset/blink.png
vendored
Normal file
BIN
damus/Assets.xcassets/Logos/blink.imageset/blink.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 216 B After Width: | Height: | Size: 216 B |
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// AlbyGradient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-05-09.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
fileprivate let alby_grad_c1 = hex_col(r: 226, g: 168, b: 122)
|
||||
fileprivate let alby_grad_c2 = hex_col(r: 249, g: 223, b: 127)
|
||||
fileprivate let alby_grad = [alby_grad_c2, alby_grad_c1]
|
||||
|
||||
let AlbyGradient: LinearGradient =
|
||||
LinearGradient(colors: alby_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// Reposted.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Reposted: View {
|
||||
let damus: DamusState
|
||||
let pubkey: Pubkey
|
||||
let target: NoteId
|
||||
@State var reposts: Int
|
||||
|
||||
init(damus: DamusState, pubkey: Pubkey, target: NoteId) {
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
self.target = target
|
||||
self.reposts = damus.boosts.counts[target] ?? 1
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image("repost")
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target))) {
|
||||
let other_reposts = reposts - 1
|
||||
if other_reposts > 0 {
|
||||
Text(" and \(other_reposts) others reposted", comment: "Text indicating that the note was reposted (i.e. re-shared) by multiple people")
|
||||
.foregroundColor(Color.gray)
|
||||
} else {
|
||||
Text("reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats), perform: { note_id in
|
||||
guard note_id == target else { return }
|
||||
let repost_count = damus.boosts.counts[target]
|
||||
if let repost_count, reposts != repost_count {
|
||||
reposts = repost_count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Reposted_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note.id)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
import AVKit
|
||||
import MediaPlayer
|
||||
import EmojiPicker
|
||||
import TipKit
|
||||
|
||||
struct ZapSheet {
|
||||
let target: ZapTarget
|
||||
@@ -178,7 +179,7 @@ struct ContentView: View {
|
||||
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
||||
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings, subtitle: $menu_subtitle)
|
||||
}
|
||||
}
|
||||
.background(DamusColors.adaptableWhite)
|
||||
@@ -199,7 +200,7 @@ struct ContentView: View {
|
||||
func MaybeReportView(target: ReportTarget) -> some View {
|
||||
Group {
|
||||
if let keypair = damus_state.keypair.to_full() {
|
||||
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
|
||||
ReportView(postbox: damus_state.nostrNetwork.postbox, target: target, keypair: keypair)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -222,12 +223,6 @@ struct ContentView: View {
|
||||
navigationCoordinator.push(route: Route.Script(script: model))
|
||||
}
|
||||
|
||||
func open_profile(pubkey: Pubkey) {
|
||||
let profile_model = ProfileModel(pubkey: pubkey, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: pubkey)
|
||||
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
||||
}
|
||||
|
||||
func open_search(filt: NostrFilter) {
|
||||
let search = SearchModel(state: damus_state!, search: filt)
|
||||
navigationCoordinator.push(route: Route.Search(search: search))
|
||||
@@ -312,6 +307,9 @@ struct ContentView: View {
|
||||
hasSeenOnboardingSuggestions = true
|
||||
}
|
||||
self.appDelegate?.state = damus_state
|
||||
Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread.
|
||||
await self.listenAndHandleLocalNotifications()
|
||||
}
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
switch item {
|
||||
@@ -320,7 +318,7 @@ struct ContentView: View {
|
||||
case .post(let action):
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .user_status:
|
||||
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.nostrNetwork.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||
.presentationDragIndicator(.visible)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
@@ -336,7 +334,20 @@ struct ContentView: View {
|
||||
.presentationDetents([.height(550)])
|
||||
.presentationDragIndicator(.visible)
|
||||
case .onboardingSuggestions:
|
||||
OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!))
|
||||
if let model = try? SuggestedUsersViewModel(damus_state: damus_state!) {
|
||||
OnboardingSuggestionsView(model: model)
|
||||
.interactiveDismissDisabled(true)
|
||||
}
|
||||
else {
|
||||
ErrorView(
|
||||
damus_state: damus_state,
|
||||
error: .init(
|
||||
user_visible_description: NSLocalizedString("Unexpected error loading user suggestions", comment: "Human readable error label"),
|
||||
tip: NSLocalizedString("Please contact support", comment: "Human readable error tip"),
|
||||
technical_info: "Error inializing SuggestedUsersViewModel"
|
||||
)
|
||||
)
|
||||
}
|
||||
case .purple(let purple_url):
|
||||
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
|
||||
case .purple_onboarding:
|
||||
@@ -359,7 +370,7 @@ struct ContentView: View {
|
||||
self.hide_bar = !show
|
||||
}
|
||||
.onReceive(timer) { n in
|
||||
self.damus_state?.postbox.try_flushing_events()
|
||||
self.damus_state?.nostrNetwork.postbox.try_flushing_events()
|
||||
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||
}
|
||||
.onReceive(handle_notify(.report)) { target in
|
||||
@@ -390,12 +401,12 @@ struct ContentView: View {
|
||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
||||
|
||||
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||
ds.postbox.send(ev)
|
||||
ds.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
.onReceive(handle_notify(.broadcast)) { ev in
|
||||
guard let ds = self.damus_state else { return }
|
||||
|
||||
ds.postbox.send(ev)
|
||||
ds.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
.onReceive(handle_notify(.unfollow)) { target in
|
||||
guard let state = self.damus_state else { return }
|
||||
@@ -417,7 +428,7 @@ struct ContentView: View {
|
||||
return
|
||||
}
|
||||
|
||||
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
|
||||
if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
|
||||
self.active_sheet = nil
|
||||
}
|
||||
}
|
||||
@@ -461,7 +472,7 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.disconnect_relays)) { () in
|
||||
damus_state.pool.disconnect()
|
||||
damus_state.nostrNetwork.pool.disconnect()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
||||
print("txn: 📙 DAMUS ACTIVE NOTIFY")
|
||||
@@ -507,31 +518,10 @@ struct ContentView: View {
|
||||
break
|
||||
case .active:
|
||||
print("txn: 📙 DAMUS ACTIVE")
|
||||
damus_state.pool.ping()
|
||||
damus_state.nostrNetwork.pool.ping()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.local_notification)) { local in
|
||||
guard let damus_state else { return }
|
||||
|
||||
switch local.mention {
|
||||
case .pubkey(let pubkey):
|
||||
open_profile(pubkey: pubkey)
|
||||
|
||||
case .note(let noteId):
|
||||
openEvent(noteId: noteId, notificationType: local.type)
|
||||
case .nevent(let nevent):
|
||||
openEvent(noteId: nevent.noteid, notificationType: local.type)
|
||||
case .nprofile(let nprofile):
|
||||
open_profile(pubkey: nprofile.author)
|
||||
case .nrelay(_):
|
||||
break
|
||||
case .naddr(let naddr):
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
||||
home.filter_events()
|
||||
@@ -547,7 +537,7 @@ struct ContentView: View {
|
||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
||||
|
||||
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||
ds.postbox.send(profile_ev)
|
||||
ds.nostrNetwork.postbox.send(profile_ev)
|
||||
}
|
||||
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||
@@ -579,7 +569,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
ds.mutelist_manager.set_mutelist(mutelist)
|
||||
ds.postbox.send(mutelist)
|
||||
ds.nostrNetwork.postbox.send(mutelist)
|
||||
|
||||
confirm_overwrite_mutelist = false
|
||||
confirm_mute = false
|
||||
@@ -611,7 +601,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
ds.mutelist_manager.set_mutelist(ev)
|
||||
ds.postbox.send(ev)
|
||||
ds.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
}, message: {
|
||||
@@ -641,6 +631,28 @@ struct ContentView: View {
|
||||
self.selected_timeline = timeline
|
||||
}
|
||||
|
||||
/// Listens to requests to open a push/local user notification
|
||||
///
|
||||
/// This function never returns, it just keeps streaming
|
||||
func listenAndHandleLocalNotifications() async {
|
||||
for await notification in await QueueableNotify<LossyLocalNotification>.shared.stream {
|
||||
self.handleNotification(notification: notification)
|
||||
}
|
||||
}
|
||||
|
||||
func handleNotification(notification: LossyLocalNotification) {
|
||||
Log.info("ContentView is handling a notification", for: .push_notifications)
|
||||
guard damus_state != nil else {
|
||||
// This should never happen because `listenAndHandleLocalNotifications` is called after damus state is initialized in `onAppear`
|
||||
assertionFailure("DamusState not loaded when ContentView (new handler) was handling a notification")
|
||||
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
let local = notification
|
||||
let openAction = local.toViewOpenAction()
|
||||
self.execute_open_action(openAction)
|
||||
}
|
||||
|
||||
func connect() {
|
||||
// nostrdb
|
||||
var mndb = Ndb()
|
||||
@@ -658,28 +670,14 @@ struct ContentView: View {
|
||||
|
||||
guard let ndb = mndb else { return }
|
||||
|
||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
if let nwc_str = settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: nwc_str) {
|
||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||
}
|
||||
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
self.damus_state = DamusState(keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
@@ -695,8 +693,6 @@ struct ContentView: View {
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator,
|
||||
@@ -704,7 +700,8 @@ struct ContentView: View {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
|
||||
home.damus_state = self.damus_state!
|
||||
@@ -720,7 +717,23 @@ struct ContentView: View {
|
||||
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
||||
}
|
||||
|
||||
pool.connect()
|
||||
damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
damus_state.nostrNetwork.connect()
|
||||
|
||||
if #available(iOS 17, *) {
|
||||
if damus_state.settings.developer_mode && damus_state.settings.reset_tips_on_launch {
|
||||
do {
|
||||
try Tips.resetDatastore()
|
||||
} catch {
|
||||
Log.error("Failed to reset tips datastore: %s", for: .tips, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
do {
|
||||
try Tips.configure()
|
||||
} catch {
|
||||
Log.error("Failed to configure tips: %s", for: .tips, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func music_changed(_ state: MusicState) {
|
||||
@@ -743,24 +756,7 @@ struct ContentView: View {
|
||||
pdata.status.music = music
|
||||
|
||||
guard let ev = music.to_note(keypair: kp) else { return }
|
||||
damus_state.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func openEvent(noteId: NoteId, notificationType: LocalNotificationType) {
|
||||
guard let target = damus_state.events.lookup(noteId) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch notificationType {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.set_active_dm(target.pubkey)
|
||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||
case .like, .zap, .mention, .repost, .reply, .tagged:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
break
|
||||
damus_state.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,6 +772,8 @@ struct ContentView: View {
|
||||
case route(Route)
|
||||
/// Open a sheet
|
||||
case sheet(Sheets)
|
||||
/// Open an external URL
|
||||
case external_url(URL)
|
||||
/// Do nothing.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
@@ -792,6 +790,8 @@ struct ContentView: View {
|
||||
navigationCoordinator.push(route: route)
|
||||
case .sheet(let sheet):
|
||||
self.active_sheet = sheet
|
||||
case .external_url(let url):
|
||||
this_app.open(url)
|
||||
case .no_action:
|
||||
return
|
||||
}
|
||||
@@ -1009,7 +1009,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
var has_event = false
|
||||
guard let filter else { return }
|
||||
|
||||
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||
state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||
guard case .nostr_event(let ev) = res else {
|
||||
return
|
||||
}
|
||||
@@ -1023,7 +1023,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
break
|
||||
case .event(_, let ev):
|
||||
has_event = true
|
||||
state.pool.unsubscribe(sub_id: subid)
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: subid)
|
||||
|
||||
switch query {
|
||||
case .profile:
|
||||
@@ -1036,11 +1036,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
case .eose:
|
||||
if !has_event {
|
||||
attempts += 1
|
||||
if attempts >= state.pool.our_descriptors.count {
|
||||
if attempts >= state.nostrNetwork.pool.our_descriptors.count {
|
||||
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
|
||||
}
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
||||
case .notice:
|
||||
break
|
||||
case .auth:
|
||||
@@ -1059,15 +1059,15 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
/// - naddr: the `naddr` address
|
||||
/// - callback: A function to handle the found event
|
||||
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
|
||||
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
||||
let nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
||||
|
||||
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
|
||||
|
||||
let subid = UUID().description
|
||||
|
||||
damus_state.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
||||
damus_state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
||||
guard case .nostr_event(let ev) = res else {
|
||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1075,14 +1075,14 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
|
||||
for tag in ev.tags {
|
||||
if(tag.count >= 2 && tag[0].string() == "d"){
|
||||
if (tag[1].string() == naddr.identifier){
|
||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
callback(ev)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1130,7 +1130,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
|
||||
|
||||
let old_contacts = state.contacts.event
|
||||
|
||||
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||
guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -1156,7 +1156,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||
guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -1216,6 +1216,44 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
||||
}
|
||||
}
|
||||
|
||||
extension LossyLocalNotification {
|
||||
/// Computes a view open action from a mention reference.
|
||||
/// Use this when opening a user-presentable interface to a specific mention reference.
|
||||
func toViewOpenAction() -> ContentView.ViewOpenAction {
|
||||
switch self.mention.nip19 {
|
||||
case .npub(let pubkey):
|
||||
return .route(.ProfileByKey(pubkey: pubkey))
|
||||
case .note(let noteId):
|
||||
return .route(.LoadableNostrEvent(note_reference: .note_id(noteId)))
|
||||
case .nevent(let nEvent):
|
||||
// TODO: Improve this by implementing a route that handles nevents with their relay hints.
|
||||
return .route(.LoadableNostrEvent(note_reference: .note_id(nEvent.noteid)))
|
||||
case .nprofile(let nProfile):
|
||||
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
|
||||
return .route(.ProfileByKey(pubkey: nProfile.author))
|
||||
case .nrelay:
|
||||
// We do not need to implement `nrelay` support, it has been deprecated.
|
||||
// See https://github.com/nostr-protocol/nips/blob/6e7a618e7f873bb91e743caacc3b09edab7796a0/BREAKING.md?plain=1#L21
|
||||
return .sheet(.error(ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("You opened an invalid link. The link you tried to open refers to \"nrelay\", which has been deprecated and is not supported.", comment: "User-visible error description for a user who tries to open a deprecated \"nrelay\" link."),
|
||||
tip: NSLocalizedString("Please contact the person who provided the link, and ask for another link.", comment: "User-visible tip on what to do if a link contains a deprecated \"nrelay\" reference."),
|
||||
technical_info: "`MentionRef.toViewOpenAction` detected deprecated `nrelay` contents"
|
||||
)))
|
||||
case .naddr(let nAddr):
|
||||
return .route(.LoadableNostrEvent(note_reference: .naddr(nAddr)))
|
||||
case .nsec(_):
|
||||
// `nsec` urls are a terrible idea security-wise, so we should intentionally not support those — in order to discourage their use.
|
||||
return .sheet(.error(ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("You opened an invalid link. The link you tried to open refers to \"nsec\", which is not supported.", comment: "User-visible error description for a user who tries to open an unsupported \"nsec\" link."),
|
||||
tip: NSLocalizedString("Please contact the person who provided the link, and ask for another link. Also, this link may have sensitive information, please use caution before sharing it.", comment: "User-visible tip on what to do if a link contains an unsupported \"nsec\" reference."),
|
||||
technical_info: "`MentionRef.toViewOpenAction` detected unsupported `nsec` contents"
|
||||
)))
|
||||
case .nscript(let script):
|
||||
return .route(.Script(script: ScriptModel(data: script, state: .not_loaded)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func logout(_ state: DamusState?)
|
||||
{
|
||||
|
||||
77
damus/Core/DIPs/DIP06/Interests.swift
Normal file
77
damus/Core/DIPs/DIP06/Interests.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Interests.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-06-25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DIP06 {
|
||||
/// Standard general interest topics.
|
||||
/// See https://github.com/damus-io/dips/pull/3
|
||||
enum Interest: String, CaseIterable {
|
||||
/// Bitcoin-related topics (e.g. Bitcoin, Lightning, e-cash etc)
|
||||
case bitcoin = "bitcoin"
|
||||
/// Any non-Bitcoin technology-related topic (e.g. Linux, new releases, software development, supersonic flight, etc)
|
||||
case technology = "technology"
|
||||
/// Any science-related topic (e.g. astronomy, biology, physics, etc)
|
||||
case science = "science"
|
||||
/// Lifestyle topics (e.g. Worldschooling, Digital nomading, vagabonding, homesteading, digital minimalism, life hacks, etc)
|
||||
case lifestyle = "lifestyle"
|
||||
/// Travel-related topics (e.g. Information about locations to visit, travel logs, etc)
|
||||
case travel = "travel"
|
||||
/// Any art-related topic (e.g. poetry, painting, sculpting, photography, etc)
|
||||
case art = "art"
|
||||
/// Topics focused on improving human health (e.g. advances in medicine, exercising, nutrition, meditation, sleep, etc)
|
||||
case health = "health"
|
||||
/// Any music-related topic (e.g. Bands, fan pages, instruments, classical music theory, etc)
|
||||
case music = "music"
|
||||
/// Any topic related to food (e.g. Cooking, recipes, meal planning, nutrition)
|
||||
case food = "food"
|
||||
/// Any topic related to sports (e.g. Athlete fan pages, general sports information, sports news, sports equipment, etc)
|
||||
case sports = "sports"
|
||||
/// Any topic related to religion, spirituality, or faith (e.g. Christianity, Judaism, Buddhism, Islamism, Hinduism, Taoism, general meditation practice, etc)
|
||||
case religionSpirituality = "religion-spirituality"
|
||||
/// General humanities topics (e.g. philosophy, sociology, culture, etc)
|
||||
case humanities = "humanities"
|
||||
/// General topics about politics
|
||||
case politics = "politics"
|
||||
/// Other miscellaneous topics that do not fit in any of the previous items of the list
|
||||
case other = "other"
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .bitcoin:
|
||||
return NSLocalizedString("₿ Bitcoin", comment: "Interest topic label")
|
||||
case .technology:
|
||||
return NSLocalizedString("💻 Tech", comment: "Interest topic label")
|
||||
case .science:
|
||||
return NSLocalizedString("🔭 Science", comment: "Interest topic label")
|
||||
case .lifestyle:
|
||||
return NSLocalizedString("🏝️ Lifestyle", comment: "Interest topic label")
|
||||
case .travel:
|
||||
return NSLocalizedString("✈️ Travel", comment: "Interest topic label")
|
||||
case .art:
|
||||
return NSLocalizedString("🎨 Art", comment: "Interest topic label")
|
||||
case .health:
|
||||
return NSLocalizedString("🏃 Health", comment: "Interest topic label")
|
||||
case .music:
|
||||
return NSLocalizedString("🎶 Music", comment: "Interest topic label")
|
||||
case .food:
|
||||
return NSLocalizedString("🍱 Food", comment: "Interest topic label")
|
||||
case .sports:
|
||||
return NSLocalizedString("⚾️ Sports", comment: "Interest topic label")
|
||||
case .religionSpirituality:
|
||||
return NSLocalizedString("🛐 Religion", comment: "Interest topic label")
|
||||
case .humanities:
|
||||
return NSLocalizedString("📚 Humanities", comment: "Interest topic label")
|
||||
case .politics:
|
||||
return NSLocalizedString("🏛️ Politics", comment: "Interest topic label")
|
||||
case .other:
|
||||
return NSLocalizedString("♾️ Other", comment: "Interest topic label")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
damus/Core/NIPs/NIP04/NIP04.swift
Normal file
79
damus/Core/NIPs/NIP04/NIP04.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// NIP04.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-03-10.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
/// Functions and utilities for the NIP-04 spec
|
||||
struct NIP04 {}
|
||||
|
||||
extension NIP04 {
|
||||
/// Encrypts a message using NIP-04.
|
||||
static func encrypt_message(message: String, privkey: Privkey, to_pk: Pubkey, encoding: EncEncoding = .base64) -> String? {
|
||||
let iv = random_bytes(count: 16).bytes
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
let utf8_message = Data(message.utf8).bytes
|
||||
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch encoding {
|
||||
case .base64:
|
||||
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
case .bech32:
|
||||
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Creates an event with encrypted `contents` field, using NIP-04
|
||||
static func create_encrypted_event(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: FullKeypair, created_at: UInt32, kind: UInt32) -> NostrEvent? {
|
||||
let privkey = keypair.privkey
|
||||
|
||||
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NostrEvent(content: enc_content, keypair: keypair.to_keypair(), kind: kind, tags: tags, createdAt: created_at)
|
||||
}
|
||||
|
||||
/// Creates a NIP-04 style direct message event
|
||||
static func create_dm(_ message: String, to_pk: Pubkey, tags: [[String]], keypair: Keypair, created_at: UInt32? = nil) -> NostrEvent?
|
||||
{
|
||||
let created = created_at ?? UInt32(Date().timeIntervalSince1970)
|
||||
|
||||
guard let keypair = keypair.to_full() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4)
|
||||
}
|
||||
|
||||
/// Decrypts string content
|
||||
static func decryptContent(recipientPrivateKey: Privkey, senderPubkey: Pubkey, content: String, encoding: EncEncoding) throws(NIP04DecryptionError) -> String {
|
||||
guard let shared_sec = get_shared_secret(privkey: recipientPrivateKey, pubkey: senderPubkey) else {
|
||||
throw .failedToComputeSharedSecret
|
||||
}
|
||||
guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
|
||||
throw .failedToDecodeEncryptedContent
|
||||
}
|
||||
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
|
||||
throw .failedToDecryptAES
|
||||
}
|
||||
guard let decryptedString = String(data: dat, encoding: .utf8) else {
|
||||
throw .utf8DecodingFailedOnDecryptedPayload
|
||||
}
|
||||
return decryptedString
|
||||
}
|
||||
|
||||
enum NIP04DecryptionError: Error {
|
||||
case failedToComputeSharedSecret
|
||||
case failedToDecodeEncryptedContent
|
||||
case failedToDecryptAES
|
||||
case utf8DecodingFailedOnDecryptedPayload
|
||||
}
|
||||
}
|
||||
111
damus/Core/NIPs/NIP51/InterestList.swift
Normal file
111
damus/Core/NIPs/NIP51/InterestList.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// InterestList.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D'Aquino on 2025-06-23.
|
||||
//
|
||||
// Some text excerpts taken from the Nostr Protocol itself (which are public domain)
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Includes models and functions for working with NIP-51
|
||||
struct NIP51: Sendable {}
|
||||
|
||||
extension NIP51 {
|
||||
/// An error thrown when decoding an item into a NIP-51 list
|
||||
enum NIP51DecodingError: Error {
|
||||
/// The Nostr event being converted is not a NIP-51 interest list
|
||||
case notInterestList
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP51 {
|
||||
/// Models a NIP-51 Interest List (kind:10015)
|
||||
struct InterestList: NostrEventConvertible, Sendable {
|
||||
typealias E = NIP51DecodingError
|
||||
|
||||
enum InterestItem: Sendable, Hashable {
|
||||
case hashtag(String)
|
||||
case interestSet(String, String, String) // a-tag: kind, pubkey, identifier
|
||||
|
||||
var tag: [String] {
|
||||
switch self {
|
||||
case .hashtag(let tag):
|
||||
return ["t", tag]
|
||||
case .interestSet(let kind, let pubkey, let identifier):
|
||||
var tag = ["a", "\(kind):\(pubkey):\(identifier)"]
|
||||
return tag
|
||||
}
|
||||
}
|
||||
|
||||
static func fromTag(tag: TagSequence) -> InterestItem? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard let t0 = i.next(),
|
||||
let t1 = i.next() else { return nil }
|
||||
|
||||
let tagName = t0.string()
|
||||
|
||||
if tagName == "t" {
|
||||
return .hashtag(t1.string())
|
||||
} else if tagName == "a" {
|
||||
let components = t1.string().split(separator: ":")
|
||||
guard components.count > 2 else { return nil }
|
||||
|
||||
let kind = String(components[0])
|
||||
let pubkey = String(components[1])
|
||||
let identifier = String(components[2])
|
||||
|
||||
return .interestSet(kind, pubkey, identifier)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let interests: [InterestItem]
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(event: NdbNote) throws(E) {
|
||||
try self.init(event: UnownedNdbNote(event))
|
||||
}
|
||||
|
||||
init(event: borrowing UnownedNdbNote) throws(E) {
|
||||
guard event.known_kind == .interest_list else {
|
||||
throw E.notInterestList
|
||||
}
|
||||
|
||||
var interests: [InterestItem] = []
|
||||
|
||||
for tag in event.tags {
|
||||
if let interest = InterestItem.fromTag(tag: tag) {
|
||||
interests.append(interest)
|
||||
}
|
||||
}
|
||||
|
||||
self.interests = interests
|
||||
}
|
||||
|
||||
init?(event: NdbNote?) throws(E) {
|
||||
guard let event else { return nil }
|
||||
try self.init(event: event)
|
||||
}
|
||||
|
||||
init(interests: [InterestItem]) {
|
||||
self.interests = interests
|
||||
}
|
||||
|
||||
// MARK: - Conversion to a Nostr Event
|
||||
|
||||
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32? = nil) -> NostrEvent? {
|
||||
return NdbNote(
|
||||
content: "",
|
||||
keypair: keypair.to_keypair(),
|
||||
kind: NostrKind.interest_list.rawValue,
|
||||
tags: self.interests.map { $0.tag },
|
||||
createdAt: timestamp ?? UInt32(Date.now.timeIntervalSince1970)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
171
damus/Core/NIPs/NIP65/NIP65.swift
Normal file
171
damus/Core/NIPs/NIP65/NIP65.swift
Normal file
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// NIP65.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-21.
|
||||
//
|
||||
// Some text excerpts taken from the Nostr Protocol itself (which are public domain)
|
||||
|
||||
import OrderedCollections
|
||||
import Foundation
|
||||
|
||||
/// Includes models and functions for working with NIP-65
|
||||
struct NIP65: Sendable {}
|
||||
|
||||
extension NIP65 {
|
||||
/// Models a NIP-65 relay list
|
||||
struct RelayList: NostrEventConvertible, Sendable {
|
||||
let relays: OrderedDictionary<RelayURL, RelayItem>
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(event: NdbNote) throws(NIP65DecodingError) {
|
||||
try self.init(event: UnownedNdbNote(event))
|
||||
}
|
||||
|
||||
init(event: borrowing UnownedNdbNote) throws(NIP65DecodingError) {
|
||||
guard event.known_kind == .relay_list else { throw .notRelayList }
|
||||
var relays: [RelayItem] = []
|
||||
for tag in event.tags {
|
||||
guard let relay = try RelayItem.fromTag(tag: tag) else { continue }
|
||||
relays.append(relay)
|
||||
}
|
||||
self.relays = Self.relayOrderedDictionary(from: relays)
|
||||
}
|
||||
|
||||
init?(event: NdbNote?) throws(NIP65DecodingError) {
|
||||
guard let event else { return nil }
|
||||
try self.init(event: event)
|
||||
}
|
||||
|
||||
init(relays: [RelayItem]) {
|
||||
self.relays = Self.relayOrderedDictionary(from: relays)
|
||||
}
|
||||
|
||||
init(relays: [RelayURL]) {
|
||||
let relayItemList = relays.map({ RelayItem(url: $0, rwConfiguration: .readWrite) })
|
||||
self.relays = Self.relayOrderedDictionary(from: relayItemList)
|
||||
}
|
||||
|
||||
private static func relayOrderedDictionary(from relayList: [RelayItem]) -> OrderedDictionary<RelayURL, RelayItem> {
|
||||
var seenUrls: Set<RelayURL> = []
|
||||
return OrderedDictionary(uniqueKeysWithValues: relayList.compactMap({
|
||||
// We need to ensure the keys are unique to avoid assertion errors from OrderedDictionary
|
||||
guard !seenUrls.contains($0.url) else { return nil }
|
||||
seenUrls.insert($0.url)
|
||||
return ($0.url, $0)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Conversion to a Nostr Event
|
||||
|
||||
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32? = nil) -> NostrEvent? {
|
||||
return NdbNote(
|
||||
content: "",
|
||||
keypair: keypair.to_keypair(),
|
||||
kind: NostrKind.relay_list.rawValue,
|
||||
tags: self.relays.values.map({ $0.tag }),
|
||||
createdAt: timestamp ?? UInt32(Date.now.timeIntervalSince1970)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP65 {
|
||||
/// An error thrown when decoding an item into a NIP-65 relay list
|
||||
enum NIP65DecodingError: Error {
|
||||
/// The Nostr event being converted is not a NIP-65 relay list
|
||||
case notRelayList
|
||||
/// The relay URL is invalid
|
||||
case invalidRelayURL
|
||||
///The relay RW marker is invalid
|
||||
case invalidRelayMarker
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP65.RelayList {
|
||||
/// An item referencing a relay and its configuration inside a relay list
|
||||
struct RelayItem: ThrowingTagConvertible, Sendable {
|
||||
typealias E = NIP65.NIP65DecodingError
|
||||
|
||||
let url: RelayURL
|
||||
let rwConfiguration: RWConfiguration
|
||||
|
||||
/// The raw tag sequence in a Nostr event
|
||||
var tag: [String] {
|
||||
var tag = ["r", url.absoluteString]
|
||||
if let rwMarker = rwConfiguration.tagItem { tag.append(rwMarker) }
|
||||
return tag
|
||||
}
|
||||
|
||||
/// Initialize a new relay item from a Nostr event's tag sequence
|
||||
static func fromTag(tag: TagSequence) throws(E) -> NIP65.RelayList.RelayItem? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
guard tag.count >= 2,
|
||||
let t0 = i.next(),
|
||||
let key = t0.single_char,
|
||||
let rkey = RefId.RefKey(rawValue: key),
|
||||
let t1 = i.next()
|
||||
else { return nil }
|
||||
|
||||
let t2 = i.next()
|
||||
|
||||
switch rkey {
|
||||
case .r: return try self.fromRawInfo(urlString: t1.string(), rwMarker: t2?.string())
|
||||
// Keep options explicit to make compiler prompt developer on whether to ignore or handle new future options
|
||||
case .e, .p, .q, .t, .d, .a: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a Relay Item based on raw information
|
||||
static func fromRawInfo(urlString: String, rwMarker: String?) throws(NIP65.NIP65DecodingError) -> NIP65.RelayList.RelayItem? {
|
||||
guard let relayUrl = RelayURL(urlString) else { throw .invalidRelayURL }
|
||||
guard let rwConfiguration = RWConfiguration.fromTagItem(rwMarker) else { throw .invalidRelayMarker }
|
||||
return NIP65.RelayList.RelayItem(url: relayUrl, rwConfiguration: rwConfiguration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP65.RelayList.RelayItem {
|
||||
/// The read/write configuration for a relay item
|
||||
enum RWConfiguration: TagItemConvertible {
|
||||
case read
|
||||
case write
|
||||
case readWrite
|
||||
|
||||
static let READ_MARKER: String = "read"
|
||||
static let WRITE_MARKER: String = "write"
|
||||
|
||||
var canRead: Bool {
|
||||
switch self {
|
||||
case .read, .readWrite: return true
|
||||
case .write: return false
|
||||
}
|
||||
}
|
||||
|
||||
var canWrite: Bool {
|
||||
switch self {
|
||||
case .write, .readWrite: return true
|
||||
case .read: return false
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw Nostr Event tag item
|
||||
var tagItem: String? {
|
||||
switch self {
|
||||
case .read: Self.READ_MARKER
|
||||
case .write: Self.WRITE_MARKER
|
||||
case .readWrite: nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize this from a raw Nostr Event tag item
|
||||
static func fromTagItem(_ item: String?) -> Self? {
|
||||
if item == READ_MARKER { return .read }
|
||||
if item == WRITE_MARKER { return .write }
|
||||
return .readWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
|
||||
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: RelayPool.Relay) -> NostrEvent? {
|
||||
let tags: [[String]] = [["relay", relay.descriptor.url.absoluteString],["challenge", challenge_string]]
|
||||
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
|
||||
return event
|
||||
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// NostrNetworkManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-26.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
/// Manages interactions with the Nostr Network.
|
||||
///
|
||||
/// This delineates a layer that is responsible for doing mid-level management of interactions with the Nostr network, controlling lower-level classes that perform more network/DB specific code, and providing an easier to use and more semantic interfaces for the rest of the app.
|
||||
///
|
||||
/// This is responsible for:
|
||||
/// - Managing the user's relay list
|
||||
/// - Establishing a `RelayPool` and maintaining it in sync with the user's relay list as it changes
|
||||
/// - Abstracting away complexities of interacting with the nostr network, providing an easier-to-use interface to fetch and send content related to the Nostr network
|
||||
///
|
||||
/// This is **NOT** responsible for:
|
||||
/// - Doing actual storage of relay list (delegated via the delegate
|
||||
/// - Handling low-level relay logic (this will be delegated to lower level classes used in RelayPool/RelayConnection)
|
||||
class NostrNetworkManager {
|
||||
/// The relay pool that we manage
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This will be marked `private` in the future to prevent other code from accessing the relay pool directly. Code outside this layer should use a higher level interface
|
||||
let pool: RelayPool // TODO: Make this private and make higher level interface for classes outside the NostrNetworkManager
|
||||
/// A delegate that allows us to interact with the rest of app without introducing hard or circular dependencies
|
||||
private var delegate: Delegate
|
||||
/// Manages the user's relay list, controls RelayPool's connected relays
|
||||
let userRelayList: UserRelayListManager
|
||||
/// Handles sending out notes to the network
|
||||
let postbox: PostBox
|
||||
/// Handles subscriptions and functions to read or consume data from the Nostr network
|
||||
let reader: SubscriptionManager
|
||||
|
||||
init(delegate: Delegate) {
|
||||
self.delegate = delegate
|
||||
let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair)
|
||||
self.pool = pool
|
||||
let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb)
|
||||
let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader)
|
||||
self.reader = reader
|
||||
self.userRelayList = userRelayList
|
||||
self.postbox = PostBox(pool: pool)
|
||||
}
|
||||
|
||||
// MARK: - Control functions
|
||||
|
||||
/// Connects the app to the Nostr network
|
||||
func connect() {
|
||||
self.userRelayList.connect()
|
||||
}
|
||||
|
||||
func relaysForEvent(event: NostrEvent) -> [RelayURL] {
|
||||
// TODO(tyiu) Ideally this list would be sorted by the event author's outbox relay preferences
|
||||
// and reliability of relays to maximize chances of others finding this event.
|
||||
if let relays = pool.seen[event.id] {
|
||||
return Array(relays)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper types
|
||||
|
||||
extension NostrNetworkManager {
|
||||
/// The delegate that provides information and structure for the `NostrNetworkManager` to function.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// This is needed to prevent a circular reference between `DamusState` and `NostrNetworkManager`, and reduce coupling.
|
||||
protocol Delegate: Sendable {
|
||||
/// NostrDB instance, used with `RelayPool` to send events for ingestion.
|
||||
var ndb: Ndb { get }
|
||||
|
||||
/// The keypair to use for relay authentication and updating relay lists
|
||||
var keypair: Keypair { get }
|
||||
|
||||
/// The latest relay list event id hex
|
||||
var latestRelayListEventIdHex: String? { get set } // TODO: Update this once we have full NostrDB query support
|
||||
|
||||
/// The latest contact list `NostrEvent`
|
||||
///
|
||||
/// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists.
|
||||
var latestContactListEvent: NostrEvent? { get }
|
||||
|
||||
/// Default bootstrap relays to start with when a user relay list is not present
|
||||
var bootstrapRelays: [RelayURL] { get }
|
||||
|
||||
/// Whether the app is in developer mode
|
||||
var developerMode: Bool { get }
|
||||
|
||||
/// The cache of relay model information
|
||||
var relayModelCache: RelayModelCache { get }
|
||||
|
||||
/// Relay filters
|
||||
var relayFilters: RelayFilters { get }
|
||||
|
||||
/// The user's connected NWC wallet
|
||||
var nwcWallet: WalletConnectURL? { get }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// SubscriptionManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-03-25.
|
||||
//
|
||||
|
||||
extension NostrNetworkManager {
|
||||
/// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This class will be a key part of the local relay model migration. Most higher-level code should fetch content from this class, which will properly setup the correct relay pool subscriptions, and provide a stream from NostrDB for higher performance and reliability.
|
||||
class SubscriptionManager {
|
||||
private let pool: RelayPool
|
||||
private var ndb: Ndb
|
||||
|
||||
init(pool: RelayPool, ndb: Ndb) {
|
||||
self.pool = pool
|
||||
self.ndb = ndb
|
||||
}
|
||||
|
||||
// MARK: - Reading data from Nostr
|
||||
|
||||
/// Subscribes to data from the user's relays
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - When we migrate to the local relay model, we should modify this function to stream directly from NostrDB
|
||||
///
|
||||
/// - Parameter filters: The nostr filters to specify what kind of data to subscribe to
|
||||
/// - Returns: An async stream of nostr data
|
||||
func subscribe(filters: [NostrFilter]) -> AsyncStream<StreamItem> {
|
||||
return AsyncStream<StreamItem> { continuation in
|
||||
let streamTask = Task {
|
||||
for await item in self.pool.subscribe(filters: filters) {
|
||||
switch item {
|
||||
case .eose: continuation.yield(.eose)
|
||||
case .event(let nostrEvent):
|
||||
// At this point of the pipeline, if the note is valid it should have been processed and verified by NostrDB,
|
||||
// in which case we should pull the note from NostrDB to ensure validity.
|
||||
// However, NdbNotes are unowned, so we return a function where our callers can temporarily borrow the NostrDB note
|
||||
let noteId = nostrEvent.id
|
||||
let lender: NdbNoteLender = { lend in
|
||||
guard let ndbNoteTxn = self.ndb.lookup_note(noteId) else {
|
||||
throw NdbNoteLenderError.errorLoadingNote
|
||||
}
|
||||
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else {
|
||||
throw NdbNoteLenderError.errorLoadingNote
|
||||
}
|
||||
lend(unownedNote)
|
||||
}
|
||||
continuation.yield(.event(borrow: lender))
|
||||
}
|
||||
}
|
||||
}
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
streamTask.cancel() // Close the RelayPool stream when caller stops streaming
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum StreamItem {
|
||||
/// An event which can be borrowed from NostrDB
|
||||
case event(borrow: NdbNoteLender)
|
||||
/// The end of stored events
|
||||
case eose
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// UserRelayListErrors.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension NostrNetworkManager.UserRelayListManager {
|
||||
/// Models an error that may occur when performing operations that change the user's relay list.
|
||||
///
|
||||
/// Callers to functions that throw this error SHOULD handle them in order to provide a better user experience.
|
||||
enum UpdateError: Error {
|
||||
/// The user is not authorized to change relay list, usually because the private key is missing.
|
||||
case notAuthorizedToChangeRelayList
|
||||
/// An error occurred when forming the relay list Nostr event.
|
||||
case cannotFormRelayListEvent
|
||||
/// Cannot add item to the relay list because the relay is already present in the list.
|
||||
case relayAlreadyExists
|
||||
/// Cannot update the relay list because we do not have the user's previous relay list.
|
||||
///
|
||||
/// Implementers must be careful not to overwrite the user's existing relay list if it exists somewhere else.
|
||||
case noInitialRelayList
|
||||
/// Cannot remove or update a specific relay because it is not on the relay list
|
||||
case noSuchRelay
|
||||
|
||||
/// Convert `RelayPool.RelayError` into `UserRelayListUpdateError`
|
||||
static func from(_ relayPoolError: RelayPool.RelayError) -> Self {
|
||||
switch relayPoolError {
|
||||
case .RelayAlreadyExists: return .relayAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
var humanReadableError: ErrorView.UserPresentableError {
|
||||
switch self {
|
||||
case .notAuthorizedToChangeRelayList:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("You do not have permission to alter this relay list.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("Please make sure you have logged-in with your private key.", comment: "Human readable tip for error"),
|
||||
technical_info: nil
|
||||
)
|
||||
case .cannotFormRelayListEvent:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("There was a problem creating the relay list event.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("Please try again later or contact support if the issue persists.", comment: "Human readable tip for error"),
|
||||
technical_info: "Failed forming Nostr event for the relay list update."
|
||||
)
|
||||
case .relayAlreadyExists:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("This relay is already in your list.", comment: "Human readable tip for error"),
|
||||
tip: NSLocalizedString("Check the address and/or the relay list.", comment: "Human readable tip for error"),
|
||||
technical_info: nil
|
||||
)
|
||||
case .noInitialRelayList:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("No initial relay list available to update.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("Please go to Settings > First Aid > Repair relay list, or contact support.", comment: "Human readable tip for error"),
|
||||
technical_info: "Missing initial relay list data for reference during update."
|
||||
)
|
||||
case .noSuchRelay:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("The specified relay that you are trying to udpate was not found in your relay list.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("This is an unexpected error, please contact support.", comment: "Human readable tip for error"),
|
||||
technical_info: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LoadingError: Error {
|
||||
case relayListParseError
|
||||
|
||||
var humanReadableError: ErrorView.UserPresentableError {
|
||||
switch self {
|
||||
case .relayListParseError:
|
||||
return ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("Your relay list appears to be broken, so we cannot connect you to your Nostr network.", comment: "Human readable error description for a failure to parse the relay list due to a bad relay list"),
|
||||
tip: NSLocalizedString("Please contact support for further help.", comment: "Human readable tips for what to do for a failure to find the relay list"),
|
||||
technical_info: "Relay list could not be parsed."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
//
|
||||
// UserRelayListManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension NostrNetworkManager {
|
||||
/// Manages the user's relay list
|
||||
///
|
||||
/// - It can compute the user's current relay list
|
||||
/// - It can compute the best relay list to connect to
|
||||
/// - It can edit the user's relay list
|
||||
class UserRelayListManager {
|
||||
private var delegate: Delegate
|
||||
private let pool: RelayPool
|
||||
private let reader: SubscriptionManager
|
||||
|
||||
private var relayListObserverTask: Task<Void, Never>? = nil
|
||||
private var walletUpdatesObserverTask: AnyCancellable? = nil
|
||||
|
||||
init(delegate: Delegate, pool: RelayPool, reader: SubscriptionManager) {
|
||||
self.delegate = delegate
|
||||
self.pool = pool
|
||||
self.reader = reader
|
||||
}
|
||||
|
||||
// MARK: - Computing the relays to connect to
|
||||
|
||||
private func relaysToConnectTo() -> [RelayPool.RelayDescriptor] {
|
||||
return self.computeRelaysToConnectTo(with: self.getBestEffortRelayList())
|
||||
}
|
||||
|
||||
private func computeRelaysToConnectTo(with relayList: NIP65.RelayList) -> [RelayPool.RelayDescriptor] {
|
||||
let regularRelayDescriptorList = relayList.toRelayDescriptors()
|
||||
if let nwcWallet = delegate.nwcWallet {
|
||||
return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)]
|
||||
}
|
||||
return regularRelayDescriptorList
|
||||
}
|
||||
|
||||
// MARK: - Getting the user's relay list
|
||||
|
||||
/// Gets the "best effort" relay list.
|
||||
///
|
||||
/// It attempts to get a relay list from the user. If one is not available, it uses the default bootstrap list.
|
||||
///
|
||||
/// This is always guaranteed to return a relay list.
|
||||
func getBestEffortRelayList() -> NIP65.RelayList {
|
||||
guard let userCurrentRelayList = self.getUserCurrentRelayList() else {
|
||||
return NIP65.RelayList(relays: delegate.bootstrapRelays)
|
||||
}
|
||||
return userCurrentRelayList
|
||||
}
|
||||
|
||||
/// Gets the user's current relay list.
|
||||
///
|
||||
/// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list.
|
||||
func getUserCurrentRelayList() -> NIP65.RelayList? {
|
||||
if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent }
|
||||
if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent }
|
||||
if let latestRelayListEvent = try? self.getLatestUserDefaultsRelayList() { return latestRelayListEvent }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Gets the latest NIP-65 relay list from NostrDB.
|
||||
///
|
||||
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||
///
|
||||
/// - Returns: The latest NIP-65 relay list object
|
||||
private func getLatestNIP65RelayList() throws(LoadingError) -> NIP65.RelayList? {
|
||||
guard let latestRelayListEvent = self.getLatestNIP65RelayListEvent() else { return nil }
|
||||
guard let list = try? NIP65.RelayList(event: latestRelayListEvent) else { throw .relayListParseError }
|
||||
return list
|
||||
}
|
||||
|
||||
/// Gets the latest NIP-65 relay list event from NostrDB.
|
||||
///
|
||||
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||
///
|
||||
/// It is recommended to use this function only if the NostrEvent metadata is needed. For cases where only the relay list info is needed, use `getLatestNIP65RelayList` instead.
|
||||
///
|
||||
/// - Returns: The latest NIP-65 relay list NdbNote
|
||||
private func getLatestNIP65RelayListEvent() -> NdbNote? {
|
||||
guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil }
|
||||
guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil }
|
||||
return delegate.ndb.lookup_note(latestRelayListEventId)?.unsafeUnownedValue?.to_owned()
|
||||
}
|
||||
|
||||
/// Gets the latest `kind:3` relay list from NostrDB.
|
||||
///
|
||||
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||
private func getLatestKind3RelayList() throws(LoadingError) -> NIP65.RelayList? {
|
||||
guard let latestContactListEvent = delegate.latestContactListEvent else { return nil }
|
||||
guard let legacyContactList = try? NIP65.RelayList.fromLegacyContactList(latestContactListEvent) else { throw .relayListParseError }
|
||||
return legacyContactList
|
||||
}
|
||||
|
||||
/// Gets the latest relay list from `UserDefaults`
|
||||
///
|
||||
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
|
||||
private func getLatestUserDefaultsRelayList() throws(LoadingError) -> NIP65.RelayList? {
|
||||
let key = bootstrap_relays_setting_key(pubkey: delegate.keypair.pubkey)
|
||||
guard let relays = UserDefaults.standard.stringArray(forKey: key) else { return nil }
|
||||
let relayUrls = relays.compactMap({ RelayURL($0) })
|
||||
if relayUrls.count == 0 { return nil }
|
||||
return NIP65.RelayList(relays: relayUrls)
|
||||
}
|
||||
|
||||
// MARK: - Getting metadata from the user's relay list
|
||||
|
||||
/// Gets the creation date of the user's current relay list, with preference to NIP-65 relay lists
|
||||
/// - Returns: The current relay list's creation date
|
||||
private func getUserCurrentRelayListCreationDate() -> UInt32? {
|
||||
if let latestNIP65RelayListEvent = self.getLatestNIP65RelayListEvent() { return latestNIP65RelayListEvent.created_at }
|
||||
if let latestKind3RelayListEvent = delegate.latestContactListEvent { return latestKind3RelayListEvent.created_at }
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Listening to and handling relay updates from the network
|
||||
|
||||
func connect() {
|
||||
self.load()
|
||||
|
||||
self.relayListObserverTask?.cancel()
|
||||
self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() }
|
||||
self.walletUpdatesObserverTask?.cancel()
|
||||
self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in self.load() }
|
||||
}
|
||||
|
||||
func listenAndHandleRelayUpdates() async {
|
||||
let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey])
|
||||
for await item in self.reader.subscribe(filters: [filter]) {
|
||||
switch item {
|
||||
case .event(borrow: let borrow): // Signature validity already ensured at this point
|
||||
let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate()
|
||||
try? borrow { note in
|
||||
guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours
|
||||
guard note.createdAt > (currentRelayListCreationDate ?? 0) else { return } // Ensure this is a newer list
|
||||
guard let relayList = try? NIP65.RelayList(event: note) else { return } // Ensure it is a valid NIP-65 list
|
||||
|
||||
try? self.set(userRelayList: relayList) // Set the validated list
|
||||
}
|
||||
case .eose: continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Editing the user's relay list
|
||||
|
||||
func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) throws(UpdateError) {
|
||||
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||
guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists }
|
||||
var newList = currentUserRelayList.relays
|
||||
newList[relay.url] = relay
|
||||
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
||||
}
|
||||
|
||||
func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) throws(UpdateError) {
|
||||
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||
guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists }
|
||||
try self.upsert(relay: relay, force: force)
|
||||
}
|
||||
|
||||
func remove(relayURL: RelayURL, force: Bool = false) throws(UpdateError) {
|
||||
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||
guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay }
|
||||
var newList = currentUserRelayList.relays
|
||||
newList[relayURL] = nil
|
||||
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
||||
}
|
||||
|
||||
func set(userRelayList: NIP65.RelayList) throws(UpdateError) {
|
||||
guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList }
|
||||
guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent }
|
||||
|
||||
self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList))
|
||||
|
||||
self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event
|
||||
self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB
|
||||
}
|
||||
|
||||
// MARK: - Syncing our saved user relay list with the active `RelayPool`
|
||||
|
||||
/// Loads the current user relay list
|
||||
func load() {
|
||||
self.apply(newRelayList: self.relaysToConnectTo())
|
||||
}
|
||||
|
||||
/// Loads a new relay list into the active relay pool, making sure it matches the specified relay list.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - state: The state of the app
|
||||
/// - newRelayList: The new relay list to be applied
|
||||
///
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This is `private` because syncing the user's saved relay list with the relay pool is `NostrNetworkManager`'s responsibility,
|
||||
/// so we do not want other classes to forcibly load this.
|
||||
private func apply(newRelayList: [RelayPool.RelayDescriptor]) {
|
||||
let currentRelayList = self.pool.relays.map({ $0.descriptor })
|
||||
|
||||
var changed = false
|
||||
let new_relay_filters = load_relay_filters(delegate.keypair.pubkey) == nil
|
||||
|
||||
for index in self.pool.relays.indices {
|
||||
guard let newDescriptor = newRelayList.first(where: { $0.url == self.pool.relays[index].descriptor.url }) else { continue }
|
||||
self.pool.relays[index].descriptor.info = newDescriptor.info
|
||||
// Relay read-write configuration change does not need reconnection to the relay, so we do not set the `changed` flag.
|
||||
}
|
||||
|
||||
// Working with URL Sets for difference analysis
|
||||
let currentRelayURLs = Set(currentRelayList.map { $0.url })
|
||||
let newRelayURLs = Set(newRelayList.map { $0.url })
|
||||
|
||||
// Analyzing which relays to add or remove
|
||||
let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs)
|
||||
let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs)
|
||||
|
||||
// Remove relays not in the new list
|
||||
relaysToRemove.forEach { url in
|
||||
pool.remove_relay(url)
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Add new relays from the new list
|
||||
relaysToAdd.forEach { url in
|
||||
guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return }
|
||||
add_new_relay(
|
||||
model_cache: delegate.relayModelCache,
|
||||
relay_filters: delegate.relayFilters,
|
||||
pool: pool,
|
||||
descriptor: descriptor,
|
||||
new_relay_filters: new_relay_filters,
|
||||
logging_enabled: delegate.developerMode
|
||||
)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
pool.connect()
|
||||
notify(.relays_changed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper extensions
|
||||
|
||||
fileprivate extension NIP65.RelayList.RelayItem {
|
||||
func toRelayDescriptor() -> RelayPool.RelayDescriptor {
|
||||
return RelayPool.RelayDescriptor(url: self.url, info: self.rwConfiguration, variant: .regular) // NIP-65 relays are regular by definition.
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension NIP65.RelayList {
|
||||
func toRelayDescriptors() -> [RelayPool.RelayDescriptor] {
|
||||
return self.relays.values.map({ $0.toRelayDescriptor() })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper functions
|
||||
|
||||
|
||||
/// Adds a new relay, taking care of other tangential concerns, such as updating the relay model cache, configuring logging, etc
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// 1. This function used to be in `HomeModel.swift` and moved here when `UserRelayListManager` was first implemented
|
||||
/// 2. This is `fileprivate` because only `UserRelayListManager` should be able to manage the user's relay list and apply them to the `RelayPool`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - model_cache: The relay model cache, that keeps metadata cached
|
||||
/// - relay_filters: Relay filters
|
||||
/// - pool: The relay pool to add this in
|
||||
/// - descriptor: The description of the relay being added
|
||||
/// - new_relay_filters: Whether to insert new relay filters
|
||||
/// - logging_enabled: Whether logging is enabled
|
||||
fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
|
||||
try? pool.add_relay(descriptor)
|
||||
let url = descriptor.url
|
||||
|
||||
let relay_id = url
|
||||
guard model_cache.model(withURL: url) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
Task.detached(priority: .background) {
|
||||
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
let model = RelayModel(url, metadata: meta)
|
||||
model_cache.insert(model: model)
|
||||
|
||||
if logging_enabled {
|
||||
pool.setLog(model.log, for: relay_id)
|
||||
}
|
||||
|
||||
// if this is the first time adding filters, we should filter non-paid relays
|
||||
if new_relay_filters && !meta.is_paid {
|
||||
relay_filters.insert(timeline: .search, relay_id: relay_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,45 +20,6 @@ enum NoteContent {
|
||||
}
|
||||
}
|
||||
|
||||
func parsed_blocks_finish(bs: inout note_blocks, tags: TagsSequence?) -> Blocks {
|
||||
var out: [Block] = []
|
||||
|
||||
var i = 0
|
||||
while (i < bs.num_blocks) {
|
||||
let block = bs.blocks[i]
|
||||
|
||||
if let converted = Block(block, tags: tags) {
|
||||
out.append(converted)
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
let words = Int(bs.words)
|
||||
blocks_free(&bs)
|
||||
|
||||
return Blocks(words: words, blocks: out)
|
||||
|
||||
}
|
||||
|
||||
func parse_note_content(content: NoteContent) -> Blocks {
|
||||
var bs = note_blocks()
|
||||
bs.num_blocks = 0;
|
||||
|
||||
blocks_init(&bs)
|
||||
|
||||
switch content {
|
||||
case .content(let s, let tags):
|
||||
return s.withCString { cptr in
|
||||
damus_parse_content(&bs, cptr)
|
||||
return parsed_blocks_finish(bs: &bs, tags: tags)
|
||||
}
|
||||
case .note(let note):
|
||||
damus_parse_content(&bs, note.content_raw)
|
||||
return parsed_blocks_finish(bs: &bs, tags: note.tags)
|
||||
}
|
||||
}
|
||||
|
||||
func interpret_event_refs(tags: TagsSequence) -> ThreadReply? {
|
||||
// migration is long over, lets just do this to fix tests
|
||||
return interpret_event_refs_ndb(tags: tags)
|
||||
@@ -34,6 +34,19 @@ protocol TagConvertible {
|
||||
static func from_tag(tag: TagSequence) -> Self?
|
||||
}
|
||||
|
||||
/// Protocol for types that can be converted from/to a tag sequence with the possibilty of an error
|
||||
protocol ThrowingTagConvertible {
|
||||
associatedtype E: Error
|
||||
var tag: [String] { get }
|
||||
static func fromTag(tag: TagSequence) throws(E) -> Self?
|
||||
}
|
||||
|
||||
/// Protocol for types that can be converted from/to a tag item
|
||||
protocol TagItemConvertible {
|
||||
var tagItem: String? { get }
|
||||
static func fromTagItem(_ item: String?) -> Self?
|
||||
}
|
||||
|
||||
struct QuoteId: IdType, TagKey, TagConvertible {
|
||||
let id: Data
|
||||
|
||||
@@ -130,8 +143,16 @@ struct ReplaceableParam: TagConvertible {
|
||||
var keychar: AsciiCharacter { "d" }
|
||||
}
|
||||
|
||||
struct Signature: Hashable, Equatable {
|
||||
struct Signature: Codable, Hashable, Equatable {
|
||||
let data: Data
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
self.init(try hex_decoder(decoder, expected_len: 64))
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
try hex_encoder(to: encoder, data: self.data)
|
||||
}
|
||||
|
||||
init(_ p: Data) {
|
||||
self.data = p
|
||||
@@ -18,22 +18,42 @@ enum MentionType: AsciiCharacter, TagKey {
|
||||
}
|
||||
}
|
||||
|
||||
enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
||||
case pubkey(Pubkey)
|
||||
case note(NoteId)
|
||||
case nevent(NEvent)
|
||||
case nprofile(NProfile)
|
||||
case nrelay(String)
|
||||
case naddr(NAddr)
|
||||
extension UnsafePointer<UInt8> {
|
||||
func as_data(size: Int) -> Data {
|
||||
return Data(bytes: self, count: size)
|
||||
}
|
||||
}
|
||||
|
||||
struct MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
||||
let nip19: Bech32Object
|
||||
|
||||
static func pubkey(_ pubkey: Pubkey) -> MentionRef {
|
||||
self.init(nip19: .npub(pubkey))
|
||||
}
|
||||
|
||||
static func note(_ note_id: NoteId) -> MentionRef {
|
||||
return self.init(nip19: .note(note_id))
|
||||
}
|
||||
|
||||
init?(block: ndb_mention_bech32_block) {
|
||||
guard let bech32_obj = Bech32Object.init(block: block) else {
|
||||
return nil
|
||||
}
|
||||
self.nip19 = bech32_obj
|
||||
}
|
||||
|
||||
init(nip19: Bech32Object) {
|
||||
self.nip19 = nip19
|
||||
}
|
||||
|
||||
var key: MentionType {
|
||||
switch self {
|
||||
case .pubkey: return .p
|
||||
case .note: return .e
|
||||
case .nevent: return .e
|
||||
case .nprofile: return .p
|
||||
switch self.nip19 {
|
||||
case .note, .nevent: return .e
|
||||
case .nprofile, .npub: return .p
|
||||
case .nrelay: return .r
|
||||
case .naddr: return .a
|
||||
case .nscript: return .a
|
||||
case .nsec: return .p
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,33 +61,64 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
||||
return Bech32Object.encode(toBech32Object())
|
||||
}
|
||||
|
||||
static func from_bech32(str: String) -> MentionRef? {
|
||||
switch Bech32Object.parse(str) {
|
||||
case .note(let noteid): return .note(noteid)
|
||||
case .npub(let pubkey): return .pubkey(pubkey)
|
||||
default: return nil
|
||||
init?(bech32_str: String) {
|
||||
guard let obj = Bech32Object.parse(bech32_str) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.nip19 = obj
|
||||
}
|
||||
|
||||
var pubkey: Pubkey? {
|
||||
switch self {
|
||||
case .pubkey(let pubkey): return pubkey
|
||||
switch self.nip19 {
|
||||
case .npub(let pubkey): return pubkey
|
||||
case .note: return nil
|
||||
case .nevent(let nevent): return nevent.author
|
||||
case .nprofile(let nprofile): return nprofile.author
|
||||
case .nrelay: return nil
|
||||
case .naddr: return nil
|
||||
case .nsec(let prv): return privkey_to_pubkey(privkey: prv)
|
||||
case .nscript(_): return nil
|
||||
}
|
||||
}
|
||||
|
||||
var tag: [String] {
|
||||
switch self {
|
||||
case .pubkey(let pubkey): return ["p", pubkey.hex()]
|
||||
switch self.nip19 {
|
||||
case .npub(let pubkey): return ["p", pubkey.hex()]
|
||||
case .note(let noteId): return ["e", noteId.hex()]
|
||||
case .nevent(let nevent): return ["e", nevent.noteid.hex()]
|
||||
case .nprofile(let nprofile): return ["p", nprofile.author.hex()]
|
||||
case .nevent(let nevent):
|
||||
var tagBuilder = ["e", nevent.noteid.hex()]
|
||||
|
||||
let relay = nevent.relays.first
|
||||
if let author = nevent.author?.hex() {
|
||||
tagBuilder.append(relay?.absoluteString ?? "")
|
||||
tagBuilder.append(author)
|
||||
} else if let relay {
|
||||
tagBuilder.append(relay.absoluteString)
|
||||
}
|
||||
|
||||
return tagBuilder
|
||||
case .nprofile(let nprofile):
|
||||
var tagBuilder = ["p", nprofile.author.hex()]
|
||||
|
||||
if let relay = nprofile.relays.first {
|
||||
tagBuilder.append(relay.absoluteString)
|
||||
}
|
||||
|
||||
return tagBuilder
|
||||
case .nrelay(let url): return ["r", url]
|
||||
case .naddr(let naddr): return ["a", naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier.string()]
|
||||
case .naddr(let naddr):
|
||||
var tagBuilder = ["a", "\(naddr.kind.description):\(naddr.author.hex()):\(naddr.identifier.string())"]
|
||||
|
||||
if let relay = naddr.relays.first {
|
||||
tagBuilder.append(relay.absoluteString)
|
||||
}
|
||||
|
||||
return tagBuilder
|
||||
case .nsec(_):
|
||||
return []
|
||||
case .nscript(_):
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,10 +138,10 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
||||
switch mention_type {
|
||||
case .p:
|
||||
guard let data = element.id() else { return nil }
|
||||
return .pubkey(Pubkey(data))
|
||||
return .init(nip19: .npub(Pubkey(data)))
|
||||
case .e:
|
||||
guard let data = element.id() else { return nil }
|
||||
return .note(NoteId(data))
|
||||
return .init(nip19: .note(NoteId(data)))
|
||||
case .a:
|
||||
let str = element.string()
|
||||
let data = str.split(separator: ":")
|
||||
@@ -99,26 +150,13 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
||||
guard let pubkey = Pubkey(hex: String(data[1])) else { return nil }
|
||||
guard let kind = UInt32(data[0]) else { return nil }
|
||||
|
||||
return .naddr(NAddr(identifier: String(data[2]), author: pubkey, relays: [], kind: kind))
|
||||
case .r: return .nrelay(element.string())
|
||||
return .init(nip19: .naddr(NAddr(identifier: String(data[2]), author: pubkey, relays: [], kind: kind)))
|
||||
case .r: return .init(nip19: .nrelay(element.string()))
|
||||
}
|
||||
}
|
||||
|
||||
func toBech32Object() -> Bech32Object {
|
||||
switch self {
|
||||
case .pubkey(let pk):
|
||||
return .npub(pk)
|
||||
case .note(let noteid):
|
||||
return .note(noteid)
|
||||
case .naddr(let naddr):
|
||||
return .naddr(naddr)
|
||||
case .nevent(let nevent):
|
||||
return .nevent(nevent)
|
||||
case .nprofile(let nprofile):
|
||||
return .nprofile(nprofile)
|
||||
case .nrelay(let url):
|
||||
return .nrelay(url)
|
||||
}
|
||||
self.nip19
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,9 +198,12 @@ struct LightningInvoice<T> {
|
||||
let amount: T
|
||||
let string: String
|
||||
let expiry: UInt64
|
||||
let payment_hash: Data
|
||||
let created_at: UInt64
|
||||
|
||||
var abbreviated: String {
|
||||
return self.string.prefix(8) + "…" + self.string.suffix(8)
|
||||
}
|
||||
|
||||
var description_string: String {
|
||||
switch description {
|
||||
case .description(let string):
|
||||
@@ -171,10 +212,21 @@ struct LightningInvoice<T> {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
static func from(string: String) -> Invoice? {
|
||||
// This feels a bit hacky at first, but it is actually clean
|
||||
// because it reuses the same well-tested parsing logic as the rest of the app,
|
||||
// avoiding code duplication and utilizing the guarantees acquired from age and testing.
|
||||
// We could also use the C function `parse_invoice`, but it requires extra C bridging logic.
|
||||
// NDBTODO: This may need updating on the nostrdb upgrade.
|
||||
guard let parsedBlocks = parse_note_content(content: .content(string,nil))?.blocks else { return nil }
|
||||
guard parsedBlocks.count == 1 else { return nil }
|
||||
return parsedBlocks[0].asInvoice
|
||||
}
|
||||
}
|
||||
|
||||
func maybe_pointee<T>(_ p: UnsafeMutablePointer<T>!) -> T? {
|
||||
guard p != nil else {
|
||||
func maybe_pointee<T>(_ p: UnsafeMutablePointer<T>?) -> T? {
|
||||
guard let p else {
|
||||
return nil
|
||||
}
|
||||
return p.pointee
|
||||
@@ -192,6 +244,13 @@ enum Amount: Equatable {
|
||||
return format_msats(amt)
|
||||
}
|
||||
}
|
||||
|
||||
func amount_sats() -> Int64? {
|
||||
switch self {
|
||||
case .any: nil
|
||||
case .specific(let amount): amount / 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func format_msats_abbrev(_ msats: Int64) -> String {
|
||||
@@ -235,7 +294,7 @@ func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
|
||||
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||
}
|
||||
|
||||
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
func convert_invoice_description(b11: ndb_invoice) -> InvoiceDescription? {
|
||||
if let desc = b11.description {
|
||||
return .description(String(cString: desc))
|
||||
}
|
||||
@@ -260,3 +319,38 @@ func find_tag_ref(type: String, id: String, tags: [[String]]) -> Int? {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
struct PostTags {
|
||||
let blocks: [Block]
|
||||
let tags: [[String]]
|
||||
}
|
||||
|
||||
/// Convert
|
||||
func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
|
||||
var new_tags = tags
|
||||
|
||||
for post_block in post_blocks {
|
||||
switch post_block {
|
||||
case .mention(let mention):
|
||||
switch(mention.ref.nip19) {
|
||||
case .note, .nevent:
|
||||
continue
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
new_tags.append(mention.ref.tag)
|
||||
case .hashtag(let hashtag):
|
||||
new_tags.append(["t", hashtag.lowercased()])
|
||||
case .text: break
|
||||
case .invoice: break
|
||||
case .relay: break
|
||||
case .url(let url):
|
||||
new_tags.append(["r", url.absoluteString])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return PostTags(blocks: post_blocks, tags: new_tags)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ class ProfileRecord {
|
||||
}
|
||||
|
||||
guard let profile = data.profile,
|
||||
let addr = profile.lud16 ?? profile.lud06 else {
|
||||
let addr = (profile.lud16 ?? profile.lud06)?.trimmingCharacters(in: .whitespaces)
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ extension NdbProfile {
|
||||
}
|
||||
|
||||
static func displayName(profile: Profile?, pubkey: Pubkey) -> DisplayName {
|
||||
return parse_display_name(profile: profile, pubkey: pubkey)
|
||||
return DisplayName(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
|
||||
}
|
||||
|
||||
var damus_donation: Int? {
|
||||
@@ -301,7 +302,7 @@ class Profile: Codable {
|
||||
*/
|
||||
|
||||
func make_test_profile() -> Profile {
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: nil, lud16: "jb55@jb55.com", nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
}
|
||||
|
||||
func make_ln_url(_ str: String?) -> URL? {
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayPool.RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
||||
var tags = zap_target_to_tags(target)
|
||||
var relay_tag = ["relays"]
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||
@@ -68,7 +68,7 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
|
||||
|
||||
guard let note = NostrEvent(content: message, keypair: identity.to_keypair(), kind: 9733, tags: tags),
|
||||
let note_json = encode_json(note),
|
||||
let enc = encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||
let enc = NIP04.encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
@@ -78,8 +78,8 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
|
||||
|
||||
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
|
||||
let rw_relay_info = RelayInfo(read: true, write: true)
|
||||
var relays: [RelayURL: RelayInfo] = [:]
|
||||
let rw_relay_info = LegacyKind3RelayRWConfiguration(read: true, write: true)
|
||||
var relays: [RelayURL: LegacyKind3RelayRWConfiguration] = [:]
|
||||
|
||||
for relay in bootstrap_relays {
|
||||
relays[relay] = rw_relay_info
|
||||
@@ -13,6 +13,18 @@ import CryptoKit
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
/// A protocol for structs and classes that can convert themselves from/to a NostrEvent
|
||||
protocol NostrEventConvertible {
|
||||
associatedtype E: Error
|
||||
|
||||
/// Iniitialize this type from a NostrEvent
|
||||
init(event: NostrEvent) throws(E)
|
||||
|
||||
/// Convert this type into a Nostr Event, using a keypair for signing and a specific timestamp
|
||||
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32?) -> NostrEvent?
|
||||
}
|
||||
|
||||
|
||||
enum ValidationResult: Decodable {
|
||||
case unknown
|
||||
case ok
|
||||
@@ -322,6 +334,27 @@ func decode_nostr_event(txt: String) -> NostrResponse? {
|
||||
return NostrResponse.owned_from_json(json: txt)
|
||||
}
|
||||
|
||||
func decode_and_verify_nostr_response(txt: String) -> NostrResponse? {
|
||||
guard let response = NostrResponse.owned_from_json(json: txt) else { return nil }
|
||||
guard verify_nostr_response(response: response) == true else { return nil }
|
||||
return response
|
||||
}
|
||||
|
||||
func verify_nostr_response(response: borrowing NostrResponse) -> Bool {
|
||||
switch response {
|
||||
case .event(_, let event):
|
||||
return event.verify()
|
||||
case .notice(_):
|
||||
return true
|
||||
case .eose(_):
|
||||
return true
|
||||
case .ok(_):
|
||||
return true
|
||||
case .auth(_):
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func encode_json<T: Encodable>(_ val: T) -> String? {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .withoutEscapingSlashes
|
||||
@@ -367,6 +400,10 @@ func decode_json<T: Decodable>(_ val: String) -> T? {
|
||||
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
|
||||
}
|
||||
|
||||
func decode_json_throwing<T: Decodable>(_ val: String) throws -> T {
|
||||
return try JSONDecoder().decode(T.self, from: Data(val.utf8))
|
||||
}
|
||||
|
||||
func decode_data<T: Decodable>(_ data: Data) -> T? {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
@@ -432,17 +469,26 @@ func random_bytes(count: Int) -> Data {
|
||||
return Data(bytes: bytes, count: count)
|
||||
}
|
||||
|
||||
func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent? {
|
||||
func make_boost_event(keypair: FullKeypair, boosted: NostrEvent, relayURL: RelayURL?) -> NostrEvent? {
|
||||
var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag })
|
||||
|
||||
tags.append(["e", boosted.id.hex(), "", "root"])
|
||||
tags.append(["p", boosted.pubkey.hex()])
|
||||
var eTagBuilder = ["e", boosted.id.hex()]
|
||||
var pTagBuilder = ["p", boosted.pubkey.hex()]
|
||||
|
||||
let relayURLString = relayURL?.absoluteString
|
||||
if let relayURLString {
|
||||
pTagBuilder.append(relayURLString)
|
||||
}
|
||||
eTagBuilder.append(contentsOf: [relayURLString ?? "", "root", boosted.pubkey.hex()])
|
||||
|
||||
tags.append(eTagBuilder)
|
||||
tags.append(pTagBuilder)
|
||||
|
||||
let content = event_to_json(ev: boosted)
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags)
|
||||
}
|
||||
|
||||
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? {
|
||||
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙", relayURL: RelayURL?) -> NostrEvent? {
|
||||
var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in
|
||||
guard tag.count >= 2,
|
||||
(tag[0].matches_char("e") || tag[0].matches_char("p")) else {
|
||||
@@ -451,8 +497,17 @@ func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String =
|
||||
ts.append(tag.strings())
|
||||
}
|
||||
|
||||
tags.append(["e", liked.id.hex()])
|
||||
tags.append(["p", liked.pubkey.hex()])
|
||||
var eTagBuilder = ["e", liked.id.hex()]
|
||||
var pTagBuilder = ["p", liked.pubkey.hex()]
|
||||
|
||||
let relayURLString = relayURL?.absoluteString
|
||||
if let relayURLString {
|
||||
pTagBuilder.append(relayURLString)
|
||||
}
|
||||
eTagBuilder.append(contentsOf: [relayURLString ?? "", liked.pubkey.hex()])
|
||||
|
||||
tags.append(eTagBuilder)
|
||||
tags.append(pTagBuilder)
|
||||
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags)
|
||||
}
|
||||
@@ -484,6 +539,15 @@ func uniq<T: Hashable>(_ xs: [T]) -> [T] {
|
||||
return ys
|
||||
}
|
||||
|
||||
func gather_quote_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
|
||||
var ids: [RefId] = [.quote(from.id.quote_id)]
|
||||
if from.pubkey != our_pubkey {
|
||||
ids.append(.pubkey(from.pubkey))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
|
||||
func gather_reply_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
|
||||
var ids: [RefId] = from.referenced_ids.first.map({ ref in [ .event(ref) ] }) ?? []
|
||||
|
||||
@@ -504,14 +568,6 @@ func gather_reply_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
|
||||
return ids
|
||||
}
|
||||
|
||||
func gather_quote_ids(our_pubkey: Pubkey, from: NostrEvent) -> [RefId] {
|
||||
var ids: [RefId] = [.quote(from.id.quote_id)]
|
||||
if from.pubkey != our_pubkey {
|
||||
ids.append(.pubkey(from.pubkey))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func event_from_json(dat: String) -> NostrEvent? {
|
||||
return NostrEvent.owned_from_json(json: dat)
|
||||
}
|
||||
@@ -527,6 +583,7 @@ func event_to_json(ev: NostrEvent) -> String {
|
||||
return str
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "NIP04.decryptContent", message: "Deprecated, please use NIP04.decryptContent instead")
|
||||
func decrypt_dm(_ privkey: Privkey?, pubkey: Pubkey, content: String, encoding: EncEncoding) -> String? {
|
||||
guard let privkey = privkey else {
|
||||
return nil
|
||||
@@ -743,42 +800,42 @@ func validate_event(ev: NostrEvent) -> ValidationResult {
|
||||
return ok ? .ok : .bad_sig
|
||||
}
|
||||
|
||||
func first_eref_mention(ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? {
|
||||
let blocks = ev.blocks(keypair).blocks.filter { block in
|
||||
guard case .mention(let mention) = block else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch mention.ref {
|
||||
case .note, .nevent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func first_eref_mention(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> Mention<NoteId>? {
|
||||
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else { return nil }
|
||||
|
||||
/// MARK: - Preview
|
||||
if let firstBlock = blocks.first,
|
||||
case .mention(let mention) = firstBlock {
|
||||
switch mention.ref {
|
||||
case .note(let note_id):
|
||||
return .note(note_id)
|
||||
case .nevent(let nevent):
|
||||
return .note(nevent.noteid)
|
||||
return blockGroup.forEachBlock({ index, block in
|
||||
switch block {
|
||||
case .mention(let mention):
|
||||
guard let mention = MentionRef(block: mention) else { return .loopContinue }
|
||||
switch mention.nip19 {
|
||||
case .note(let noteId):
|
||||
return .loopReturn(Mention<NoteId>.note(noteId, index: index))
|
||||
case .nevent(let nEvent):
|
||||
return .loopReturn(Mention<NoteId>.note(nEvent.noteid, index: index))
|
||||
default:
|
||||
return .loopContinue
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
return .loopContinue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func separate_invoices(ev: NostrEvent, keypair: Keypair) -> [Invoice]? {
|
||||
let invoiceBlocks: [Invoice] = ev.blocks(keypair).blocks.reduce(into: []) { invoices, block in
|
||||
guard case .invoice(let invoice) = block else {
|
||||
return
|
||||
}
|
||||
invoices.append(invoice)
|
||||
func separate_invoices(ndb: Ndb, ev: NostrEvent, keypair: Keypair) -> [Invoice]? {
|
||||
guard let blockGroup = try? NdbBlockGroup.from(event: ev, using: ndb, and: keypair) else {
|
||||
return nil
|
||||
}
|
||||
let invoiceBlocks: [Invoice] = (try? blockGroup.reduce(initialResult: [Invoice](), { index, invoices, block in
|
||||
switch block {
|
||||
case .invoice(let invoice):
|
||||
if let invoice = invoice.as_invoice() {
|
||||
return .loopReturn(invoices + [invoice])
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return .loopContinue
|
||||
})) ?? []
|
||||
return invoiceBlocks.isEmpty ? nil : invoiceBlocks
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
/// A known Nostr event kind, addressable by name, with the actual number assigned by the protocol as the value
|
||||
enum NostrKind: UInt32, Codable {
|
||||
case metadata = 0
|
||||
case text = 1
|
||||
@@ -18,6 +19,8 @@ enum NostrKind: UInt32, Codable {
|
||||
case like = 7
|
||||
case chat = 42
|
||||
case mute_list = 10000
|
||||
case relay_list = 10002
|
||||
case interest_list = 10015
|
||||
case list_deprecated = 30000
|
||||
case draft = 31234
|
||||
case longform = 30023
|
||||
@@ -28,4 +31,5 @@ enum NostrKind: UInt32, Codable {
|
||||
case nwc_response = 23195
|
||||
case http_auth = 27235
|
||||
case status = 30315
|
||||
case follow_list = 39089
|
||||
}
|
||||
@@ -12,6 +12,7 @@ enum NostrLink: Equatable {
|
||||
case ref(RefId)
|
||||
case filter(NostrFilter)
|
||||
case script([UInt8])
|
||||
case invoice(String)
|
||||
}
|
||||
|
||||
func encode_pubkey_uri(_ pubkey: Pubkey) -> String {
|
||||
@@ -93,8 +94,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
return
|
||||
}
|
||||
|
||||
if parts.count >= 2 && parts[0] == "t" {
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
if parts.count >= 2 {
|
||||
switch parts[0] {
|
||||
case "t":
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
case "lightning":
|
||||
return .invoice(parts[1])
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard parts.count == 1 else {
|
||||
@@ -12,11 +12,14 @@ struct NostrSubscribe {
|
||||
let sub_id: String
|
||||
}
|
||||
|
||||
|
||||
/// Models a request/message that is sent to a Nostr relay
|
||||
enum NostrRequestType {
|
||||
/// A standard nostr request
|
||||
case typical(NostrRequest)
|
||||
/// A customized nostr request. Generally used in the context of a nostrscript.
|
||||
case custom(String)
|
||||
|
||||
/// Whether this request is meant to write data to a relay
|
||||
var is_write: Bool {
|
||||
guard case .typical(let req) = self else {
|
||||
return true
|
||||
@@ -25,6 +28,7 @@ enum NostrRequestType {
|
||||
return req.is_write
|
||||
}
|
||||
|
||||
/// Whether this request is meant to read data from a relay
|
||||
var is_read: Bool {
|
||||
guard case .typical(let req) = self else {
|
||||
return true
|
||||
@@ -34,12 +38,18 @@ enum NostrRequestType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Models a standard request/message that is sent to a Nostr relay.
|
||||
enum NostrRequest {
|
||||
/// Subscribes to receive information from the relay
|
||||
case subscribe(NostrSubscribe)
|
||||
/// Unsubscribes from an existing subscription, addressed by its id
|
||||
case unsubscribe(String)
|
||||
/// Posts an event
|
||||
case event(NostrEvent)
|
||||
/// Authenticate with the relay
|
||||
case auth(NostrEvent)
|
||||
|
||||
/// Whether this request is meant to write data to a relay
|
||||
var is_write: Bool {
|
||||
switch self {
|
||||
case .subscribe:
|
||||
@@ -53,6 +63,7 @@ enum NostrRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this request is meant to read data from a relay
|
||||
var is_read: Bool {
|
||||
return !is_write
|
||||
}
|
||||
@@ -45,7 +45,7 @@ enum NostrResponse {
|
||||
|
||||
static func owned_from_json(json: String) -> NostrResponse? {
|
||||
return json.withCString{ cstr in
|
||||
let bufsize: Int = max(Int(Double(json.utf8.count) * 4.0), Int(getpagesize()))
|
||||
let bufsize: Int = max(Int(Double(json.utf8.count) * 8.0), Int(getpagesize()))
|
||||
let data = malloc(bufsize)
|
||||
|
||||
if data == nil {
|
||||
@@ -89,7 +89,7 @@ enum NostrResponse {
|
||||
free(data)
|
||||
return nil
|
||||
}
|
||||
let new_note = note_data.assumingMemoryBound(to: ndb_note.self)
|
||||
let new_note = ndb_note_ptr(ptr: OpaquePointer(note_data))
|
||||
let note = NdbNote(note: new_note, size: Int(len), owned: true, key: nil)
|
||||
|
||||
guard let subid = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
|
||||
@@ -35,6 +35,7 @@ class Profiles {
|
||||
@MainActor
|
||||
private var profiles: [Pubkey: ProfileData] = [:]
|
||||
|
||||
// Map of validated NIP-05 address to pubkey.
|
||||
@MainActor
|
||||
var nip05_pubkey: [String: Pubkey] = [:]
|
||||
|
||||
@@ -115,6 +115,19 @@ enum FollowRef: TagKeys, Hashable, TagConvertible, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Models common tag references defined by the Nostr protocol, and their associated values.
|
||||
///
|
||||
/// For example, this raw JSON tag sequence:
|
||||
/// ```json
|
||||
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
|
||||
/// ```
|
||||
///
|
||||
/// would be parsed into something equivalent to `.pubkey(Pubkey(hex: "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"))`
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// - Not all tag information from all NIPs can be modelled using this alone, as some NIPs may define extra associated values for specific event types. You may need to use a specialized type for some event kinds
|
||||
///
|
||||
enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
case event(NoteId)
|
||||
case pubkey(Pubkey)
|
||||
@@ -124,6 +137,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
case naddr(NAddr)
|
||||
case reference(String)
|
||||
|
||||
/// The key that defines the type of reference being made
|
||||
var key: RefKey {
|
||||
switch self {
|
||||
case .event: return .e
|
||||
@@ -136,6 +150,14 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the type of reference being made on a Nostr event tag
|
||||
///
|
||||
/// Example:
|
||||
/// ```json
|
||||
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
|
||||
/// ```
|
||||
///
|
||||
/// The `RefKey` is "p"
|
||||
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
||||
case e, p, t, d, q, a, r
|
||||
|
||||
@@ -148,10 +170,12 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw nostr-style tag sequence representation of this object
|
||||
var tag: [String] {
|
||||
[self.key.description, self.description]
|
||||
}
|
||||
|
||||
|
||||
/// Describes what is being referenced, as a `String`
|
||||
var description: String {
|
||||
switch self {
|
||||
case .event(let noteId): return noteId.hex()
|
||||
@@ -166,6 +190,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a raw tag sequence
|
||||
static func from_tag(tag: TagSequence) -> RefId? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
196
damus/Core/Nostr/Relay.swift
Normal file
196
damus/Core/Nostr/Relay.swift
Normal file
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// Relay.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct LegacyKind3RelayRWConfiguration: Codable, Sendable {
|
||||
public let read: Bool?
|
||||
public let write: Bool?
|
||||
|
||||
init(read: Bool, write: Bool) {
|
||||
self.read = read
|
||||
self.write = write
|
||||
}
|
||||
|
||||
static let rw = LegacyKind3RelayRWConfiguration(read: true, write: true)
|
||||
|
||||
func toNIP65RWConfiguration() -> NIP65.RelayList.RelayItem.RWConfiguration? {
|
||||
switch (self.read, self.write) {
|
||||
case (false, true): return .write
|
||||
case (true, false): return .read
|
||||
case (true, true): return .readWrite
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RelayVariant {
|
||||
case regular
|
||||
case ephemeral
|
||||
case nwc
|
||||
}
|
||||
|
||||
extension RelayPool {
|
||||
/// Describes a relay for use in `RelayPool`
|
||||
public struct RelayDescriptor {
|
||||
let url: RelayURL
|
||||
var info: NIP65.RelayList.RelayItem.RWConfiguration
|
||||
let variant: RelayVariant
|
||||
|
||||
init(url: RelayURL, info: NIP65.RelayList.RelayItem.RWConfiguration, variant: RelayVariant = .regular) {
|
||||
self.url = url
|
||||
self.info = info
|
||||
self.variant = variant
|
||||
}
|
||||
|
||||
var ephemeral: Bool {
|
||||
switch variant {
|
||||
case .regular:
|
||||
return false
|
||||
case .ephemeral:
|
||||
return true
|
||||
case .nwc:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
static func nwc(url: RelayURL) -> RelayDescriptor {
|
||||
return RelayDescriptor(url: url, info: .readWrite, variant: .nwc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RelayFlags: Int {
|
||||
case none = 0
|
||||
case broken = 1
|
||||
}
|
||||
|
||||
enum RelayAuthenticationError {
|
||||
/// Only a public key was provided in keypair to sign challenge.
|
||||
///
|
||||
/// A private key is required to sign `auth` challenge.
|
||||
case no_private_key
|
||||
/// No keypair was provided to sign challenge.
|
||||
case no_key
|
||||
}
|
||||
enum RelayAuthenticationState: Equatable {
|
||||
/// No `auth` request has been made from this relay
|
||||
case none
|
||||
/// We have received an `auth` challenge, but have not yet replied to the challenge
|
||||
case pending
|
||||
/// We have received an `auth` challenge and replied with an `auth` event
|
||||
case verified
|
||||
/// We received an `auth` challenge but failed to reply to the challenge
|
||||
case error(RelayAuthenticationError)
|
||||
}
|
||||
|
||||
struct Limitations: Codable {
|
||||
let payment_required: Bool?
|
||||
|
||||
static var empty: Limitations {
|
||||
Limitations(payment_required: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct Admission: Codable {
|
||||
let amount: Int64
|
||||
let unit: String
|
||||
}
|
||||
|
||||
struct Subscription: Codable {
|
||||
let amount: Int64
|
||||
let unit: String
|
||||
let period: Int
|
||||
}
|
||||
|
||||
struct Publication: Codable {
|
||||
let kinds: [Int]
|
||||
let amount: Int64
|
||||
let unit: String
|
||||
}
|
||||
|
||||
struct Fees: Codable {
|
||||
let admission: [Admission]?
|
||||
let subscription: [Subscription]?
|
||||
let publication: [Publication]?
|
||||
|
||||
static var empty: Fees {
|
||||
Fees(admission: nil, subscription: nil, publication: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayMetadata: Codable {
|
||||
let name: String?
|
||||
let description: String?
|
||||
let pubkey: Pubkey?
|
||||
let contact: String?
|
||||
let supported_nips: [Int]?
|
||||
let software: String?
|
||||
let version: String?
|
||||
let limitation: Limitations?
|
||||
let payments_url: String?
|
||||
let icon: String?
|
||||
let fees: Fees?
|
||||
|
||||
var is_paid: Bool {
|
||||
return limitation?.payment_required ?? false
|
||||
}
|
||||
}
|
||||
|
||||
extension RelayPool {
|
||||
class Relay: Identifiable {
|
||||
var descriptor: RelayDescriptor
|
||||
let connection: RelayConnection
|
||||
var authentication_state: RelayAuthenticationState
|
||||
|
||||
var flags: Int
|
||||
|
||||
init(descriptor: RelayDescriptor, connection: RelayConnection) {
|
||||
self.flags = 0
|
||||
self.descriptor = descriptor
|
||||
self.connection = connection
|
||||
self.authentication_state = RelayAuthenticationState.none
|
||||
}
|
||||
|
||||
var is_broken: Bool {
|
||||
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
|
||||
}
|
||||
|
||||
var id: RelayURL {
|
||||
return descriptor.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RelayPool {
|
||||
enum RelayError: Error {
|
||||
case RelayAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Extension to bridge NIP-65 relay list structs with app-native objects
|
||||
|
||||
extension NIP65.RelayList {
|
||||
static func fromLegacyContactList(_ contactList: NdbNote) throws(BridgeError) -> Self {
|
||||
guard let relayListInfo = decode_json_relays(contactList.content) else { throw .couldNotDecodeRelayListInfo }
|
||||
let relayItems = relayListInfo.map({ url, rwConfiguration in
|
||||
return RelayItem(url: url, rwConfiguration: rwConfiguration.toNIP65RWConfiguration() ?? .readWrite)
|
||||
})
|
||||
return NIP65.RelayList(relays: relayItems)
|
||||
}
|
||||
|
||||
static func fromLegacyContactList(_ contactList: NdbNote?) throws(BridgeError) -> Self? {
|
||||
guard let contactList = contactList else { return nil }
|
||||
return try fromLegacyContactList(contactList)
|
||||
}
|
||||
|
||||
enum BridgeError: Error {
|
||||
case couldNotDecodeRelayListInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,32 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
enum NostrConnectionEvent {
|
||||
case ws_event(WebSocketEvent)
|
||||
/// Other non-message websocket events
|
||||
case ws_connection_event(WSConnectionEvent)
|
||||
/// A nostr response
|
||||
case nostr_event(NostrResponse)
|
||||
|
||||
/// Models non-messaging websocket events
|
||||
///
|
||||
/// Implementation note: Messaging events should use `.nostr_event` in `NostrConnectionEvent`
|
||||
enum WSConnectionEvent {
|
||||
case connected
|
||||
case disconnected(URLSessionWebSocketTask.CloseCode, String?)
|
||||
case error(Error)
|
||||
|
||||
static func from(full_ws_event: WebSocketEvent) -> Self? {
|
||||
switch full_ws_event {
|
||||
case .connected:
|
||||
return .connected
|
||||
case .message(_):
|
||||
return nil
|
||||
case .disconnected(let closeCode, let string):
|
||||
return .disconnected(closeCode, string)
|
||||
case .error(let error):
|
||||
return .error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class RelayConnection: ObservableObject {
|
||||
@@ -31,11 +55,11 @@ final class RelayConnection: ObservableObject {
|
||||
|
||||
init(url: RelayURL,
|
||||
handleEvent: @escaping (NostrConnectionEvent) -> (),
|
||||
processEvent: @escaping (WebSocketEvent) -> ())
|
||||
processUnverifiedWSEvent: @escaping (WebSocketEvent) -> ())
|
||||
{
|
||||
self.relay_url = url
|
||||
self.handleEvent = handleEvent
|
||||
self.processEvent = processEvent
|
||||
self.processEvent = processUnverifiedWSEvent
|
||||
}
|
||||
|
||||
func ping() {
|
||||
@@ -115,6 +139,7 @@ final class RelayConnection: ObservableObject {
|
||||
}
|
||||
|
||||
private func receive(event: WebSocketEvent) {
|
||||
assert(!Thread.isMainThread, "This code must not be executed on the main thread")
|
||||
processEvent(event)
|
||||
switch event {
|
||||
case .connected:
|
||||
@@ -152,7 +177,8 @@ final class RelayConnection: ObservableObject {
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.handleEvent(.ws_event(event))
|
||||
guard let ws_connection_event = NostrConnectionEvent.WSConnectionEvent.from(full_ws_event: event) else { return }
|
||||
self.handleEvent(.ws_connection_event(ws_connection_event))
|
||||
}
|
||||
|
||||
if let description = event.description {
|
||||
@@ -190,7 +216,9 @@ final class RelayConnection: ObservableObject {
|
||||
private func receive(message: URLSessionWebSocketTask.Message) {
|
||||
switch message {
|
||||
case .string(let messageString):
|
||||
if let ev = decode_nostr_event(txt: messageString) {
|
||||
// NOTE: Once we switch to the local relay model,
|
||||
// we will not need to verify nostr events at this point.
|
||||
if let ev = decode_and_verify_nostr_response(txt: messageString) {
|
||||
DispatchQueue.main.async {
|
||||
self.handleEvent(.nostr_event(ev))
|
||||
}
|
||||
@@ -19,18 +19,15 @@ struct QueuedRequest {
|
||||
let skip_ephemeral: Bool
|
||||
}
|
||||
|
||||
struct SeenEvent: Hashable {
|
||||
let relay_id: RelayURL
|
||||
let evid: NoteId
|
||||
}
|
||||
|
||||
/// Establishes and manages connections and subscriptions to a list of relays.
|
||||
class RelayPool {
|
||||
var relays: [Relay] = []
|
||||
private(set) var relays: [Relay] = []
|
||||
var handlers: [RelayHandler] = []
|
||||
var request_queue: [QueuedRequest] = []
|
||||
var seen: Set<SeenEvent> = Set()
|
||||
var seen: [NoteId: Set<RelayURL>] = [:]
|
||||
var counts: [RelayURL: UInt64] = [:]
|
||||
var ndb: Ndb
|
||||
/// The keypair used to authenticate with relays
|
||||
var keypair: Keypair?
|
||||
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
||||
var message_sent_function: (((String, Relay)) -> Void)?
|
||||
@@ -122,14 +119,14 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
func add_relay(_ desc: RelayDescriptor) throws {
|
||||
func add_relay(_ desc: RelayDescriptor) throws(RelayError) {
|
||||
let relay_id = desc.url
|
||||
if get_relay(relay_id) != nil {
|
||||
throw RelayError.RelayAlreadyExists
|
||||
}
|
||||
let conn = RelayConnection(url: desc.url, handleEvent: { event in
|
||||
self.handle_event(relay_id: relay_id, event: event)
|
||||
}, processEvent: { wsev in
|
||||
}, processUnverifiedWSEvent: { wsev in
|
||||
guard case .message(let msg) = wsev,
|
||||
case .string(let str) = msg
|
||||
else { return }
|
||||
@@ -200,6 +197,64 @@ class RelayPool {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||
}
|
||||
|
||||
/// Subscribes to data from the `RelayPool` based on a filter and a list of desired relays.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - filters: The filters specifying the desired content.
|
||||
/// - desiredRelays: The desired relays which to subsctibe to. If `nil`, it defaults to the `RelayPool`'s default list
|
||||
/// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal, in seconds
|
||||
/// - Returns: Returns an async stream that callers can easily consume via a for-loop
|
||||
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: TimeInterval = 10) -> AsyncStream<StreamItem> {
|
||||
let desiredRelays = desiredRelays ?? self.relays.map({ $0.descriptor.url })
|
||||
return AsyncStream<StreamItem> { continuation in
|
||||
let sub_id = UUID().uuidString
|
||||
var seenEvents: Set<NoteId> = []
|
||||
var relaysWhoFinishedInitialResults: Set<RelayURL> = []
|
||||
var eoseSent = false
|
||||
self.subscribe(sub_id: sub_id, filters: filters, handler: { (relayUrl, connectionEvent) in
|
||||
switch connectionEvent {
|
||||
case .ws_connection_event(let ev):
|
||||
// Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here.
|
||||
// For the future, perhaps we should abstract away `.ws_connection_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer.
|
||||
break
|
||||
case .nostr_event(let nostrResponse):
|
||||
guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription
|
||||
switch nostrResponse {
|
||||
case .event(_, let nostrEvent):
|
||||
if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events.
|
||||
continuation.yield(with: .success(.event(nostrEvent)))
|
||||
seenEvents.insert(nostrEvent.id)
|
||||
case .notice(let note):
|
||||
break // We do not support handling these yet
|
||||
case .eose(_):
|
||||
relaysWhoFinishedInitialResults.insert(relayUrl)
|
||||
if relaysWhoFinishedInitialResults == Set(desiredRelays) {
|
||||
continuation.yield(with: .success(.eose))
|
||||
eoseSent = true
|
||||
}
|
||||
case .ok(_): break // No need to handle this, we are not sending an event to the relay
|
||||
case .auth(_): break // Handled in a separate function in RelayPool
|
||||
}
|
||||
}
|
||||
}, to: desiredRelays)
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000 * UInt64(eoseTimeout))
|
||||
if !eoseSent { continuation.yield(with: .success(.eose)) }
|
||||
}
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
self.unsubscribe(sub_id: sub_id, to: desiredRelays)
|
||||
self.remove_handler(sub_id: sub_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum StreamItem {
|
||||
/// A Nostr event
|
||||
case event(NostrEvent)
|
||||
/// The "end of stored events" signal
|
||||
case eose
|
||||
}
|
||||
|
||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
@@ -243,19 +298,19 @@ class RelayPool {
|
||||
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||
|
||||
self.send_raw_to_local_ndb(req)
|
||||
self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy
|
||||
|
||||
for relay in relays {
|
||||
if req.is_read && !(relay.descriptor.info.read ?? true) {
|
||||
continue
|
||||
if req.is_read && !(relay.descriptor.info.canRead) {
|
||||
continue // Do not send read requests to relays that are not READ relays
|
||||
}
|
||||
|
||||
if req.is_write && !(relay.descriptor.info.write ?? true) {
|
||||
continue
|
||||
if req.is_write && !(relay.descriptor.info.canWrite) {
|
||||
continue // Do not send write requests to relays that are not WRITE relays
|
||||
}
|
||||
|
||||
if relay.descriptor.ephemeral && skip_ephemeral {
|
||||
continue
|
||||
continue // Do not send requests to ephemeral relays if we want to skip them
|
||||
}
|
||||
|
||||
guard relay.connection.isConnected else {
|
||||
@@ -297,15 +352,12 @@ class RelayPool {
|
||||
func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) {
|
||||
if case .nostr_event(let ev) = event {
|
||||
if case .event(_, let nev) = ev {
|
||||
let k = SeenEvent(relay_id: relay_id, evid: nev.id)
|
||||
if !seen.contains(k) {
|
||||
seen.insert(k)
|
||||
if counts[relay_id] == nil {
|
||||
counts[relay_id] = 1
|
||||
} else {
|
||||
counts[relay_id] = (counts[relay_id] ?? 0) + 1
|
||||
}
|
||||
if seen[nev.id]?.contains(relay_id) == true {
|
||||
return
|
||||
}
|
||||
seen[nev.id, default: Set()].insert(relay_id)
|
||||
counts[relay_id, default: 0] += 1
|
||||
notify(.update_stats(note_id: nev.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +366,7 @@ class RelayPool {
|
||||
record_seen(relay_id: relay_id, event: event)
|
||||
|
||||
// run req queue when we reconnect
|
||||
if case .ws_event(let ws) = event {
|
||||
if case .ws_connection_event(let ws) = event {
|
||||
if case .connected = ws {
|
||||
run_queue(relay_id)
|
||||
}
|
||||
@@ -354,7 +406,7 @@ class RelayPool {
|
||||
}
|
||||
|
||||
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
|
||||
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
|
||||
try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import LinkPresentation
|
||||
import EmojiPicker
|
||||
|
||||
class DamusState: HeadlessDamusState {
|
||||
let pool: RelayPool
|
||||
let keypair: Keypair
|
||||
let likes: EventCounter
|
||||
let boosts: EventCounter
|
||||
@@ -28,8 +27,6 @@ class DamusState: HeadlessDamusState {
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
let bookmarks: BookmarksManager
|
||||
let postbox: PostBox
|
||||
let bootstrap_relays: [RelayURL]
|
||||
let replies: ReplyCounter
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
@@ -39,9 +36,10 @@ class DamusState: HeadlessDamusState {
|
||||
var purple: DamusPurple
|
||||
var push_notification_client: PushNotificationClient
|
||||
let emoji_provider: EmojiProvider
|
||||
let favicon_cache: FaviconCache
|
||||
private(set) var nostrNetwork: NostrNetworkManager
|
||||
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
||||
self.pool = pool
|
||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
@@ -58,8 +56,6 @@ class DamusState: HeadlessDamusState {
|
||||
self.drafts = drafts
|
||||
self.events = events
|
||||
self.bookmarks = bookmarks
|
||||
self.postbox = postbox
|
||||
self.bootstrap_relays = bootstrap_relays
|
||||
self.replies = replies
|
||||
self.wallet = wallet
|
||||
self.nav = nav
|
||||
@@ -73,6 +69,10 @@ class DamusState: HeadlessDamusState {
|
||||
self.quote_reposts = quote_reposts
|
||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||
self.emoji_provider = emoji_provider
|
||||
self.favicon_cache = FaviconCache()
|
||||
|
||||
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
||||
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -98,27 +98,13 @@ class DamusState: HeadlessDamusState {
|
||||
guard let ndb = mndb else { return nil }
|
||||
let pubkey = keypair.pubkey
|
||||
|
||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
if let nwc_str = settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: nwc_str) {
|
||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||
}
|
||||
self.init(
|
||||
pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
@@ -135,8 +121,6 @@ class DamusState: HeadlessDamusState {
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: navigationCoordinator,
|
||||
@@ -144,7 +128,8 @@ class DamusState: HeadlessDamusState {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -179,7 +164,7 @@ class DamusState: HeadlessDamusState {
|
||||
try await self.push_notification_client.revoke_token()
|
||||
}
|
||||
wallet.disconnect()
|
||||
pool.close()
|
||||
nostrNetwork.pool.close()
|
||||
ndb.close()
|
||||
}
|
||||
|
||||
@@ -189,7 +174,6 @@ class DamusState: HeadlessDamusState {
|
||||
let kp = Keypair(pubkey: empty_pub, privkey: nil)
|
||||
|
||||
return DamusState.init(
|
||||
pool: RelayPool(ndb: .empty),
|
||||
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
|
||||
likes: EventCounter(our_pubkey: empty_pub),
|
||||
boosts: EventCounter(our_pubkey: empty_pub),
|
||||
@@ -206,8 +190,6 @@ class DamusState: HeadlessDamusState {
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: .empty),
|
||||
bookmarks: BookmarksManager(pubkey: empty_pub),
|
||||
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
||||
bootstrap_relays: [],
|
||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||
wallet: WalletModel(settings: UserSettingsStore()),
|
||||
nav: NavigationCoordinator(),
|
||||
@@ -215,7 +197,34 @@ class DamusState: HeadlessDamusState {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: .empty,
|
||||
quote_reposts: .init(our_pubkey: empty_pub),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension DamusState {
|
||||
struct NostrNetworkManagerDelegate: NostrNetworkManager.Delegate {
|
||||
let settings: UserSettingsStore
|
||||
let contacts: Contacts
|
||||
|
||||
var ndb: Ndb
|
||||
var keypair: Keypair
|
||||
|
||||
var latestRelayListEventIdHex: String? {
|
||||
get { self.settings.latestRelayListEventIdHex }
|
||||
set { self.settings.latestRelayListEventIdHex = newValue }
|
||||
}
|
||||
|
||||
var latestContactListEvent: NostrEvent? { self.contacts.event }
|
||||
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
|
||||
var developerMode: Bool { self.settings.developer_mode }
|
||||
var relayModelCache: RelayModelCache
|
||||
var relayFilters: RelayFilters
|
||||
|
||||
var nwcWallet: WalletConnectURL? {
|
||||
guard let nwcString = self.settings.nostr_wallet_connect else { return nil }
|
||||
return WalletConnectURL(str: nwcString)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,11 @@
|
||||
// Block.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Kyle Roucis on 2023-08-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
fileprivate extension String {
|
||||
/// Failable initializer to build a Swift.String from a C-backed `str_block_t`.
|
||||
init?(_ s: str_block_t) {
|
||||
let len = s.end - s.start
|
||||
let bytes = Data(bytes: s.start, count: len)
|
||||
self.init(bytes: bytes, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a block of data stored by the NOSTR protocol. This can be
|
||||
/// Represents a block of data stored in nostrdb. This can be
|
||||
/// simple text, a hashtag, a url, a relay reference, a mention ref and
|
||||
/// potentially more in the future.
|
||||
enum Block: Equatable {
|
||||
@@ -37,7 +26,7 @@ enum Block: Equatable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case text(String)
|
||||
case mention(Mention<MentionRef>)
|
||||
case hashtag(String)
|
||||
@@ -51,61 +40,56 @@ struct Blocks: Equatable {
|
||||
let blocks: [Block]
|
||||
}
|
||||
|
||||
extension ndb_str_block {
|
||||
func as_str() -> String {
|
||||
let buf = UnsafeBufferPointer(start: self.str, count: Int(self.len))
|
||||
let uint8Buf = buf.map { UInt8(bitPattern: $0) }
|
||||
return String(decoding: uint8Buf, as: UTF8.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ndb_block_ptr {
|
||||
func as_str() -> String {
|
||||
guard let str_block = ndb_block_str(self.ptr) else {
|
||||
return ""
|
||||
}
|
||||
return str_block.pointee.as_str()
|
||||
}
|
||||
|
||||
var block: ndb_block.__Unnamed_union_block {
|
||||
self.ptr.pointee.block
|
||||
}
|
||||
}
|
||||
|
||||
extension Block {
|
||||
/// Failable initializer for the C-backed type `block_t`. This initializer will inspect
|
||||
/// the underlying block type and build the appropriate enum value as needed.
|
||||
init?(_ block: block_t, tags: TagsSequence? = nil) {
|
||||
switch block.type {
|
||||
init?(block: ndb_block_ptr, tags: TagsSequence?) {
|
||||
switch ndb_get_block_type(block.ptr) {
|
||||
case BLOCK_HASHTAG:
|
||||
guard let str = String(block.block.str) else {
|
||||
return nil
|
||||
}
|
||||
self = .hashtag(str)
|
||||
self = .hashtag(block.as_str())
|
||||
case BLOCK_TEXT:
|
||||
guard let str = String(block.block.str) else {
|
||||
return nil
|
||||
}
|
||||
self = .text(str)
|
||||
self = .text(block.as_str())
|
||||
case BLOCK_MENTION_INDEX:
|
||||
guard let b = Block(index: Int(block.block.mention_index), tags: tags) else {
|
||||
return nil
|
||||
}
|
||||
self = b
|
||||
case BLOCK_URL:
|
||||
guard let b = Block(block.block.str) else {
|
||||
return nil
|
||||
}
|
||||
self = b
|
||||
guard let url = URL(string: block.as_str()) else { return nil }
|
||||
self = .url(url)
|
||||
case BLOCK_INVOICE:
|
||||
guard let b = Block(invoice: block.block.invoice) else {
|
||||
return nil
|
||||
}
|
||||
guard let b = Block(invoice: block.block.invoice) else { return nil }
|
||||
self = b
|
||||
case BLOCK_MENTION_BECH32:
|
||||
guard let b = Block(bech32: block.block.mention_bech32) else {
|
||||
return nil
|
||||
}
|
||||
guard let b = Block(bech32: block.block.mention_bech32) else { return nil }
|
||||
self = b
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
fileprivate extension Block {
|
||||
/// Failable initializer for the C-backed type `str_block_t`.
|
||||
init?(_ b: str_block_t) {
|
||||
guard let str = String(b) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let url = URL(string: str) {
|
||||
self = .url(url)
|
||||
}
|
||||
else {
|
||||
self = .text(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
fileprivate extension Block {
|
||||
/// Failable initializer for a block index and a tag sequence.
|
||||
init?(index: Int, tags: TagsSequence? = nil) {
|
||||
@@ -127,34 +111,34 @@ fileprivate extension Block {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Block {
|
||||
/// Failable initializer for the C-backed type `invoice_block_t`.
|
||||
init?(invoice: invoice_block_t) {
|
||||
guard let invstr = String(invoice.invstr) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard var b11 = maybe_pointee(invoice.bolt11) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let description = convert_invoice_description(b11: b11) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
|
||||
let payment_hash = Data(bytes: &b11.payment_hash, count: 32)
|
||||
let created_at = b11.timestamp
|
||||
|
||||
tal_free(invoice.bolt11)
|
||||
self = .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
|
||||
init?(invoice: ndb_invoice_block) {
|
||||
|
||||
guard let invoice = invoice_block_as_invoice(invoice) else { return nil }
|
||||
self = .invoice(invoice)
|
||||
}
|
||||
}
|
||||
|
||||
func invoice_block_as_invoice(_ invoice: ndb_invoice_block) -> Invoice? {
|
||||
let invstr = invoice.invstr.as_str()
|
||||
let b11 = invoice.invoice
|
||||
|
||||
guard let description = convert_invoice_description(b11: b11) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let amount: Amount = b11.amount == 0 ? .any : .specific(Int64(b11.amount))
|
||||
|
||||
return Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, created_at: b11.timestamp)
|
||||
|
||||
}
|
||||
|
||||
fileprivate extension Block {
|
||||
/// Failable initializer for the C-backed type `mention_bech32_block_t`. This initializer will inspect the
|
||||
/// bech32 type code and build the appropriate enum type.
|
||||
init?(bech32 b: mention_bech32_block_t) {
|
||||
init?(bech32 b: ndb_mention_bech32_block) {
|
||||
guard let decoded = decodeCBech32(b.bech32) else {
|
||||
return nil
|
||||
}
|
||||
@@ -164,6 +148,7 @@ fileprivate extension Block {
|
||||
self = .mention(.any(ref))
|
||||
}
|
||||
}
|
||||
|
||||
extension Block {
|
||||
var asString: String {
|
||||
switch self {
|
||||
@@ -186,3 +171,13 @@ extension Block {
|
||||
}
|
||||
}
|
||||
}
|
||||
extension Block {
|
||||
var asInvoice: Invoice? {
|
||||
switch self {
|
||||
case .invoice(let invoice):
|
||||
return invoice
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,4 +51,15 @@ struct NoteId: IdType, TagKey, TagConvertible {
|
||||
|
||||
return note_id
|
||||
}
|
||||
|
||||
func withUnsafePointer<T>(_ body: (UnsafePointer<UInt8>) throws -> T) rethrows -> T {
|
||||
return try self.id.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
|
||||
guard let baseAddress = bytes.baseAddress else {
|
||||
fatalError("Cannot get base address")
|
||||
}
|
||||
return try baseAddress.withMemoryRebound(to: UInt8.self, capacity: bytes.count) { ptr in
|
||||
return try body(ptr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,5 +44,14 @@ struct Pubkey: IdType, TagKey, TagConvertible, Identifiable {
|
||||
return pubkey
|
||||
}
|
||||
|
||||
func withUnsafePointer<T>(_ body: (UnsafePointer<UInt8>) throws -> T) rethrows -> T {
|
||||
return try self.id.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
|
||||
guard let baseAddress = bytes.baseAddress else {
|
||||
fatalError("Cannot get base address")
|
||||
}
|
||||
return try baseAddress.withMemoryRebound(to: UInt8.self, capacity: bytes.count) { ptr in
|
||||
return try body(ptr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,13 @@ class ActionBarModel: ObservableObject {
|
||||
@Published private(set) var zaps: Int
|
||||
@Published var zap_total: Int64
|
||||
@Published var replies: Int
|
||||
|
||||
@Published var relays: Int
|
||||
|
||||
static func empty() -> ActionBarModel {
|
||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
}
|
||||
|
||||
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0) {
|
||||
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0, relays: Int = 0) {
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
self.zaps = zaps
|
||||
@@ -42,6 +43,7 @@ class ActionBarModel: ObservableObject {
|
||||
self.our_reply = our_reply
|
||||
self.our_quote_repost = our_quote_repost
|
||||
self.quote_reposts = quote_reposts
|
||||
self.relays = relays
|
||||
}
|
||||
|
||||
func update(damus: DamusState, evid: NoteId) {
|
||||
@@ -56,11 +58,12 @@ class ActionBarModel: ObservableObject {
|
||||
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||
self.our_reply = damus.replies.our_reply(evid)
|
||||
self.our_quote_repost = damus.quote_reposts.our_events[evid]
|
||||
self.relays = (damus.nostrNetwork.pool.seen[evid] ?? []).count
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
var is_empty: Bool {
|
||||
return likes == 0 && boosts == 0 && zaps == 0
|
||||
return likes == 0 && boosts == 0 && zaps == 0 && quote_reposts == 0 && relays == 0
|
||||
}
|
||||
|
||||
var liked: Bool {
|
||||
@@ -217,7 +217,16 @@ struct EventActionBar: View {
|
||||
AnyView(self.action_bar_content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var event_relay_url_strings: [RelayURL] {
|
||||
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
|
||||
if !relays.isEmpty {
|
||||
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
||||
}
|
||||
|
||||
return userProfile.getCappedRelays()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
self.content
|
||||
.onAppear {
|
||||
@@ -233,7 +242,9 @@ struct EventActionBar: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) {
|
||||
ShareSheet(activityItems: [URL(string: "https://damus.io/" + event.id.bech32)!])
|
||||
if let url = URL(string: "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) {
|
||||
|
||||
@@ -262,7 +273,7 @@ struct EventActionBar: View {
|
||||
|
||||
func send_like(emoji: String) {
|
||||
guard let keypair = damus_state.keypair.to_full(),
|
||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else {
|
||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -270,7 +281,7 @@ struct EventActionBar: View {
|
||||
|
||||
generator.impactOccurred()
|
||||
|
||||
damus_state.postbox.send(like_ev)
|
||||
damus_state.nostrNetwork.postbox.send(like_ev)
|
||||
}
|
||||
|
||||
// MARK: Helper structures
|
||||
@@ -59,6 +59,16 @@ struct EventDetailBar: View {
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
if bar.relays > 0 {
|
||||
let relays = Array(state.nostrNetwork.pool.seen[target] ?? [])
|
||||
NavigationLink(value: Route.UserRelays(relays: relays)) {
|
||||
let nounString = pluralizedString(key: "relays_count", count: bar.relays)
|
||||
let noun = Text(nounString).foregroundColor(.gray)
|
||||
Text("\(Text(verbatim: bar.relays.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many relays a note was found on. In source English, the first variable is the number of relays, and the second variable is 'Relay' or 'Relays'.")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,16 @@ struct ShareAction: View {
|
||||
self.userProfile = userProfile
|
||||
self._show_share = show_share
|
||||
}
|
||||
|
||||
|
||||
var event_relay_url_strings: [RelayURL] {
|
||||
let relays = userProfile.damus.nostrNetwork.relaysForEvent(event: event)
|
||||
if !relays.isEmpty {
|
||||
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
||||
}
|
||||
|
||||
return userProfile.getCappedRelays()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
@@ -40,7 +49,7 @@ struct ShareAction: View {
|
||||
|
||||
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) {
|
||||
dismiss()
|
||||
UIPasteboard.general.string = "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(noteid: event.id, relays: userProfile.getCappedRelayStrings())))
|
||||
UIPasteboard.general.string = "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(noteid: event.id, relays: userProfile.getCappedRelays())))
|
||||
}
|
||||
|
||||
let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"
|
||||
@@ -132,9 +132,9 @@ struct ReportView_Previews: PreviewProvider {
|
||||
let ds = test_damus_state
|
||||
VStack {
|
||||
|
||||
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!)
|
||||
ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!)
|
||||
|
||||
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
|
||||
ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
|
||||
|
||||
}
|
||||
}
|
||||
42
damus/Features/Actions/Reposts/Views/QuoteRepostsView.swift
Normal file
42
damus/Features/Actions/Reposts/Views/QuoteRepostsView.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// QuoteRepostsView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2024-03-16.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct QuoteRepostsView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var model: EventsModel
|
||||
|
||||
var body: some View {
|
||||
TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: ContentFilters.default_filters(damus_state: damus_state).filter(ev:)) {
|
||||
ZStack(alignment: .leading) {
|
||||
DamusBackground(maxHeight: 250)
|
||||
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom))
|
||||
Text("Quotes", comment: "Navigation bar title for Quote Reposts view.")
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
.font(.title.bold())
|
||||
.padding(.leading, 30)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.padding(.bottom, tabHeight)
|
||||
.onAppear {
|
||||
model.subscribe()
|
||||
}
|
||||
.onDisappear {
|
||||
model.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QuoteRepostsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state
|
||||
QuoteRepostsView(damus_state: state, model: .reposts(state: state, target: test_note.id))
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,11 @@ struct RepostAction: View {
|
||||
dismiss()
|
||||
|
||||
guard let keypair = self.damus_state.keypair.to_full(),
|
||||
let boost = make_boost_event(keypair: keypair, boosted: self.event) else {
|
||||
let boost = make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.postbox.send(boost)
|
||||
damus_state.nostrNetwork.postbox.send(boost)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
|
||||
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
|
||||
77
damus/Features/Actions/Reposts/Views/Reposted.swift
Normal file
77
damus/Features/Actions/Reposts/Views/Reposted.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Reposted.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Reposted: View {
|
||||
let damus: DamusState
|
||||
let pubkey: Pubkey
|
||||
let target: NostrEvent
|
||||
@State var reposts: Int
|
||||
|
||||
init(damus: DamusState, pubkey: Pubkey, target: NostrEvent) {
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
self.target = target
|
||||
self.reposts = damus.boosts.counts[target.id] ?? 1
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image("repost")
|
||||
.foregroundColor(Color.gray)
|
||||
|
||||
// Show profile picture of the reposter only if the reposter is not the author of the reposted note.
|
||||
if pubkey != target.pubkey {
|
||||
ProfilePicView(pubkey: pubkey, size: eventview_pfp_size(.small), highlight: .none, profiles: damus.profiles, disable_animation: damus.settings.disable_animation)
|
||||
.onTapGesture {
|
||||
show_profile_action_sheet_if_enabled(damus_state: damus, pubkey: pubkey)
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: 0.1) {
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
damus.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target.id))) {
|
||||
Text(people_reposted_text(profiles: damus.profiles, pubkey: pubkey, reposts: reposts))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats), perform: { note_id in
|
||||
guard note_id == target.id else { return }
|
||||
let repost_count = damus.boosts.counts[target.id]
|
||||
if let repost_count, reposts != repost_count {
|
||||
reposts = repost_count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func people_reposted_text(profiles: Profiles, pubkey: Pubkey, reposts: Int, locale: Locale = Locale.current) -> String {
|
||||
guard reposts > 0 else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
let other_reposts = reposts - 1
|
||||
let display_name = event_author_name(profiles: profiles, pubkey: pubkey)
|
||||
|
||||
if other_reposts == 0 {
|
||||
return String(format: NSLocalizedString("%@ reposted", bundle: bundle, comment: "Text indicating that the note was reposted (i.e. re-shared)."), locale: locale, display_name)
|
||||
} else {
|
||||
return String(format: localizedStringFormat(key: "people_reposted_count", locale: locale), locale: locale, other_reposts, display_name)
|
||||
}
|
||||
}
|
||||
|
||||
struct Reposted_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ struct RepostedEvent: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) {
|
||||
Reposted(damus: damus, pubkey: event.pubkey, target: inner_ev.id)
|
||||
Reposted(damus: damus, pubkey: event.pubkey, target: inner_ev)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user