Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce0068be53 | ||
|
|
4c6578ccb1 | ||
|
|
d6d3f8a52f | ||
|
|
84bfafa2e9 | ||
|
|
24e8522268 | ||
|
|
f670859b92 | ||
|
|
bca86f115f | ||
|
|
a9b9a7a02a | ||
|
|
1abb8e3f36 | ||
|
|
15d834dc0f | ||
|
|
ea862dac5f | ||
|
|
55a4868813 | ||
|
|
60978f43b5 | ||
|
|
37cf601c01 | ||
|
|
87da5035da | ||
|
|
ddea5028ee | ||
|
|
de68d61c3f | ||
|
|
f7e837e3ab | ||
|
|
02070c2868 | ||
|
|
998ef8f4b1 | ||
|
|
6fdf0dbcf5 | ||
|
|
15c0eaa5dc | ||
|
|
226909ef16 | ||
|
|
8ca88804dd | ||
|
|
55f9b9194e | ||
|
|
a26116702d | ||
|
|
35d376ca6c | ||
|
|
57b48c7368 | ||
|
|
b29937a04e | ||
|
|
06374fe1fc | ||
|
|
2d5c0bf549 | ||
|
|
0a5b4656c3 | ||
|
|
cad2c7faa3 | ||
|
|
19cfecb9ea | ||
|
|
89d0710ce5 |
@@ -1,3 +1,5 @@
|
|||||||
|
exclude = ["target/**/*.toml", "Cargo.lock"]
|
||||||
|
|
||||||
[formatting]
|
[formatting]
|
||||||
align_entries = true
|
align_entries = true
|
||||||
indent_tables = true
|
indent_tables = true
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -5,31 +5,61 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.2.0] - 2025-06-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `--force-id` flag to bypass case validation in `repo announce` - by Awiteb
|
||||||
|
- Add `--address-file` flag to `repo announce` command - by Awiteb
|
||||||
|
- Read the `nostr-address` file in `repo view` command - by Awiteb
|
||||||
|
- Read the `nostr-address` file in `issue new` command - by Awiteb
|
||||||
|
- Read the `nostr-address` file in `reply` command - by Awiteb
|
||||||
|
- A `--quote-to` flag to quote the replied to content in the editor - by Awiteb
|
||||||
|
- Enter repository as nip5 - by Awiteb
|
||||||
|
- Make the relays list optional - by Awiteb
|
||||||
|
- Events and naddrs can starts with `nostr:` - by Awiteb
|
||||||
|
- Support relays and naddrs sets - by Awiteb
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Add `chrono@0.4.41` to the dependencies - by Awiteb
|
||||||
|
- Enable `nip05` feature of `nostr` crate - by Awiteb
|
||||||
|
- Add `serde@1.0.219`, `dirs@6.0.0` and `toml@0.8.22` - by Awiteb
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Create a valid naddr string - by Awiteb
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Support more than one naddr instead of one - by Awiteb
|
||||||
|
|
||||||
## [0.1.0] - 2025-05-21
|
## [0.1.0] - 2025-05-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Setup the CLI and create `repo view` command - (Awiteb at [d962732](https://git.4rs.nl/awiteb/n34.git/commit/?id=d962732e5251b3af81b6d12327e258b49e3e603e))
|
- Setup the CLI and create `repo view` command - by Awiteb
|
||||||
- Add `repo announce` command - (Awiteb at [b444aeb](https://git.4rs.nl/awiteb/n34.git/commit/?id=b444aeba0ac6d19cb32715fb70c7dc1da297f8e9))
|
- Add `repo announce` command - by Awiteb
|
||||||
- Support PoW - (Awiteb at [c0a5e47](https://git.4rs.nl/awiteb/n34.git/commit/?id=c0a5e4779385024c8b65ae7626081a54ccbcb5b7))
|
- Support PoW - by Awiteb
|
||||||
- Add `issue new` command - (Awiteb at [54f1c7e](https://git.4rs.nl/awiteb/n34.git/commit/?id=54f1c7e0e68670926a99993651db6f4e15087250))
|
- Add `issue new` command - by Awiteb
|
||||||
- New `reply` command - (Awiteb at [9444fc1](https://git.4rs.nl/awiteb/n34.git/commit/?id=9444fc1a8c75dcfb9f74be5e7ea7df6759a5ce6c))
|
- New `reply` command - by Awiteb
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- Add nedded dependencies - (Awiteb at [d37c696](https://git.4rs.nl/awiteb/n34.git/commit/?id=d37c6964f7111097550a04d25768085d4c6694d6))
|
- Add nedded dependencies - by Awiteb
|
||||||
- Add `easy_ext@1.0.2` to the dependencies - (Awiteb at [fb77a8c](https://git.4rs.nl/awiteb/n34.git/commit/?id=fb77a8ca141e885f351ef671680dbc3f54229303))
|
- Add `easy_ext@1.0.2` to the dependencies - by Awiteb
|
||||||
- Add `convert_case@0.8.0` to the dependencies - (Awiteb at [8a553f4](https://git.4rs.nl/awiteb/n34.git/commit/?id=8a553f4de85891c40965a9100f2853034919b6c8))
|
- Add `convert_case@0.8.0` to the dependencies - by Awiteb
|
||||||
- Add `tempfile@3.19.1` to the dependencies - (Awiteb at [ce58f29](https://git.4rs.nl/awiteb/n34.git/commit/?id=ce58f2902246637c8ebd8b66d14cbdf958c09543))
|
- Add `tempfile@3.19.1` to the dependencies - by Awiteb
|
||||||
- Add `futures@0.3.31` to the dependencies - (Awiteb at [bd08653](https://git.4rs.nl/awiteb/n34.git/commit/?id=bd086538fdeed1b39d3d8a355fc453722ab04bc2))
|
- Add `futures@0.3.31` to the dependencies - by Awiteb
|
||||||
- Bump `nostr` and `nostr_sdk` to `0.42.0` - (Awiteb at [724e270](https://git.4rs.nl/awiteb/n34.git/commit/?id=724e2707c69fc8ac16f05331970919439619346e))
|
- Bump `nostr` and `nostr_sdk` to `0.42.0` - by Awiteb
|
||||||
|
|
||||||
## [0.0.0] - 2025-05-01
|
## [0.0.0] - 2025-05-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Initialize the project - (Awiteb at [c3594c6](https://git.4rs.nl/awiteb/n34.git/commit/?id=c3594c64671f622386a6dab12cb97b9f44c1611e))
|
- Initialize the project - by Awiteb
|
||||||
|
|
||||||
|
[0.2.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.2.0
|
||||||
[0.1.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.1.0
|
[0.1.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.1.0
|
||||||
[0.0.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.0.0
|
[0.0.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.0.0
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,22 @@ Before submitting changes, please read the [Developer Certificate of Origin](DCO
|
|||||||
All patches must include a `Signed-off-by: NAME <EMAIL>` line to acknowledge
|
All patches must include a `Signed-off-by: NAME <EMAIL>` line to acknowledge
|
||||||
your agreement with the DCO.
|
your agreement with the DCO.
|
||||||
|
|
||||||
|
Ensure your Git name and email are correctly configured. While you don’t need to
|
||||||
|
use your real details, avoid leaving them as the default values. To verify your
|
||||||
|
current settings, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config user.name
|
||||||
|
git config user.email
|
||||||
|
```
|
||||||
|
|
||||||
|
If they’re incorrect or unset, update them using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --global user.name "Your Name"
|
||||||
|
git config --global user.email "your.email@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
We welcome all contributions, whether it be bug reports, fixes, feature
|
We welcome all contributions, whether it be bug reports, fixes, feature
|
||||||
submissions, feature requests, or improving documentation or testing. Enjoy
|
submissions, feature requests, or improving documentation or testing. Enjoy
|
||||||
collaborating!
|
collaborating!
|
||||||
@@ -49,6 +65,9 @@ to their read relays, most tools handle this automatically.
|
|||||||
#### Patch Guidelines
|
#### Patch Guidelines
|
||||||
|
|
||||||
- Keep patches small: Focused changes are easier to review and merge.
|
- Keep patches small: Focused changes are easier to review and merge.
|
||||||
|
- Run `just ci` before submitting your patch.
|
||||||
|
- Update the change log with your patch. Run `just changelog` or `git-cliff > CHANGELOG.md`
|
||||||
|
- Add your name to the [AUTHORS](AUTHORS) file if this is your first contribution. (alphabetical order)
|
||||||
- Use [Conventional Commits]: Start the patch subject with one of these types:
|
- Use [Conventional Commits]: Start the patch subject with one of these types:
|
||||||
- `feat`: New feature
|
- `feat`: New feature
|
||||||
- `fix`: Bug fix
|
- `fix`: Bug fix
|
||||||
@@ -62,7 +81,6 @@ to their read relays, most tools handle this automatically.
|
|||||||
- For all other changes, use `chore`.
|
- For all other changes, use `chore`.
|
||||||
- Add `!` to the subject if your patch contains a breacking change, e.g.
|
- Add `!` to the subject if your patch contains a breacking change, e.g.
|
||||||
`remove!: text` and `fix(reply)!: text`
|
`remove!: text` and `fix(reply)!: text`
|
||||||
- Run `just ci` before submitting your patch.
|
|
||||||
|
|
||||||
#### Code Style
|
#### Code Style
|
||||||
|
|
||||||
|
|||||||
656
Cargo.lock
generated
656
Cargo.lock
generated
@@ -36,6 +36,21 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@@ -147,7 +162,7 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
"object",
|
"object",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -272,6 +287,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -296,6 +317,20 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -372,6 +407,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -409,6 +450,27 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -432,6 +494,12 @@ version = "1.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.12"
|
version = "0.3.12"
|
||||||
@@ -582,9 +650,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasi 0.14.2+wasi-0.2.4",
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -605,6 +675,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -646,12 +722,116 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-body-util"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"httparse",
|
||||||
|
"itoa",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
"want",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-util"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"libc",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -759,6 +939,16 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -781,6 +971,12 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnet"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.1"
|
||||||
@@ -815,6 +1011,16 @@ version = "0.2.172"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -845,6 +1051,12 @@ version = "2.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
@@ -867,18 +1079,22 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "n34"
|
name = "n34"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap-verbosity-flag",
|
"clap-verbosity-flag",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
|
"dirs",
|
||||||
"easy-ext",
|
"easy-ext",
|
||||||
"futures",
|
"futures",
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
@@ -905,6 +1121,7 @@ dependencies = [
|
|||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
"instant",
|
"instant",
|
||||||
"regex",
|
"regex",
|
||||||
|
"reqwest",
|
||||||
"scrypt",
|
"scrypt",
|
||||||
"secp256k1",
|
"secp256k1",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -965,6 +1182,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.36.7"
|
version = "0.36.7"
|
||||||
@@ -986,6 +1212,12 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -1069,6 +1301,60 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"rand 0.9.1",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@@ -1143,6 +1429,17 @@ dependencies = [
|
|||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -1172,6 +1469,50 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"ipnet",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-socks",
|
||||||
|
"tower",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
|
"windows-registry",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -1192,6 +1533,12 @@ version = "0.1.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -1219,12 +1566,22 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pemfile"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1324,6 +1681,27 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -1415,6 +1793,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@@ -1579,6 +1966,74 @@ dependencies = [
|
|||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.41"
|
version = "0.1.41"
|
||||||
@@ -1636,6 +2091,12 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "try-lock"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tungstenite"
|
name = "tungstenite"
|
||||||
version = "0.26.2"
|
version = "0.26.2"
|
||||||
@@ -1740,6 +2201,15 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "want"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
||||||
|
dependencies = [
|
||||||
|
"try-lock",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
@@ -1836,6 +2306,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.26.11"
|
version = "0.26.11"
|
||||||
@@ -1876,13 +2356,92 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings 0.4.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-registry"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings 0.3.1",
|
||||||
|
"windows-targets 0.53.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1891,7 +2450,7 @@ version = "0.59.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1900,14 +2459,30 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm 0.52.6",
|
||||||
"windows_i686_msvc",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm 0.53.0",
|
||||||
|
"windows_aarch64_msvc 0.53.0",
|
||||||
|
"windows_i686_gnu 0.53.0",
|
||||||
|
"windows_i686_gnullvm 0.53.0",
|
||||||
|
"windows_i686_msvc 0.53.0",
|
||||||
|
"windows_x86_64_gnu 0.53.0",
|
||||||
|
"windows_x86_64_gnullvm 0.53.0",
|
||||||
|
"windows_x86_64_msvc 0.53.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1916,48 +2491,105 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen-rt"
|
name = "wit-bindgen-rt"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "n34"
|
name = "n34"
|
||||||
description = "A CLI to interact with NIP-34 and other stuff related to codes in nostr"
|
description = "A CLI to interact with NIP-34 and other stuff related to codes in nostr"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
authors = ["Awiteb <a@4rs.nl>"]
|
authors = ["Awiteb <a@4rs.nl>"]
|
||||||
@@ -12,17 +12,21 @@ categories = ["command-line-utilities"]
|
|||||||
rust-version = "1.85.1"
|
rust-version = "1.85.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = "0.4.41"
|
||||||
clap = { version = "4.5.37", features = ["derive"] }
|
clap = { version = "4.5.37", features = ["derive"] }
|
||||||
clap-verbosity-flag = { version = "3.0.2", default-features = false, features = [
|
clap-verbosity-flag = { version = "3.0.2", default-features = false, features = [
|
||||||
"tracing",
|
"tracing",
|
||||||
] }
|
] }
|
||||||
convert_case = "0.8.0"
|
convert_case = "0.8.0"
|
||||||
|
dirs = "6.0.0"
|
||||||
easy-ext = "1.0.2"
|
easy-ext = "1.0.2"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
nostr = { version = "0.42.0", features = ["parser"] }
|
nostr = { version = "0.42.0", features = ["parser", "nip05"] }
|
||||||
nostr-sdk = "0.42.0"
|
nostr-sdk = "0.42.0"
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
tempfile = "3.19.1"
|
tempfile = "3.19.1"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
|
||||||
|
toml = "0.8.22"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ reports or feature requests) or submitting patches. **All contributions must be
|
|||||||
submitted through Nostr**. For more details on the process, please refer to the
|
submitted through Nostr**. For more details on the process, please refer to the
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md) file. Your support is greatly appreciated!
|
[CONTRIBUTING.md](CONTRIBUTING.md) file. Your support is greatly appreciated!
|
||||||
|
|
||||||
|
## Contributions & Changes
|
||||||
|
|
||||||
|
You can find the changelog at [CHANGELOG.md](CHANGELOG.md) and the list of
|
||||||
|
contributors at [AUTHORS](AUTHORS) file.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
n34 is licensed under the GPL-3.0 License. This means that you are free to use,
|
n34 is licensed under the GPL-3.0 License. This means that you are free to use,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ body = """
|
|||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
### {{ group | upper_first }}
|
### {{ group | upper_first }}
|
||||||
{% for commit in commits %}
|
{% for commit in commits %}
|
||||||
- {{ commit.message | split(pat="\n") | first | split(pat=":") | slice(start=1) | join(sep=":") | upper_first | trim }} - \
|
- {{ commit.message | split(pat="\n") | first | split(pat=":") | slice(start=1) | join(sep=":") | trim | upper_first }} - \
|
||||||
({{ commit.author.name}} at [{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/?id={{ commit.id }}))\
|
by {{ commit.author.name}}\
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}\n
|
{% endfor %}\n
|
||||||
"""
|
"""
|
||||||
|
|||||||
2
justfile
2
justfile
@@ -64,7 +64,7 @@ changelog:
|
|||||||
[script]
|
[script]
|
||||||
release version:
|
release version:
|
||||||
set -e
|
set -e
|
||||||
TAG_MSG=$(git-cliff --strip all --unreleased --body '{{ tag_change_body }}')
|
TAG_MSG="Version {{ version }}\n\n$(git-cliff --strip all --unreleased --body '{{ tag_change_body }}')\n"
|
||||||
sed -i "s/^version\s*= \".*\"/version = \"{{ version }}\"/" ./Cargo.toml
|
sed -i "s/^version\s*= \".*\"/version = \"{{ version }}\"/" ./Cargo.toml
|
||||||
taplo fmt --config ./.taplo.toml ./Cargo.toml
|
taplo fmt --config ./.taplo.toml ./Cargo.toml
|
||||||
{{ JUST_EXECUTABLE }} ci
|
{{ JUST_EXECUTABLE }} ci
|
||||||
|
|||||||
15
nostr-address
Normal file
15
nostr-address
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# This file contains NIP-19 `naddr` entities for repositories that accept this
|
||||||
|
# project's issues and patches.
|
||||||
|
#
|
||||||
|
# The file acts as a **read-only reference** for retrieving repository relays
|
||||||
|
# when embedded in an `naddr` and mentions those repositories when opening
|
||||||
|
# patches or issues. Modifications here will not affect in the relays, as the
|
||||||
|
# file is **explicitly untracked**. Its goal is to simplify contributions by
|
||||||
|
# removing the need for manual address entry.
|
||||||
|
#
|
||||||
|
# Each entry must start with "naddr". Embedded relays are **strongly recommended**
|
||||||
|
# to assist client-side discovery.
|
||||||
|
#
|
||||||
|
# Empty lines are ignored. Lines starting with "#" are treated as comments.
|
||||||
|
|
||||||
|
naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqaueqyf8wumn8ghj7mn0wd68yt35wfejumnvqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqpp4mhxue69uhkummn9ekx7mqppamhxue69uhkummnw3ezumt0d5q3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qg5waehxw309ahx7um5wghx77r5wghxgetkxpx8xj
|
||||||
@@ -16,12 +16,22 @@
|
|||||||
|
|
||||||
|
|
||||||
use clap::{ArgGroup, Args};
|
use clap::{ArgGroup, Args};
|
||||||
use nostr::{event::EventBuilder, nips::nip19::Nip19Coordinate};
|
use futures::future;
|
||||||
|
use nostr::event::{EventBuilder, Tag};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, CommandRunner, parsers},
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
CommandRunner,
|
||||||
|
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
nostr_utils::{
|
||||||
|
NostrClient,
|
||||||
|
traits::{NaddrsUtils, NewGitRepositoryAnnouncement, ReposUtils},
|
||||||
|
utils,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -39,9 +49,12 @@ use crate::{
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub struct NewArgs {
|
pub struct NewArgs {
|
||||||
/// Repository address
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
#[arg(short, long, value_parser = parsers::repo_naddr)]
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
naddr: Nip19Coordinate,
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// Markdown content for the issue. Cannot be used together with the
|
/// Markdown content for the issue. Cannot be used together with the
|
||||||
/// `--editor` flag.
|
/// `--editor` flag.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
@@ -69,7 +82,7 @@ impl NewArgs {
|
|||||||
return Ok((None, content.trim().to_owned()));
|
return Ok((None, content.trim().to_owned()));
|
||||||
}
|
}
|
||||||
// If the `self.content` is `None` then the `self.editor` is `true`
|
// If the `self.content` is `None` then the `self.editor` is `true`
|
||||||
let file_content = utils::read_editor(".md")?;
|
let file_content = utils::read_editor(None, ".md")?;
|
||||||
if file_content.contains('\n') {
|
if file_content.contains('\n') {
|
||||||
Ok(file_content
|
Ok(file_content
|
||||||
.split_once('\n')
|
.split_once('\n')
|
||||||
@@ -84,30 +97,60 @@ impl NewArgs {
|
|||||||
|
|
||||||
impl CommandRunner for NewArgs {
|
impl CommandRunner for NewArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
let client = NostrClient::init(&options).await;
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = utils::naddrs_or_file(
|
||||||
|
self.naddrs.flat_naddrs(&config.sets)?,
|
||||||
|
&utils::nostr_address_path()?,
|
||||||
|
)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = options.pubkey().await?;
|
let user_pubk = options.pubkey().await?;
|
||||||
|
let mut naddrs_iter = naddrs.clone().into_iter();
|
||||||
|
|
||||||
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
|
|
||||||
let relays_list = client.user_relays_list(user_pubk).await?;
|
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
let mut write_relays =
|
|
||||||
utils::add_write_relays(options.relays.clone(), relays_list.as_ref());
|
|
||||||
client.add_relays(&options.relays).await;
|
|
||||||
client.add_relays(&self.naddr.relays).await;
|
|
||||||
write_relays.extend(client.fetch_repo(&self.naddr.coordinate).await?.relays);
|
|
||||||
|
|
||||||
let (subject, content) = self.issue_content()?;
|
let (subject, content) = self.issue_content()?;
|
||||||
let content_details = client.parse_content(&content).await;
|
let content_details = client.parse_content(&content).await;
|
||||||
write_relays.extend(content_details.write_relays.clone());
|
|
||||||
|
|
||||||
let event = EventBuilder::new_git_issue(
|
let event = EventBuilder::new_git_issue(
|
||||||
self.naddr.coordinate.clone(),
|
naddrs_iter
|
||||||
|
.next()
|
||||||
|
.expect("There is at least one address")
|
||||||
|
.coordinate
|
||||||
|
.clone(),
|
||||||
content,
|
content,
|
||||||
subject,
|
subject,
|
||||||
self.label,
|
self.label,
|
||||||
)?
|
)?
|
||||||
.tags(content_details.into_tags())
|
.dedup_tags()
|
||||||
.pow(options.pow)
|
.pow(options.pow)
|
||||||
|
.tags(content_details.clone().into_tags())
|
||||||
|
// p-tag the reset of the reposotoies owners
|
||||||
|
.tags(naddrs_iter.clone().map(|n| Tag::public_key(n.public_key)))
|
||||||
|
// a-tag the reset of the reposotoies
|
||||||
|
.tags(naddrs_iter.clone().map(|n| Tag::coordinate(n.coordinate, n.relays.first().cloned())))
|
||||||
.build(user_pubk);
|
.build(user_pubk);
|
||||||
let event_id = event.id.expect("There is an id");
|
let event_id = event.id.expect("There is an id");
|
||||||
|
|
||||||
|
let write_relays = [
|
||||||
|
relays,
|
||||||
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
|
client
|
||||||
|
.fetch_repos(&naddrs.into_coordinates())
|
||||||
|
.await?
|
||||||
|
.extract_relays(),
|
||||||
|
// Include read relays for each repository owner (if found)
|
||||||
|
future::join_all(naddrs_iter.map(|c| client.read_relays_from_user(c.public_key)))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
content_details.write_relays.clone().into_iter().collect(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
tracing::trace!(relays = ?write_relays, "Write relays list");
|
tracing::trace!(relays = ?write_relays, "Write relays list");
|
||||||
let success = client
|
let success = client
|
||||||
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
||||||
141
src/cli/commands/mod.rs
Normal file
141
src/cli/commands/mod.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
/// `issue` subcommands
|
||||||
|
mod issue;
|
||||||
|
/// 'reply` command
|
||||||
|
mod reply;
|
||||||
|
/// `repo` subcommands
|
||||||
|
mod repo;
|
||||||
|
/// `sets` subcommands
|
||||||
|
mod sets;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::{ArgGroup, Args, Parser};
|
||||||
|
use nostr::key::{Keys, PublicKey, SecretKey};
|
||||||
|
|
||||||
|
use self::issue::IssueSubcommands;
|
||||||
|
use self::reply::ReplyArgs;
|
||||||
|
use self::repo::RepoSubcommands;
|
||||||
|
use self::sets::SetsSubcommands;
|
||||||
|
use super::traits::CommandRunner;
|
||||||
|
use super::types::RelayOrSet;
|
||||||
|
use crate::error::{N34Error, N34Result};
|
||||||
|
|
||||||
|
/// Default path used when no path is provided via command line arguments.
|
||||||
|
///
|
||||||
|
/// This is a workaround since Clap doesn't support lazy evaluation of defaults.
|
||||||
|
pub const DEFAULT_FALLBACK_PATH: &str = "I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_LAZY_DEFAULT";
|
||||||
|
|
||||||
|
// TODO: Make the `signer` group optional
|
||||||
|
/// The command-line interface options
|
||||||
|
#[derive(Args, Clone)]
|
||||||
|
#[clap(
|
||||||
|
group(
|
||||||
|
ArgGroup::new("signer")
|
||||||
|
.args(&["secret_key"])
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct CliOptions {
|
||||||
|
/// Your Nostr secret key
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub secret_key: Option<SecretKey>,
|
||||||
|
/// Fallbacks relay to write and read from it. Multiple relays can be
|
||||||
|
/// passed.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub relays: Vec<RelayOrSet>,
|
||||||
|
/// Proof of Work difficulty when creatring events
|
||||||
|
#[arg(long, default_value_t = 0)]
|
||||||
|
pub pow: u8,
|
||||||
|
/// Config path [default: `$XDG_CONFIG_HOME` or `$HOME/.config`]
|
||||||
|
#[arg(long, value_name = "PATH", default_value = DEFAULT_FALLBACK_PATH,
|
||||||
|
hide_default_value = true
|
||||||
|
)]
|
||||||
|
pub config_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// N34 commands
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Manage reposotoies and relays sets
|
||||||
|
Sets {
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommands: SetsSubcommands,
|
||||||
|
},
|
||||||
|
/// Manage repositories
|
||||||
|
Repo {
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommands: RepoSubcommands,
|
||||||
|
},
|
||||||
|
/// Manage issues
|
||||||
|
Issue {
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommands: IssueSubcommands,
|
||||||
|
},
|
||||||
|
// /// Manage patches
|
||||||
|
// Patch {
|
||||||
|
// #[command(subcommand)]
|
||||||
|
// subcommands: PatchSubcommands,
|
||||||
|
// },
|
||||||
|
/// Reply to issues and patches.
|
||||||
|
Reply(ReplyArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl CommandRunner for Commands {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
tracing::trace!("Options: {options:#?}");
|
||||||
|
tracing::trace!("Handling: {self:#?}");
|
||||||
|
match self {
|
||||||
|
Self::Repo { subcommands } => subcommands.run(options).await,
|
||||||
|
Self::Issue { subcommands } => subcommands.run(options).await,
|
||||||
|
Commands::Reply(args) => args.run(options).await,
|
||||||
|
Commands::Sets { subcommands } => subcommands.run(options).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement `ensure_signer` function with similar functionality to
|
||||||
|
// `ensure_relays`
|
||||||
|
impl CliOptions {
|
||||||
|
/// Gets the public key of the user.
|
||||||
|
pub async fn pubkey(&self) -> N34Result<PublicKey> {
|
||||||
|
if let Some(sk) = &self.secret_key {
|
||||||
|
return Ok(Keys::new(sk.clone()).public_key());
|
||||||
|
}
|
||||||
|
unreachable!("There is no other method until now")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an error if there are no relays.
|
||||||
|
pub fn ensure_relays(&self) -> N34Result<()> {
|
||||||
|
if self.relays.is_empty() {
|
||||||
|
return Err(N34Error::EmptyRelays);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for CliOptions {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("CliOptions")
|
||||||
|
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
||||||
|
.field("relays", &self.relays)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
296
src/cli/commands/reply.rs
Normal file
296
src/cli/commands/reply.rs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::{fs, str::FromStr};
|
||||||
|
|
||||||
|
use clap::{ArgGroup, Args};
|
||||||
|
use futures::future;
|
||||||
|
use nostr::{
|
||||||
|
event::{Event, EventBuilder, EventId, Kind},
|
||||||
|
filter::Filter,
|
||||||
|
nips::{
|
||||||
|
nip01::{Coordinate, Metadata},
|
||||||
|
nip19::{self, FromBech32, ToBech32},
|
||||||
|
},
|
||||||
|
types::RelayUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{CliOptions, CommandRunner};
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
nostr_utils::{
|
||||||
|
NostrClient,
|
||||||
|
traits::{NaddrsUtils, ReposUtils},
|
||||||
|
utils,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Length of a Nostr npub (public key) in characters.
|
||||||
|
const NPUB_LEN: usize = 63;
|
||||||
|
/// The max date "9999-01-01 at 00:00 UTC"
|
||||||
|
const MAX_DATE: i64 = 253370764800;
|
||||||
|
|
||||||
|
/// Parses and represents a Nostr `nevent1` or `note1`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct NostrEvent {
|
||||||
|
/// Unique identifier for the event.
|
||||||
|
event_id: EventId,
|
||||||
|
/// List of relay URLs associated with the event. Empty if parsing a
|
||||||
|
/// `note1`.
|
||||||
|
relays: Vec<RelayUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrEvent {
|
||||||
|
/// Create a new [`NostrEvent`] instance
|
||||||
|
fn new(event_id: EventId, relays: Vec<RelayUrl>) -> Self {
|
||||||
|
Self { event_id, relays }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NostrEvent {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let str_event = s.trim().trim_start_matches("nostr:");
|
||||||
|
if str_event.starts_with("nevent1") {
|
||||||
|
let event = nip19::Nip19Event::from_bech32(str_event).map_err(|e| e.to_string())?;
|
||||||
|
Ok(Self::new(event.event_id, event.relays))
|
||||||
|
} else if str_event.starts_with("note1") {
|
||||||
|
Ok(Self::new(
|
||||||
|
EventId::from_bech32(str_event).map_err(|e| e.to_string())?,
|
||||||
|
Vec::new(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err("Invalid event id, must starts with `note1` or `nevent1`".to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Arguments for the `reply` command
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[clap(
|
||||||
|
group(
|
||||||
|
ArgGroup::new("comment-content")
|
||||||
|
.args(["comment", "editor"])
|
||||||
|
.required(true)
|
||||||
|
),
|
||||||
|
group(
|
||||||
|
ArgGroup::new("quote-reply-to")
|
||||||
|
.args(["comment", "quote_to"])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct ReplyArgs {
|
||||||
|
/// The issue, patch, or comment to reply to
|
||||||
|
#[arg(long, value_name = "nevent1-or-note1")]
|
||||||
|
to: NostrEvent,
|
||||||
|
/// Quote the replied-to event in the editor
|
||||||
|
#[arg(long)]
|
||||||
|
quote_to: bool,
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
/// The comment (cannot be used with --editor)
|
||||||
|
#[arg(short, long)]
|
||||||
|
comment: Option<String>,
|
||||||
|
/// Open editor to write comment (cannot be used with --content)
|
||||||
|
#[arg(short, long)]
|
||||||
|
editor: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for ReplyArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let nostr_address_path = utils::nostr_address_path()?;
|
||||||
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
|
let user_pubk = options.pubkey().await?;
|
||||||
|
|
||||||
|
let repo_naddrs = if let Some(naddrs) = self.naddrs.flat_naddrs(&config.sets)? {
|
||||||
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
|
Some(naddrs)
|
||||||
|
} else if fs::exists(&nostr_address_path).is_ok() {
|
||||||
|
let naddrs = utils::naddrs_or_file(None, &nostr_address_path)?;
|
||||||
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
|
Some(naddrs)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
client.add_relays(&self.to.relays).await;
|
||||||
|
|
||||||
|
let reply_to = client
|
||||||
|
.fetch_event(Filter::new().id(self.to.event_id))
|
||||||
|
.await?
|
||||||
|
.ok_or(N34Error::EventNotFound)?;
|
||||||
|
let root = client.find_root(reply_to.clone()).await?;
|
||||||
|
|
||||||
|
let repos_coordinate = if let Some(naddrs) = repo_naddrs {
|
||||||
|
naddrs.into_coordinates()
|
||||||
|
} else if let Some(ref root_event) = root {
|
||||||
|
coordinates_from_root(root_event)?
|
||||||
|
} else {
|
||||||
|
return Err(N34Error::NotFoundRepo);
|
||||||
|
};
|
||||||
|
|
||||||
|
let repos = client.fetch_repos(&repos_coordinate).await?;
|
||||||
|
|
||||||
|
let quoted_content = if self.quote_to {
|
||||||
|
Some(quote_reply_to_content(&client, &reply_to).await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = utils::get_content(self.comment.as_ref(), quoted_content.as_ref(), ".txt")?;
|
||||||
|
let content_details = client.parse_content(&content).await;
|
||||||
|
|
||||||
|
let event = EventBuilder::comment(
|
||||||
|
content,
|
||||||
|
&reply_to,
|
||||||
|
root.as_ref(),
|
||||||
|
repos.first().and_then(|r| r.relays.first()).cloned(),
|
||||||
|
)
|
||||||
|
.dedup_tags()
|
||||||
|
.pow(options.pow)
|
||||||
|
.tags(content_details.clone().into_tags())
|
||||||
|
.build(user_pubk);
|
||||||
|
|
||||||
|
let event_id = event.id.expect("There is an id");
|
||||||
|
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
|
let author_read_relays =
|
||||||
|
utils::add_read_relays(client.user_relays_list(user_pubk).await?.as_ref());
|
||||||
|
let write_relays = [
|
||||||
|
relays,
|
||||||
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
|
// Merge repository announcement relays into write relays
|
||||||
|
repos.extract_relays(),
|
||||||
|
// Include read relays for each repository owner (if found)
|
||||||
|
future::join_all(
|
||||||
|
repos_coordinate
|
||||||
|
.iter()
|
||||||
|
.map(|c| client.read_relays_from_user(c.public_key)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
// read relays of the root event and the reply to event
|
||||||
|
{
|
||||||
|
let (r1, r2) = future::join(
|
||||||
|
client.read_relays_from_user(reply_to.pubkey),
|
||||||
|
event_author_read_relays(&client, root.as_ref()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
[r1, r2].concat()
|
||||||
|
},
|
||||||
|
content_details.write_relays.into_iter().collect(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
tracing::trace!(relays = ?write_relays, "Write relays list");
|
||||||
|
let (success, ..) = futures::join!(
|
||||||
|
client.send_event_to(event, relays_list.as_ref(), &write_relays),
|
||||||
|
client.broadcast(&reply_to, &author_read_relays),
|
||||||
|
async {
|
||||||
|
if let Some(root_event) = root {
|
||||||
|
let _ = client.broadcast(&root_event, &author_read_relays).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
let nevent = utils::new_nevent(event_id, &success?)?;
|
||||||
|
println!("Comment created: {nevent}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a quoted reply string in the format "On yyyy-mm-dd at hh:mm UTC,
|
||||||
|
/// <author> wrote:" followed by the event content. Uses display name if
|
||||||
|
/// available, otherwise falls back to a shortened npub string. Dates are
|
||||||
|
/// formatted in UTC.
|
||||||
|
async fn quote_reply_to_content(client: &NostrClient, quoted_event: &Event) -> String {
|
||||||
|
let author_name = client
|
||||||
|
.fetch_event(
|
||||||
|
Filter::new()
|
||||||
|
.kind(Kind::Metadata)
|
||||||
|
.author(quoted_event.pubkey),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|e| Metadata::try_from(&e).ok())
|
||||||
|
.and_then(|m| m.display_name.or(m.name))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let pubkey = quoted_event
|
||||||
|
.pubkey
|
||||||
|
.to_bech32()
|
||||||
|
.expect("The error is `Infallible`");
|
||||||
|
format!("{}...{}", &pubkey[..8], &pubkey[NPUB_LEN - 8..])
|
||||||
|
});
|
||||||
|
|
||||||
|
let fdate = chrono::DateTime::from_timestamp(
|
||||||
|
quoted_event
|
||||||
|
.created_at
|
||||||
|
.as_u64()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or(MAX_DATE),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.map(|datetime| datetime.format("On %F at %R UTC, ").to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{fdate}{author_name} wrote:\n> {}",
|
||||||
|
quoted_event.content.trim().replace("\n", "\n> ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the repository coordinate from a root Nostr event's tags.
|
||||||
|
/// The event must contain a coordinate tag with GitRepoAnnouncement kind.
|
||||||
|
fn coordinates_from_root(root: &Event) -> N34Result<Vec<Coordinate>> {
|
||||||
|
let coordinates: Vec<Coordinate> = root
|
||||||
|
.tags
|
||||||
|
.coordinates()
|
||||||
|
.filter(|c| c.kind == Kind::GitRepoAnnouncement)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if coordinates.is_empty() {
|
||||||
|
return Err(N34Error::InvalidEvent(
|
||||||
|
"The Git issue/patch does not specify a target repository".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(coordinates)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the event author read relays if found, otherwise an empty vector
|
||||||
|
async fn event_author_read_relays(client: &NostrClient, event: Option<&Event>) -> Vec<RelayUrl> {
|
||||||
|
if let Some(root_event) = event {
|
||||||
|
client.read_relays_from_user(root_event.pubkey).await
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/cli/commands/repo/announce.rs
Normal file
155
src/cli/commands/repo/announce.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::{fs, io::Write};
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
use futures::future;
|
||||||
|
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{CliConfig, CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, types::RelayOrSetVecExt},
|
||||||
|
error::N34Result,
|
||||||
|
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Header written to new `nostr-address` files. Contains two trailing newline
|
||||||
|
/// for formatting.
|
||||||
|
const NOSTR_ADDRESS_FILE_HEADER: &str = r##"# This file contains NIP-19 `naddr` entities for repositories that accept this
|
||||||
|
# project's issues and patches.
|
||||||
|
#
|
||||||
|
# The file acts as a **read-only reference** for retrieving repository relays
|
||||||
|
# when embedded in an `naddr` and mentions those repositories when opening
|
||||||
|
# patches or issues. Modifications here will not affect in the relays, as the
|
||||||
|
# file is **explicitly untracked**. Its goal is to simplify contributions by
|
||||||
|
# removing the need for manual address entry.
|
||||||
|
#
|
||||||
|
# Each entry must start with "naddr". Embedded relays are **strongly recommended**
|
||||||
|
# to assist client-side discovery.
|
||||||
|
#
|
||||||
|
# Empty lines are ignored. Lines starting with "#" are treated as comments.
|
||||||
|
|
||||||
|
"##;
|
||||||
|
|
||||||
|
/// Arguments for the `repo announce` command
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct AnnounceArgs {
|
||||||
|
/// Unique identifier for the repository in kebab-case.
|
||||||
|
#[arg(long = "id")]
|
||||||
|
repo_id: String,
|
||||||
|
/// A name for the repository.
|
||||||
|
#[arg(short, long)]
|
||||||
|
name: Option<String>,
|
||||||
|
/// A description for the repository.
|
||||||
|
#[arg(short, long)]
|
||||||
|
description: Option<String>,
|
||||||
|
/// Webpage URLs for the repository (if provided by the git server).
|
||||||
|
#[arg(short, long)]
|
||||||
|
web: Vec<Url>,
|
||||||
|
/// URLs for cloning the repository.
|
||||||
|
#[arg(short, long)]
|
||||||
|
clone: Vec<Url>,
|
||||||
|
/// Additional maintainers of the repository (besides yourself).
|
||||||
|
#[arg(short, long)]
|
||||||
|
maintainers: Vec<PublicKey>,
|
||||||
|
/// Labels to categorize the repository. Can be specified multiple times.
|
||||||
|
#[arg(short, long)]
|
||||||
|
label: Vec<String>,
|
||||||
|
/// Skip kebab-case validation for the repository ID
|
||||||
|
#[arg(long)]
|
||||||
|
force_id: bool,
|
||||||
|
/// If set, creates a `nostr-address` file to enable automatic address
|
||||||
|
/// discovery by n34
|
||||||
|
#[arg(long)]
|
||||||
|
address_file: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for AnnounceArgs {
|
||||||
|
async fn run(mut self, options: CliOptions) -> N34Result<()> {
|
||||||
|
options.ensure_relays()?;
|
||||||
|
|
||||||
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
|
let user_pubk = options.pubkey().await?;
|
||||||
|
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
|
|
||||||
|
if !self.maintainers.contains(&user_pubk) {
|
||||||
|
self.maintainers.insert(0, user_pubk);
|
||||||
|
}
|
||||||
|
|
||||||
|
let naddr = utils::repo_naddr(&self.repo_id, user_pubk, &relays)?;
|
||||||
|
let event = EventBuilder::new_git_repo(
|
||||||
|
self.repo_id,
|
||||||
|
self.name.map(utils::str_trim),
|
||||||
|
self.description.map(utils::str_trim),
|
||||||
|
self.web,
|
||||||
|
self.clone,
|
||||||
|
relays.clone(),
|
||||||
|
self.maintainers.clone(),
|
||||||
|
self.label.into_iter().map(utils::str_trim).collect(),
|
||||||
|
self.force_id,
|
||||||
|
)?
|
||||||
|
.dedup_tags()
|
||||||
|
.pow(options.pow)
|
||||||
|
.build(user_pubk);
|
||||||
|
|
||||||
|
|
||||||
|
if self.address_file {
|
||||||
|
let address_path = std::env::current_dir()?.join(NOSTR_ADDRESS_FILE);
|
||||||
|
if !address_path.exists() {
|
||||||
|
tracing::info!(
|
||||||
|
"Creating new address file: '{NOSTR_ADDRESS_FILE}' at path '{}' with default \
|
||||||
|
header",
|
||||||
|
address_path.display()
|
||||||
|
);
|
||||||
|
fs::write(&address_path, NOSTR_ADDRESS_FILE_HEADER)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = fs::OpenOptions::new().append(true).open(&address_path)?;
|
||||||
|
|
||||||
|
tracing::info!("Appending naddr '{naddr}' to address file: '{NOSTR_ADDRESS_FILE}'");
|
||||||
|
file.write_all(format!("{naddr}\n").as_bytes())?;
|
||||||
|
tracing::info!("Successfully wrote naddr to address file");
|
||||||
|
}
|
||||||
|
|
||||||
|
let write_relays = [
|
||||||
|
relays,
|
||||||
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
|
// Include read relays for each maintainer (if found)
|
||||||
|
future::join_all(
|
||||||
|
self.maintainers
|
||||||
|
.iter()
|
||||||
|
.map(|pkey| client.read_relays_from_user(*pkey)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
let nevent = utils::new_nevent(event.id.expect("There is an id"), &write_relays)?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Event: {nevent}",);
|
||||||
|
println!("Repo Address: {naddr}",);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/cli/commands/repo/view.rs
Normal file
104
src/cli/commands/repo/view.rs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
CommandRunner,
|
||||||
|
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Arguments for the `repo view` command
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct ViewArgs {
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
///
|
||||||
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for ViewArgs {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
|
||||||
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = utils::naddrs_or_file(
|
||||||
|
self.naddrs.flat_naddrs(&config.sets)?,
|
||||||
|
&utils::nostr_address_path()?,
|
||||||
|
)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
|
|
||||||
|
let repos = client.fetch_repos(&naddrs.into_coordinates()).await?;
|
||||||
|
let mut repos_details: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for repo in repos {
|
||||||
|
let mut repo_details = format!("ID: {}", repo.id);
|
||||||
|
|
||||||
|
if let Some(name) = repo.name {
|
||||||
|
repo_details.push_str(&format!("\nName: {name}"));
|
||||||
|
}
|
||||||
|
if let Some(desc) = repo.description {
|
||||||
|
repo_details.push_str(&format!("\nDescription: {desc}"));
|
||||||
|
}
|
||||||
|
if !repo.web.is_empty() {
|
||||||
|
repo_details.push_str(&format!("\nWebpages:\n{}", format_list(repo.web)));
|
||||||
|
}
|
||||||
|
if !repo.clone.is_empty() {
|
||||||
|
repo_details.push_str(&format!("\nClone urls:\n{}", format_list(repo.clone)));
|
||||||
|
}
|
||||||
|
if !repo.relays.is_empty() {
|
||||||
|
repo_details.push_str(&format!("\nRelays:\n{}", format_list(repo.relays)));
|
||||||
|
}
|
||||||
|
if let Some(euc) = repo.euc {
|
||||||
|
repo_details.push_str(&format!("\nEarliest unique commit: {euc}"));
|
||||||
|
}
|
||||||
|
if !repo.maintainers.is_empty() {
|
||||||
|
repo_details.push_str(&format!(
|
||||||
|
"\nMaintainers:\n{}",
|
||||||
|
format_list(repo.maintainers)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
repos_details.push(repo_details);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", repos_details.join("\n----------\n"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a vector to print it
|
||||||
|
fn format_list<T>(vector: Vec<T>) -> String
|
||||||
|
where
|
||||||
|
T: fmt::Display,
|
||||||
|
{
|
||||||
|
vector
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| format!(" - {t}"))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
57
src/cli/commands/sets/mod.rs
Normal file
57
src/cli/commands/sets/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
/// `sets new` command
|
||||||
|
mod new;
|
||||||
|
/// `sets remove` command
|
||||||
|
mod remove;
|
||||||
|
/// `sets show` commands
|
||||||
|
mod show;
|
||||||
|
/// `sets update` command
|
||||||
|
mod update;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use self::new::NewArgs;
|
||||||
|
use self::remove::RemoveArgs;
|
||||||
|
use self::show::ShowArgs;
|
||||||
|
use self::update::UpdateArgs;
|
||||||
|
use super::{CliOptions, CommandRunner};
|
||||||
|
use crate::error::N34Result;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum SetsSubcommands {
|
||||||
|
/// Remove a set, or specific repos and relays within it
|
||||||
|
Remove(RemoveArgs),
|
||||||
|
/// Create a new set
|
||||||
|
New(NewArgs),
|
||||||
|
/// Modify an existing set
|
||||||
|
Update(UpdateArgs),
|
||||||
|
/// Show a single set or all the stored sets
|
||||||
|
Show(ShowArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl CommandRunner for SetsSubcommands {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Remove(args) => args.run(options).await,
|
||||||
|
Self::New(args) => args.run(options).await,
|
||||||
|
Self::Update(args) => args.run(options).await,
|
||||||
|
Self::Show(args) => args.run(options).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/cli/commands/sets/new.rs
Normal file
59
src/cli/commands/sets/new.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
ConfigError,
|
||||||
|
MutRepoRelaySetsExt,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct NewArgs {
|
||||||
|
/// Unique name for the set
|
||||||
|
name: String,
|
||||||
|
/// Optional relay to add it to the set, either as URL or set name to
|
||||||
|
/// extract its relays. [aliases: `--sr`]
|
||||||
|
#[arg(long = "set-relay", alias("sr"))]
|
||||||
|
relays: Vec<RelayOrSet>,
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Vec<NaddrOrSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for NewArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let mut config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = self.naddrs.flat_naddrs(&config.sets)?;
|
||||||
|
let relays = self.relays.flat_relays(&config.sets)?;
|
||||||
|
|
||||||
|
if relays.is_empty() && naddrs.is_empty() {
|
||||||
|
return Err(ConfigError::NewEmptySet.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
config.sets.push_set(self.name, naddrs, relays)?;
|
||||||
|
config.dump_toml(&options.config_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/cli/commands/sets/remove.rs
Normal file
65
src/cli/commands/sets/remove.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
MutRepoRelaySetsExt,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct RemoveArgs {
|
||||||
|
/// Set name to delete
|
||||||
|
name: String,
|
||||||
|
/// Specific relay to remove it from the set, either as URL or set name to
|
||||||
|
/// extract its relays. [aliases: `--sr`]
|
||||||
|
#[arg(long = "set-relay", alias("sr"))]
|
||||||
|
relays: Vec<RelayOrSet>,
|
||||||
|
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Vec<NaddrOrSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for RemoveArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let mut config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = self.naddrs.flat_naddrs(&config.sets)?;
|
||||||
|
let relays = self.relays.flat_relays(&config.sets)?;
|
||||||
|
|
||||||
|
if relays.is_empty() && naddrs.is_empty() {
|
||||||
|
config.sets.remove_set(self.name)?;
|
||||||
|
} else {
|
||||||
|
if !relays.is_empty() {
|
||||||
|
config.sets.remove_relays(&self.name, relays.into_iter())?;
|
||||||
|
}
|
||||||
|
if !naddrs.is_empty() {
|
||||||
|
config.sets.remove_naddrs(self.name, naddrs.into_iter())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.dump_toml(&options.config_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/cli/commands/sets/show.rs
Normal file
85
src/cli/commands/sets/show.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
use nostr::nips::nip19::ToBech32;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{CliConfig, CliOptions, RepoRelaySet, RepoRelaySetsExt, traits::CommandRunner},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct ShowArgs {
|
||||||
|
/// Name of the set to display. If not provided, lists all available sets.
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for ShowArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
|
||||||
|
if let Some(name) = self.name {
|
||||||
|
println!("{}", format_set(config.sets.as_slice().get_set(&name)?));
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
config
|
||||||
|
.sets
|
||||||
|
.iter()
|
||||||
|
.map(format_set)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n----------\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a set to view it to the user
|
||||||
|
fn format_set(set: &RepoRelaySet) -> String {
|
||||||
|
let naddrs = if set.naddrs.is_empty() {
|
||||||
|
"Nothing".to_owned()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"\n- {}",
|
||||||
|
set.naddrs
|
||||||
|
.iter()
|
||||||
|
.map(|naddr| naddr.to_bech32().expect("We did decoded before"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n- ")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let relays = if set.relays.is_empty() {
|
||||||
|
"Nothing".to_owned()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"\n- {}",
|
||||||
|
set.relays
|
||||||
|
.iter()
|
||||||
|
.map(|relay| relay.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n- ")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Name: {}\nّّRepositories: {naddrs}\nRelays: {relays}",
|
||||||
|
set.name
|
||||||
|
)
|
||||||
|
}
|
||||||
72
src/cli/commands/sets/update.rs
Normal file
72
src/cli/commands/sets/update.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
MutRepoRelaySetsExt,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct UpdateArgs {
|
||||||
|
/// Name of the set to update
|
||||||
|
name: String,
|
||||||
|
/// Add relay to the set, either as URL or set name to extract its relays.
|
||||||
|
/// [aliases: `--sr`]
|
||||||
|
#[arg(long = "set-relay", alias("sr"))]
|
||||||
|
relays: Vec<RelayOrSet>,
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Vec<NaddrOrSet>,
|
||||||
|
/// Replace existing relays/repositories instead of adding to them
|
||||||
|
#[arg(long = "override")]
|
||||||
|
override_set: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for UpdateArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let mut config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = self.naddrs.flat_naddrs(&config.sets)?;
|
||||||
|
let relays = self.relays.flat_relays(&config.sets)?;
|
||||||
|
|
||||||
|
let set = config.sets.get_mut_set(&self.name)?;
|
||||||
|
|
||||||
|
if self.override_set {
|
||||||
|
if !relays.is_empty() {
|
||||||
|
set.relays = HashSet::from_iter(relays);
|
||||||
|
}
|
||||||
|
if !naddrs.is_empty() {
|
||||||
|
set.naddrs = HashSet::from_iter(naddrs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
set.relays.extend(relays);
|
||||||
|
set.naddrs.extend(naddrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.dump_toml(&options.config_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
303
src/cli/config.rs
Normal file
303
src/cli/config.rs
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::{collections::HashSet, fs, path::Path};
|
||||||
|
|
||||||
|
use nostr::{
|
||||||
|
nips::nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
||||||
|
types::RelayUrl,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::error::{N34Error, N34Result};
|
||||||
|
|
||||||
|
/// Errors that can occur when working with configuration files.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error(
|
||||||
|
"Could not determine the default config path: both `$XDG_CONFIG_HOME` and `$HOME` \
|
||||||
|
environment variables are missing or unset."
|
||||||
|
)]
|
||||||
|
CanNotFindConfigPath,
|
||||||
|
#[error("Couldn't read the config file: {0}")]
|
||||||
|
ReadFile(std::io::Error),
|
||||||
|
#[error("Couldn't write in the config file: {0}")]
|
||||||
|
WriteFile(std::io::Error),
|
||||||
|
#[error("Couldn't serialize the config. This is a bug, please report it: {0}")]
|
||||||
|
Serialize(toml::ser::Error),
|
||||||
|
#[error("Failed to parse the config file: {0}")]
|
||||||
|
ParseFile(toml::de::Error),
|
||||||
|
#[error("Duplicate configuration set name detected: '{0}'. Each set must have a unique name.")]
|
||||||
|
SetDuplicateName(String),
|
||||||
|
#[error("No set with the given name `{0}`")]
|
||||||
|
SetNotFound(String),
|
||||||
|
#[error("You can't create an new empty set.")]
|
||||||
|
NewEmptySet,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the command-line interface.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)]
|
||||||
|
pub struct CliConfig {
|
||||||
|
/// Groups of repositories and relays.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub sets: Vec<RepoRelaySet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named group of repositories and relays.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Default, Clone, Debug)]
|
||||||
|
pub struct RepoRelaySet {
|
||||||
|
/// Unique identifier for this group.
|
||||||
|
pub name: String,
|
||||||
|
/// Repository addresses in this group.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "HashSet::is_empty",
|
||||||
|
serialize_with = "ser_naddrs",
|
||||||
|
deserialize_with = "de_naddrs"
|
||||||
|
)]
|
||||||
|
pub naddrs: HashSet<Nip19Coordinate>,
|
||||||
|
/// Relay URLs in this group.
|
||||||
|
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
|
||||||
|
pub relays: HashSet<RelayUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliConfig {
|
||||||
|
/// Reads and parse a TOML config file from the given path, creating it if
|
||||||
|
/// missing.
|
||||||
|
pub fn load_toml(file_path: &Path) -> N34Result<Self> {
|
||||||
|
tracing::info!(path = %file_path.display(), "Loading configuration from file");
|
||||||
|
// Make sure the file is exist
|
||||||
|
if let Some(parent) = file_path.parent() {
|
||||||
|
if !parent.exists() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = fs::File::create_new(file_path);
|
||||||
|
|
||||||
|
let mut config: Self =
|
||||||
|
toml::from_str(&fs::read_to_string(file_path).map_err(ConfigError::ReadFile)?)
|
||||||
|
.map_err(ConfigError::ParseFile)?;
|
||||||
|
|
||||||
|
config.post_sets()?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dump the config as toml in a file
|
||||||
|
pub fn dump_toml(mut self, file_path: &Path) -> N34Result<()> {
|
||||||
|
tracing::debug!(config = ?self, "Writing configuration to {}", file_path.display());
|
||||||
|
self.post_sets()?;
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
file_path,
|
||||||
|
toml::to_string_pretty(&self).map_err(ConfigError::Serialize)?,
|
||||||
|
)
|
||||||
|
.map_err(ConfigError::WriteFile)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs post-processing validation on the sets after loading or before
|
||||||
|
/// dumping.
|
||||||
|
fn post_sets(&mut self) -> N34Result<()> {
|
||||||
|
self.sets.as_slice().ensure_names()?;
|
||||||
|
self.sets.dedup_naddrs();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoRelaySet {
|
||||||
|
/// Create a new [`RepoRelaySet`]
|
||||||
|
pub fn new(
|
||||||
|
name: impl Into<String>,
|
||||||
|
naddrs: impl IntoIterator<Item = Nip19Coordinate>,
|
||||||
|
relays: impl IntoIterator<Item = RelayUrl>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
naddrs: HashSet::from_iter(naddrs),
|
||||||
|
relays: HashSet::from_iter(relays),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes duplicate repository addresses by comparing their coordinates,
|
||||||
|
/// ignoring embedded relays.
|
||||||
|
pub fn dedup_naddrs(&mut self) {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
self.naddrs.retain(|n| seen.insert(n.coordinate.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(MutRepoRelaySetsExt)]
|
||||||
|
impl Vec<RepoRelaySet> {
|
||||||
|
/// Removes duplicate repository addresses from each set.
|
||||||
|
///
|
||||||
|
/// Relays are automatically deduplicated by the HashSet, but
|
||||||
|
/// repository addresses may appear duplicated if relays are sorted
|
||||||
|
/// differently or when relay counts vary. This compares addresses by
|
||||||
|
/// their coordinates, ignoring any embedded relay details.
|
||||||
|
pub fn dedup_naddrs(&mut self) {
|
||||||
|
self.iter_mut().for_each(RepoRelaySet::dedup_naddrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds and returns a mutable reference a set with the given name. Returns
|
||||||
|
/// an error if no set with this name exists.
|
||||||
|
pub fn get_mut_set(&mut self, name: impl AsRef<str>) -> N34Result<&mut RepoRelaySet> {
|
||||||
|
let name = name.as_ref();
|
||||||
|
let set = self
|
||||||
|
.iter_mut()
|
||||||
|
.find(|set| set.name == name)
|
||||||
|
.ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
name = %name, set = ?set,
|
||||||
|
"Successfully located a set with the giving name"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates and pushes a new set with the given name.
|
||||||
|
///
|
||||||
|
/// Returns an error if a set with the same name already exists.
|
||||||
|
pub fn push_set(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
repos: impl IntoIterator<Item = Nip19Coordinate>,
|
||||||
|
relays: impl IntoIterator<Item = RelayUrl>,
|
||||||
|
) -> N34Result<()> {
|
||||||
|
let set_name: String = name.into();
|
||||||
|
tracing::trace!(sets = ?self, "Pushing set '{set_name}' to sets collection");
|
||||||
|
|
||||||
|
if self.as_slice().exists(&set_name) {
|
||||||
|
return Err(ConfigError::SetDuplicateName(set_name).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push(RepoRelaySet::new(set_name, repos, relays));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the set with the given name if it exists. Returns an error if
|
||||||
|
/// the set is not found.
|
||||||
|
pub fn remove_set(&mut self, name: impl Into<String>) -> N34Result<()> {
|
||||||
|
let set_name: String = name.into();
|
||||||
|
tracing::trace!(set_name, sets = ?self, "Removing set '{set_name}' from sets collection");
|
||||||
|
|
||||||
|
if !self.as_slice().exists(&set_name) {
|
||||||
|
return Err(ConfigError::SetNotFound(set_name).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.retain(|s| s.name != set_name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the given relays from the specified set.
|
||||||
|
pub fn remove_relays(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
relays: impl Iterator<Item = RelayUrl>,
|
||||||
|
) -> N34Result<()> {
|
||||||
|
let relays = Vec::from_iter(relays);
|
||||||
|
let set = self.get_mut_set(name.into())?;
|
||||||
|
|
||||||
|
set.relays.retain(|r| !relays.contains(r));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the given naddrs from the specified set.
|
||||||
|
pub fn remove_naddrs(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
naddrs: impl Iterator<Item = Nip19Coordinate>,
|
||||||
|
) -> N34Result<()> {
|
||||||
|
let coordinates = Vec::from_iter(naddrs.map(|n| n.coordinate));
|
||||||
|
let set = self.get_mut_set(name.into())?;
|
||||||
|
|
||||||
|
set.naddrs.retain(|n| !coordinates.contains(&n.coordinate));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(RepoRelaySetsExt)]
|
||||||
|
impl &[RepoRelaySet] {
|
||||||
|
/// Checks for duplicate set names. Returns an error if any duplicates are
|
||||||
|
/// found.
|
||||||
|
pub fn ensure_names(&self) -> N34Result<()> {
|
||||||
|
let mut names = Vec::with_capacity(self.len());
|
||||||
|
names.extend(self.iter().map(|s| s.name.to_owned()));
|
||||||
|
|
||||||
|
names.sort_unstable();
|
||||||
|
|
||||||
|
if let Some(duplicate) = duplicate_in_sorted(&names) {
|
||||||
|
return Err(ConfigError::SetDuplicateName(duplicate.clone()).into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a set with the given name exists.
|
||||||
|
pub fn exists(&self, set_name: &str) -> bool {
|
||||||
|
self.iter().any(|set| set.name == set_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds and returns a reference a set with the given name. Returns an
|
||||||
|
/// error if no set with this name exists.
|
||||||
|
pub fn get_set(&self, name: impl AsRef<str>) -> N34Result<&RepoRelaySet> {
|
||||||
|
let name = name.as_ref();
|
||||||
|
let set = self
|
||||||
|
.iter()
|
||||||
|
.find(|set| set.name == name)
|
||||||
|
.ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;
|
||||||
|
tracing::trace!(
|
||||||
|
name = %name, set = ?set,
|
||||||
|
"Successfully located a set with the giving name"
|
||||||
|
);
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function that checks for duplicates in a sorted slice
|
||||||
|
fn duplicate_in_sorted<T: PartialEq + Clone>(items: &[T]) -> Option<&T> {
|
||||||
|
items.windows(2).find(|w| w[0] == w[1]).map(|w| &w[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ser_naddrs<S>(naddr: &HashSet<Nip19Coordinate>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let str_naddrs = naddr
|
||||||
|
.iter()
|
||||||
|
.map(|n| n.to_bech32().map_err(|err| err.to_string()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(serde::ser::Error::custom)?;
|
||||||
|
|
||||||
|
str_naddrs.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de_naddrs<'de, D>(deserializer: D) -> Result<HashSet<Nip19Coordinate>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Vec::<String>::deserialize(deserializer)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|naddr| Nip19Coordinate::from_bech32(&naddr))
|
||||||
|
.collect::<Result<HashSet<_>, _>>()
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
28
src/cli/defaults.rs
Normal file
28
src/cli/defaults.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{cli::ConfigError, error::N34Result};
|
||||||
|
|
||||||
|
|
||||||
|
/// Default config path
|
||||||
|
pub fn config_path() -> N34Result<PathBuf> {
|
||||||
|
Ok(dirs::config_dir()
|
||||||
|
.ok_or(ConfigError::CanNotFindConfigPath)?
|
||||||
|
.join("n34")
|
||||||
|
.join("config.toml"))
|
||||||
|
}
|
||||||
105
src/cli/mod.rs
105
src/cli/mod.rs
@@ -14,27 +14,24 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
/// `issue` subcommands
|
/// Commands module
|
||||||
mod issue;
|
pub mod commands;
|
||||||
|
/// The CLI config
|
||||||
|
pub mod config;
|
||||||
|
/// Default lazy values for CLI arguments
|
||||||
|
pub mod defaults;
|
||||||
/// CLI arguments parsers
|
/// CLI arguments parsers
|
||||||
pub mod parsers;
|
pub mod parsers;
|
||||||
/// 'reply` command
|
|
||||||
mod reply;
|
|
||||||
/// `repo` subcommands
|
|
||||||
mod repo;
|
|
||||||
/// CLI traits
|
/// CLI traits
|
||||||
mod traits;
|
pub mod traits;
|
||||||
|
/// Common helper types used throughout the CLI.
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
use std::fmt;
|
use clap::Parser;
|
||||||
|
|
||||||
use clap::{ArgGroup, Args, Parser};
|
|
||||||
use clap_verbosity_flag::Verbosity;
|
use clap_verbosity_flag::Verbosity;
|
||||||
use nostr::key::Keys;
|
|
||||||
use nostr::{PublicKey, RelayUrl, SecretKey};
|
|
||||||
|
|
||||||
use self::issue::IssueSubcommands;
|
pub use self::commands::*;
|
||||||
use self::reply::ReplyArgs;
|
pub use self::config::*;
|
||||||
use self::repo::RepoSubcommands;
|
|
||||||
use self::traits::CommandRunner;
|
use self::traits::CommandRunner;
|
||||||
use crate::error::N34Result;
|
use crate::error::N34Result;
|
||||||
|
|
||||||
@@ -49,26 +46,8 @@ Git repository: https://git.4rs.nl/awiteb/n34.git"#;
|
|||||||
/// Footer message, used in the help message
|
/// Footer message, used in the help message
|
||||||
const FOOTER: &str = r#"Please report bugs to <naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqauesksc39>."#;
|
const FOOTER: &str = r#"Please report bugs to <naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqauesksc39>."#;
|
||||||
|
|
||||||
/// The command-line interface options
|
/// Name of the file storing the repository address
|
||||||
#[derive(Args, Clone)]
|
pub const NOSTR_ADDRESS_FILE: &str = "nostr-address";
|
||||||
#[clap(
|
|
||||||
group(
|
|
||||||
ArgGroup::new("auth")
|
|
||||||
.args(&["secret_key"])
|
|
||||||
.required(true)
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub struct CliOptions {
|
|
||||||
/// Your Nostr secret key
|
|
||||||
#[arg(short, long)]
|
|
||||||
pub secret_key: Option<SecretKey>,
|
|
||||||
/// Where your relays list. And repository relays if not included in naddr
|
|
||||||
#[arg(short, long, required = true)]
|
|
||||||
pub relays: Vec<RelayUrl>,
|
|
||||||
/// Proof of Work difficulty when creatring events
|
|
||||||
#[arg(long, default_value_t = 0)]
|
|
||||||
pub pow: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(about, version, before_long_help = HEADER, after_long_help = FOOTER)]
|
#[command(about, version, before_long_help = HEADER, after_long_help = FOOTER)]
|
||||||
@@ -76,36 +55,15 @@ pub struct CliOptions {
|
|||||||
/// code-related stuff.
|
/// code-related stuff.
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub options: CliOptions,
|
pub options: commands::CliOptions,
|
||||||
/// Controls the verbosity level of output
|
/// Controls the verbosity level of output
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub verbosity: Verbosity,
|
pub verbosity: Verbosity,
|
||||||
/// The subcommand to execute
|
/// The subcommand to execute
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: commands::Commands,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// N34 commands
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
pub enum Commands {
|
|
||||||
/// Manage repositories
|
|
||||||
Repo {
|
|
||||||
#[command(subcommand)]
|
|
||||||
subcommands: RepoSubcommands,
|
|
||||||
},
|
|
||||||
/// Manage issues
|
|
||||||
Issue {
|
|
||||||
#[command(subcommand)]
|
|
||||||
subcommands: IssueSubcommands,
|
|
||||||
},
|
|
||||||
// /// Manage patches
|
|
||||||
// Patch {
|
|
||||||
// #[command(subcommand)]
|
|
||||||
// subcommands: PatchSubcommands,
|
|
||||||
// },
|
|
||||||
/// Reply to issues and patches.
|
|
||||||
Reply(ReplyArgs),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
/// Executes the command
|
/// Executes the command
|
||||||
@@ -113,34 +71,3 @@ impl Cli {
|
|||||||
self.command.run(self.options).await
|
self.command.run(self.options).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandRunner for Commands {
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
|
||||||
tracing::trace!("Options: {options:#?}");
|
|
||||||
tracing::trace!("Handling: {self:#?}");
|
|
||||||
match self {
|
|
||||||
Self::Repo { subcommands } => subcommands.run(options).await,
|
|
||||||
Self::Issue { subcommands } => subcommands.run(options).await,
|
|
||||||
Commands::Reply(args) => args.run(options).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CliOptions {
|
|
||||||
/// Gets the public key of the user.
|
|
||||||
pub async fn pubkey(&self) -> N34Result<PublicKey> {
|
|
||||||
if let Some(sk) = &self.secret_key {
|
|
||||||
return Ok(Keys::new(sk.clone()).public_key());
|
|
||||||
}
|
|
||||||
unreachable!("There is no other method until now")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for CliOptions {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("CliOptions")
|
|
||||||
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
|
||||||
.field("relays", &self.relays)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,26 +14,52 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
Kind,
|
Kind,
|
||||||
nips::nip19::{FromBech32, Nip19Coordinate},
|
nips::nip19::{FromBech32, Nip19Coordinate},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Parses a Nostr naddr string into a Git repository announcement coordinate.
|
use super::Cli;
|
||||||
///
|
use crate::{
|
||||||
/// # Errors
|
cli::DEFAULT_FALLBACK_PATH,
|
||||||
/// Returns an error if:
|
error::{N34Error, N34Result},
|
||||||
/// - The bech32 decoding fails
|
};
|
||||||
/// - The naddr doesn't represent a Git repository announcement (kind != 30617)
|
|
||||||
pub fn repo_naddr(naddr: &str) -> Result<Nip19Coordinate, String> {
|
|
||||||
let naddr = Nip19Coordinate::from_bech32(naddr).map_err(|err| err.to_string())?;
|
|
||||||
if naddr.kind != Kind::GitRepoAnnouncement {
|
|
||||||
return Err("The naddr is not repo announcement address".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
pub fn parse_repo_naddr(repo_naddr: &str) -> Result<Nip19Coordinate, String> {
|
||||||
|
let naddr = Nip19Coordinate::from_bech32(repo_naddr).map_err(|err| err.to_string())?;
|
||||||
if naddr.relays.is_empty() {
|
if naddr.relays.is_empty() {
|
||||||
tracing::warn!("The repository naddr does not contain any relay hints");
|
tracing::warn!("The repository naddr does not contain any relay hints");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(naddr)
|
(naddr.kind == Kind::GitRepoAnnouncement)
|
||||||
|
.then_some(naddr)
|
||||||
|
.ok_or_else(|| "Invalid naddr: must be of kind 30617 (GitRepoAnnouncement)".to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a nostr-address file into a NIP-19 coordinates. Expects the file to
|
||||||
|
/// contain a repository announcements.
|
||||||
|
pub fn parse_nostr_address_file(file_path: &Path) -> N34Result<Vec<Nip19Coordinate>> {
|
||||||
|
let addresses = fs::read_to_string(file_path)
|
||||||
|
.map_err(N34Error::CanNotReadNostrAddressFile)?
|
||||||
|
.split("\n")
|
||||||
|
.filter_map(|line| {
|
||||||
|
(!line.starts_with("#") && !line.trim().is_empty())
|
||||||
|
.then_some(parse_repo_naddr(line).map_err(N34Error::InvalidNostrAddressFileContent))
|
||||||
|
})
|
||||||
|
.collect::<N34Result<Vec<Nip19Coordinate>>>()?;
|
||||||
|
if addresses.is_empty() {
|
||||||
|
return Err(N34Error::EmptyNostrAddressFile);
|
||||||
|
}
|
||||||
|
Ok(addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Post parse cli arguments
|
||||||
|
pub fn post_parse_cli(mut cli: Cli) -> N34Result<Cli> {
|
||||||
|
if let Some(DEFAULT_FALLBACK_PATH) = cli.options.config_path.to_str() {
|
||||||
|
cli.options.config_path = super::defaults::config_path()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cli)
|
||||||
}
|
}
|
||||||
|
|||||||
178
src/cli/reply.rs
178
src/cli/reply.rs
@@ -1,178 +0,0 @@
|
|||||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
|
||||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use clap::{ArgGroup, Args};
|
|
||||||
use nostr::{
|
|
||||||
event::{EventBuilder, EventId, Kind, Tag},
|
|
||||||
filter::Filter,
|
|
||||||
nips::nip19::{self, FromBech32, Nip19Coordinate},
|
|
||||||
types::RelayUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{CliOptions, CommandRunner, parsers};
|
|
||||||
use crate::{
|
|
||||||
error::{N34Error, N34Result},
|
|
||||||
nostr_utils::{NostrClient, utils},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Parses and represents a Nostr `nevent1` or `note1`.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct NostrEvent {
|
|
||||||
/// Unique identifier for the event.
|
|
||||||
event_id: EventId,
|
|
||||||
/// List of relay URLs associated with the event. Empty if parsing a
|
|
||||||
/// `note1`.
|
|
||||||
relays: Vec<RelayUrl>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NostrEvent {
|
|
||||||
/// Create a new [`NostrEvent`] instance
|
|
||||||
fn new(event_id: EventId, relays: Vec<RelayUrl>) -> Self {
|
|
||||||
Self { event_id, relays }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for NostrEvent {
|
|
||||||
type Err = String;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if s.trim().starts_with("nevent1") {
|
|
||||||
let event = nip19::Nip19Event::from_bech32(s).map_err(|e| e.to_string())?;
|
|
||||||
Ok(Self::new(event.event_id, event.relays))
|
|
||||||
} else if s.trim().starts_with("note1") {
|
|
||||||
Ok(Self::new(
|
|
||||||
EventId::from_bech32(s).map_err(|e| e.to_string())?,
|
|
||||||
Vec::new(),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Err("Invalid event id, must starts with `note1` or `nevent1`".to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Arguments for the `reply` command
|
|
||||||
#[derive(Args, Debug)]
|
|
||||||
#[clap(
|
|
||||||
group(
|
|
||||||
ArgGroup::new("comment-content")
|
|
||||||
.args(["comment", "editor"])
|
|
||||||
.required(true)
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub struct ReplyArgs {
|
|
||||||
/// The issue, patch, or comment to reply to
|
|
||||||
#[arg(long)]
|
|
||||||
to: NostrEvent,
|
|
||||||
/// Repository address
|
|
||||||
#[arg(short, long, value_parser = parsers::repo_naddr)]
|
|
||||||
naddr: Option<Nip19Coordinate>,
|
|
||||||
/// The comment (cannot be used with --editor)
|
|
||||||
#[arg(short, long)]
|
|
||||||
comment: Option<String>,
|
|
||||||
/// Open editor to write comment (cannot be used with --content)
|
|
||||||
#[arg(short, long)]
|
|
||||||
editor: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandRunner for ReplyArgs {
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
|
||||||
let client = NostrClient::init(&options).await;
|
|
||||||
let user_pubk = options.pubkey().await?;
|
|
||||||
let relays_list = client.user_relays_list(user_pubk).await?;
|
|
||||||
let mut write_relays =
|
|
||||||
utils::add_write_relays(options.relays.clone(), relays_list.as_ref());
|
|
||||||
client.add_relays(&options.relays).await;
|
|
||||||
client.add_relays(&self.to.relays).await;
|
|
||||||
|
|
||||||
if let Some(ref naddr) = self.naddr {
|
|
||||||
client.add_relays(&naddr.relays).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let reply_to = client
|
|
||||||
.fetch_event(Filter::new().id(self.to.event_id))
|
|
||||||
.await?
|
|
||||||
.ok_or(N34Error::EventNotFound)?;
|
|
||||||
let root = client.find_root(reply_to.clone()).await?;
|
|
||||||
|
|
||||||
|
|
||||||
let repo_naddr = if let Some(naddr) = self.naddr {
|
|
||||||
naddr.coordinate
|
|
||||||
} else if let Some(ref root_event) = root {
|
|
||||||
root_event
|
|
||||||
.tags
|
|
||||||
.coordinates()
|
|
||||||
.find(|c| c.kind == Kind::GitRepoAnnouncement)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
N34Error::InvalidEvent(
|
|
||||||
"The Git issue/patch does not specify a target repository".to_owned(),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.clone()
|
|
||||||
} else {
|
|
||||||
return Err(N34Error::NotFoundRepo);
|
|
||||||
};
|
|
||||||
|
|
||||||
let repo = client.fetch_repo(&repo_naddr).await?;
|
|
||||||
write_relays.extend(repo.relays.clone());
|
|
||||||
|
|
||||||
write_relays = client
|
|
||||||
.read_relays_from_user(write_relays, reply_to.pubkey)
|
|
||||||
.await;
|
|
||||||
if let Some(root_event) = &root {
|
|
||||||
write_relays = client
|
|
||||||
.read_relays_from_user(write_relays, root_event.pubkey)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = utils::get_content(self.comment.as_ref(), ".txt")?;
|
|
||||||
let content_details = client.parse_content(&content).await;
|
|
||||||
write_relays.extend(content_details.write_relays.clone());
|
|
||||||
|
|
||||||
let event = EventBuilder::comment(
|
|
||||||
content,
|
|
||||||
&reply_to,
|
|
||||||
root.as_ref(),
|
|
||||||
repo.relays.first().cloned(),
|
|
||||||
)
|
|
||||||
.tag(Tag::public_key(repo_naddr.public_key))
|
|
||||||
.tags(content_details.into_tags())
|
|
||||||
.pow(options.pow)
|
|
||||||
.build(user_pubk);
|
|
||||||
|
|
||||||
let event_id = event.id.expect("There is an id");
|
|
||||||
let author_read_relays = utils::add_read_relays(Vec::new(), relays_list.as_ref());
|
|
||||||
|
|
||||||
tracing::trace!(relays = ?write_relays, "Write relays list");
|
|
||||||
let (success, ..) = futures::join!(
|
|
||||||
client.send_event_to(event, relays_list.as_ref(), &write_relays),
|
|
||||||
client.broadcast(&reply_to, &author_read_relays),
|
|
||||||
async {
|
|
||||||
if let Some(root_event) = root {
|
|
||||||
let _ = client.broadcast(&root_event, &author_read_relays).await;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
let nevent = utils::new_nevent(event_id, &success?)?;
|
|
||||||
println!("Comment created: {nevent}");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
|
||||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
|
||||||
|
|
||||||
use clap::Args;
|
|
||||||
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
cli::{CliOptions, CommandRunner},
|
|
||||||
error::N34Result,
|
|
||||||
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/// Arguments for the `repo announce` command
|
|
||||||
#[derive(Args, Debug)]
|
|
||||||
pub struct AnnounceArgs {
|
|
||||||
/// Unique identifier for the repository in kebab-case.
|
|
||||||
#[arg(long = "id")]
|
|
||||||
repo_id: String,
|
|
||||||
/// A name for the repository.
|
|
||||||
#[arg(short, long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// A description for the repository.
|
|
||||||
#[arg(short, long)]
|
|
||||||
description: Option<String>,
|
|
||||||
/// Webpage URLs for the repository (if provided by the git server).
|
|
||||||
#[arg(short, long)]
|
|
||||||
web: Vec<Url>,
|
|
||||||
/// URLs for cloning the repository.
|
|
||||||
#[arg(short, long)]
|
|
||||||
clone: Vec<Url>,
|
|
||||||
/// Additional maintainers of the repository (besides yourself).
|
|
||||||
#[arg(short, long)]
|
|
||||||
maintainers: Vec<PublicKey>,
|
|
||||||
/// Labels to categorize the repository. Can be specified multiple times.
|
|
||||||
#[arg(short, long)]
|
|
||||||
label: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandRunner for AnnounceArgs {
|
|
||||||
async fn run(mut self, options: CliOptions) -> N34Result<()> {
|
|
||||||
let client = NostrClient::init(&options).await;
|
|
||||||
let user_pubk = options.pubkey().await?;
|
|
||||||
let relays_list = client.user_relays_list(user_pubk).await?;
|
|
||||||
let write_relays = utils::add_write_relays(options.relays.clone(), relays_list.as_ref());
|
|
||||||
|
|
||||||
if !self.maintainers.contains(&user_pubk) {
|
|
||||||
self.maintainers.insert(0, user_pubk);
|
|
||||||
}
|
|
||||||
|
|
||||||
let event = EventBuilder::new_git_repo(
|
|
||||||
self.repo_id,
|
|
||||||
self.name.map(utils::str_trim),
|
|
||||||
self.description.map(utils::str_trim),
|
|
||||||
self.web,
|
|
||||||
self.clone,
|
|
||||||
options.relays.clone(),
|
|
||||||
self.maintainers,
|
|
||||||
self.label.into_iter().map(utils::str_trim).collect(),
|
|
||||||
)?
|
|
||||||
.pow(options.pow)
|
|
||||||
.build(user_pubk);
|
|
||||||
|
|
||||||
let nevent = utils::new_nevent(event.id.expect("There is an id"), &write_relays)?;
|
|
||||||
let naddr = utils::repo_naddr(user_pubk, &options.relays)?;
|
|
||||||
client
|
|
||||||
.send_event_to(event, relays_list.as_ref(), &write_relays)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!("Event: {nevent}",);
|
|
||||||
println!("Repo Address: {naddr}",);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
|
||||||
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use clap::Args;
|
|
||||||
use nostr::nips::nip19::Nip19Coordinate;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
cli::{CliOptions, CommandRunner, parsers},
|
|
||||||
error::N34Result,
|
|
||||||
nostr_utils::NostrClient,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Arguments for the `repo view` command
|
|
||||||
#[derive(Args, Debug)]
|
|
||||||
pub struct ViewArgs {
|
|
||||||
/// Nostr repository address
|
|
||||||
#[arg(short, long, value_parser = parsers::repo_naddr)]
|
|
||||||
naddr: Nip19Coordinate,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandRunner for ViewArgs {
|
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
|
||||||
let client = NostrClient::init(&options).await;
|
|
||||||
if !self.naddr.relays.is_empty() {
|
|
||||||
client.add_relays(&self.naddr.relays).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo = client.fetch_repo(&self.naddr.coordinate).await?;
|
|
||||||
let mut msg = format!("ID: {}", repo.id);
|
|
||||||
|
|
||||||
if let Some(name) = repo.name {
|
|
||||||
msg.push_str(&format!("\nName: {name}"));
|
|
||||||
}
|
|
||||||
if let Some(desc) = repo.description {
|
|
||||||
msg.push_str(&format!("\nDescription: {desc}"));
|
|
||||||
}
|
|
||||||
if !repo.web.is_empty() {
|
|
||||||
msg.push_str(&format!("\nWebpages:\n{}", format_list(repo.web)));
|
|
||||||
}
|
|
||||||
if !repo.clone.is_empty() {
|
|
||||||
msg.push_str(&format!("\nClone urls:\n{}", format_list(repo.clone)));
|
|
||||||
}
|
|
||||||
if !repo.relays.is_empty() {
|
|
||||||
msg.push_str(&format!("\nRelays:\n{}", format_list(repo.relays)));
|
|
||||||
}
|
|
||||||
if let Some(euc) = repo.euc {
|
|
||||||
msg.push_str(&format!("\nEarliest unique commit: {euc}"));
|
|
||||||
}
|
|
||||||
if !repo.maintainers.is_empty() {
|
|
||||||
msg.push_str(&format!(
|
|
||||||
"\nMaintainers:\n{}",
|
|
||||||
format_list(repo.maintainers)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{msg}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format a vector to print it
|
|
||||||
fn format_list<T>(vector: Vec<T>) -> String
|
|
||||||
where
|
|
||||||
T: fmt::Display,
|
|
||||||
{
|
|
||||||
vector
|
|
||||||
.into_iter()
|
|
||||||
.map(|t| format!(" - {t}"))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
194
src/cli/types.rs
Normal file
194
src/cli/types.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use nostr::{
|
||||||
|
event::Kind,
|
||||||
|
nips::{self, nip01::Coordinate, nip19::Nip19Coordinate},
|
||||||
|
types::RelayUrl,
|
||||||
|
};
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
|
use super::{RepoRelaySetsExt, parsers};
|
||||||
|
use crate::{
|
||||||
|
cli::RepoRelaySet,
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Either a NIP-19 coordinate (naddr) or a named set.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum NaddrOrSet {
|
||||||
|
/// NIP-19 coordinate.
|
||||||
|
Naddr(Nip19Coordinate),
|
||||||
|
/// Name of a set (may not exist).
|
||||||
|
Set(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Either relay URL or a named set.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RelayOrSet {
|
||||||
|
/// Relay URL.
|
||||||
|
Relay(RelayUrl),
|
||||||
|
/// Name of a set (may not exist).
|
||||||
|
Set(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NaddrOrSet {
|
||||||
|
/// Returns the naddr if `Naddr` or try to get the relays from the set.
|
||||||
|
/// Returns error if the set naddrs are empty or the set not found.
|
||||||
|
pub fn get_naddrs(self, sets: &[RepoRelaySet]) -> N34Result<Vec<Nip19Coordinate>> {
|
||||||
|
match self {
|
||||||
|
Self::Naddr(nip19_coordinate) => Ok(vec![nip19_coordinate]),
|
||||||
|
Self::Set(name) => {
|
||||||
|
let set = sets
|
||||||
|
.get_set(&name)
|
||||||
|
.map_err(|_| N34Error::InvalidNaddrArg(name.clone()))?;
|
||||||
|
if set.naddrs.is_empty() {
|
||||||
|
Err(N34Error::EmptySetNaddrs(name))
|
||||||
|
} else {
|
||||||
|
Ok(Vec::from_iter(set.naddrs.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl RelayOrSet {
|
||||||
|
/// Returns the relay if `Relay` or try to get the relays from the set.
|
||||||
|
/// Returns error if the set relays are empty or the set not found
|
||||||
|
pub fn get_relays(self, sets: &[RepoRelaySet]) -> N34Result<Vec<RelayUrl>> {
|
||||||
|
match self {
|
||||||
|
Self::Relay(relay) => Ok(vec![relay]),
|
||||||
|
Self::Set(name) => {
|
||||||
|
let set = sets.get_set(&name)?;
|
||||||
|
if set.relays.is_empty() {
|
||||||
|
Err(N34Error::EmptySetRelays(name))
|
||||||
|
} else {
|
||||||
|
Ok(Vec::from_iter(set.relays.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NaddrOrSet {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
/// Parses a Git repository address which can be either:
|
||||||
|
/// - A bech32-encoded naddr (e.g. "naddr1...") for Git repository
|
||||||
|
/// announcements (kind 30617)
|
||||||
|
/// - A NIP-05 identifier with repository ID (e.g. "4rs.nl/n34" or
|
||||||
|
/// "_@4rs.nl/n34")
|
||||||
|
/// - A set name.
|
||||||
|
///
|
||||||
|
/// Returns an error for invalid formats, failed bech32 decoding, wrong
|
||||||
|
/// event kind.
|
||||||
|
fn from_str(naddr_or_set: &str) -> Result<Self, Self::Err> {
|
||||||
|
let naddr_or_set = naddr_or_set.trim();
|
||||||
|
|
||||||
|
if naddr_or_set.contains("/") {
|
||||||
|
let (nip5, repo_id) = naddr_or_set.split_once("/").expect("There is a `/`");
|
||||||
|
parse_nip5_repo(nip5, repo_id)
|
||||||
|
} else if naddr_or_set.starts_with("naddr1") || naddr_or_set.starts_with("nostr:naddr1") {
|
||||||
|
parsers::parse_repo_naddr(naddr_or_set.trim_start_matches("nostr:")).map(Self::Naddr)
|
||||||
|
} else {
|
||||||
|
Ok(Self::Set(naddr_or_set.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RelayOrSet {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
/// Parse a string into a relay URL or a set name.
|
||||||
|
/// If the string is a valid URL (e.g., "wss://example.com"), it's treated
|
||||||
|
/// as a relay URL. Otherwise, it's treated as a set name, and its
|
||||||
|
/// associated relays will be merged.
|
||||||
|
fn from_str(relay_or_set: &str) -> Result<Self, Self::Err> {
|
||||||
|
let relay_or_set = relay_or_set.trim();
|
||||||
|
|
||||||
|
if relay_or_set.starts_with("wss://") {
|
||||||
|
RelayUrl::from_str(relay_or_set)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
.map(Self::Relay)
|
||||||
|
} else {
|
||||||
|
Ok(Self::Set(relay_or_set.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<NaddrOrSet, String> {
|
||||||
|
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
|
||||||
|
|
||||||
|
let nip5_profile = tokio::task::block_in_place(|| {
|
||||||
|
Handle::current().block_on(async {
|
||||||
|
nips::nip05::profile(format!("{username}@{domain}"), None)
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(NaddrOrSet::Naddr(
|
||||||
|
Nip19Coordinate::new(
|
||||||
|
Coordinate::new(Kind::GitRepoAnnouncement, nip5_profile.public_key).identifier(repo_id),
|
||||||
|
nip5_profile.relays,
|
||||||
|
)
|
||||||
|
.expect("The relays is `RelayUrl`"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(NaddrOrSetVecExt)]
|
||||||
|
impl Vec<NaddrOrSet> {
|
||||||
|
/// Converts this vector of [`NaddrOrSet`] into a flat vector of
|
||||||
|
/// [`Nip19Coordinates`] using the given sets.
|
||||||
|
pub fn flat_naddrs(self, sets: &[RepoRelaySet]) -> N34Result<Vec<Nip19Coordinate>> {
|
||||||
|
self.into_iter()
|
||||||
|
.map(|n| n.get_naddrs(sets))
|
||||||
|
.try_fold(Vec::new(), |mut acc, item| {
|
||||||
|
acc.extend(item?);
|
||||||
|
Ok(acc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(RelayOrSetVecExt)]
|
||||||
|
impl Vec<RelayOrSet> {
|
||||||
|
/// Converts this vector of [`RelayOrSet`] into a flat vector of
|
||||||
|
/// [`RelayUrl`] using the given sets.
|
||||||
|
pub fn flat_relays(self, sets: &[RepoRelaySet]) -> N34Result<Vec<RelayUrl>> {
|
||||||
|
self.into_iter()
|
||||||
|
.map(|n| n.get_relays(sets))
|
||||||
|
.try_fold(Vec::new(), |mut acc, item| {
|
||||||
|
acc.extend(item?);
|
||||||
|
Ok(acc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[easy_ext::ext(OptionNaddrOrSetVecExt)]
|
||||||
|
impl Option<Vec<NaddrOrSet>> {
|
||||||
|
/// Converts this vector of [`NaddrOrSet`] into a flat vector of
|
||||||
|
/// [`Nip19Coordinates`] using the given sets.
|
||||||
|
pub fn flat_naddrs(&self, sets: &[RepoRelaySet]) -> N34Result<Option<Vec<Nip19Coordinate>>> {
|
||||||
|
// Clones self here to simplify command code
|
||||||
|
self.clone()
|
||||||
|
.map(|naddrs| naddrs.flat_naddrs(sets))
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/error.rs
28
src/error.rs
@@ -19,6 +19,8 @@ use std::process::ExitCode;
|
|||||||
use nostr::event::builder::Error as EventBuilderError;
|
use nostr::event::builder::Error as EventBuilderError;
|
||||||
use nostr_sdk::client::Error as ClientError;
|
use nostr_sdk::client::Error as ClientError;
|
||||||
|
|
||||||
|
use crate::cli::ConfigError;
|
||||||
|
|
||||||
pub type N34Result<T> = Result<T, N34Error>;
|
pub type N34Result<T> = Result<T, N34Error>;
|
||||||
|
|
||||||
/// N34 errors
|
/// N34 errors
|
||||||
@@ -26,6 +28,8 @@ pub type N34Result<T> = Result<T, N34Error>;
|
|||||||
pub enum N34Error {
|
pub enum N34Error {
|
||||||
#[error("IO: {0}")]
|
#[error("IO: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Config(#[from] ConfigError),
|
||||||
#[error("No editor specified in the `EDITOR` environment variable")]
|
#[error("No editor specified in the `EDITOR` environment variable")]
|
||||||
EditorNotFound,
|
EditorNotFound,
|
||||||
#[error("The file you edited is empty. Please save your changes before exiting the editor.")]
|
#[error("The file you edited is empty. Please save your changes before exiting the editor.")]
|
||||||
@@ -51,6 +55,30 @@ pub enum N34Error {
|
|||||||
to."
|
to."
|
||||||
)]
|
)]
|
||||||
CanNotReplyToEvent,
|
CanNotReplyToEvent,
|
||||||
|
#[error("No repository address given and couldn't read `nostr-address` file: {0}")]
|
||||||
|
CanNotReadNostrAddressFile(std::io::Error),
|
||||||
|
#[error(
|
||||||
|
"The `nostr-address` file is empty. Please add a valid Nostr repository address (naddr) \
|
||||||
|
to the file or provide it manually as a flag."
|
||||||
|
)]
|
||||||
|
EmptyNostrAddressFile,
|
||||||
|
#[error("Invalid `nostr-address` file content: {0}")]
|
||||||
|
InvalidNostrAddressFileContent(String),
|
||||||
|
#[error("This command requires at least one relay, but none were provided")]
|
||||||
|
EmptyRelays,
|
||||||
|
#[error(
|
||||||
|
"Invalid repository address. Expected one of these formats:\n- NIP-05 identifier with \
|
||||||
|
repository ID: `<user@domain.com>/<repo_id>`\n- Valid NIP-19 naddr string (starts with \
|
||||||
|
'naddr1...')\n- Existing set name (merges all repositories in set)\nError: No set named \
|
||||||
|
'{0}' exists."
|
||||||
|
)]
|
||||||
|
InvalidNaddrArg(String),
|
||||||
|
#[error(
|
||||||
|
"The set '{0}' doesn't contain any addresses. Use 'sets update' to add addresses to it."
|
||||||
|
)]
|
||||||
|
EmptySetNaddrs(String),
|
||||||
|
#[error("The set '{0}' doesn't contain any relays. Use 'sets update' to add addresses to it.")]
|
||||||
|
EmptySetRelays(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl N34Error {
|
impl N34Error {
|
||||||
|
|||||||
@@ -61,7 +61,14 @@ fn set_log_level(verbosity: Verbosity) {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
let cli = Cli::parse();
|
let cli = match cli::parsers::post_parse_cli(Cli::parse()) {
|
||||||
|
Ok(cli) => cli,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{err}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
set_log_level(cli.verbosity);
|
set_log_level(cli.verbosity);
|
||||||
|
|
||||||
if let Err(err) = cli.run().await {
|
if let Err(err) = cli.run().await {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ use crate::{
|
|||||||
const CLIENT_TIMEOUT: Duration = Duration::from_millis(1500);
|
const CLIENT_TIMEOUT: Duration = Duration::from_millis(1500);
|
||||||
|
|
||||||
/// Parsed content details
|
/// Parsed content details
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ContentDetails {
|
pub struct ContentDetails {
|
||||||
/// Public keys of users mentioned in the content.
|
/// Public keys of users mentioned in the content.
|
||||||
pub p_tagged: HashSet<PublicKey>,
|
pub p_tagged: HashSet<PublicKey>,
|
||||||
@@ -82,6 +83,7 @@ impl ContentDetails {
|
|||||||
tags.extend(self.hashtags.into_iter().map(Tag::hashtag));
|
tags.extend(self.hashtags.into_iter().map(Tag::hashtag));
|
||||||
tags.extend(self.p_tagged.into_iter().map(Tag::public_key));
|
tags.extend(self.p_tagged.into_iter().map(Tag::public_key));
|
||||||
tags.extend(self.quotes.into_iter().map(|(event_id, relay_url)| {
|
tags.extend(self.quotes.into_iter().map(|(event_id, relay_url)| {
|
||||||
|
// TODO: Add the author public key if we know it
|
||||||
Tag::from_standardized(TagStandard::Quote {
|
Tag::from_standardized(TagStandard::Quote {
|
||||||
event_id,
|
event_id,
|
||||||
relay_url,
|
relay_url,
|
||||||
@@ -100,7 +102,7 @@ impl NostrClient {
|
|||||||
|
|
||||||
/// Initializes a new [`NostrClient`] instance and connects to the specified
|
/// Initializes a new [`NostrClient`] instance and connects to the specified
|
||||||
/// relays.
|
/// relays.
|
||||||
pub async fn init(options: &CliOptions) -> Self {
|
pub async fn init(options: &CliOptions, relays: &[RelayUrl]) -> Self {
|
||||||
let client = Self::new(
|
let client = Self::new(
|
||||||
Client::builder()
|
Client::builder()
|
||||||
.signer(Keys::new(
|
.signer(Keys::new(
|
||||||
@@ -113,7 +115,7 @@ impl NostrClient {
|
|||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
client.add_relays(&options.relays).await;
|
client.add_relays(relays).await;
|
||||||
client
|
client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,18 +164,24 @@ impl NostrClient {
|
|||||||
relays_list: Option<&Event>,
|
relays_list: Option<&Event>,
|
||||||
relays: &[RelayUrl],
|
relays: &[RelayUrl],
|
||||||
) -> N34Result<Vec<RelayUrl>> {
|
) -> N34Result<Vec<RelayUrl>> {
|
||||||
event.ensure_id();
|
|
||||||
self.add_relays(relays).await;
|
self.add_relays(relays).await;
|
||||||
let event_id = event.id.expect("It's there");
|
let event_id = event.id();
|
||||||
|
|
||||||
if let Some(event) = relays_list {
|
let (result, ..) = futures::join!(
|
||||||
let _ = self.client.send_event_to(relays, event).await;
|
async {
|
||||||
}
|
N34Result::Ok(
|
||||||
|
self.client
|
||||||
let result = self
|
.send_event_to(relays, &event.sign(&self.client.signer().await?).await?)
|
||||||
.client
|
.await?,
|
||||||
.send_event_to(relays, &event.sign(&self.client.signer().await?).await?)
|
)
|
||||||
.await?;
|
},
|
||||||
|
async {
|
||||||
|
if let Some(event) = relays_list {
|
||||||
|
let _ = self.client.send_event_to(relays, event).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let result = result?;
|
||||||
|
|
||||||
for relay in &result.success {
|
for relay in &result.success {
|
||||||
tracing::info!(event_id = %event_id, relay = %relay, "Event sent successfully");
|
tracing::info!(event_id = %event_id, relay = %relay, "Event sent successfully");
|
||||||
@@ -195,20 +203,27 @@ impl NostrClient {
|
|||||||
.first_owned())
|
.first_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to fetch a repository and returns it
|
/// Try to fetch the reposotoies and returns them
|
||||||
pub async fn fetch_repo(
|
pub async fn fetch_repos(
|
||||||
&self,
|
&self,
|
||||||
repo_naddr: &Coordinate,
|
repo_naddrs: &[Coordinate],
|
||||||
) -> N34Result<GitRepositoryAnnouncement> {
|
) -> N34Result<Vec<GitRepositoryAnnouncement>> {
|
||||||
let filter = Filter::new()
|
future::join_all(repo_naddrs.iter().map(|c| {
|
||||||
.author(repo_naddr.public_key)
|
async {
|
||||||
.kind(Kind::GitRepoAnnouncement)
|
self.fetch_event(
|
||||||
.identifier(&repo_naddr.identifier);
|
Filter::new()
|
||||||
|
.author(c.public_key)
|
||||||
self.fetch_event(filter)
|
.identifier(&c.identifier)
|
||||||
.await?
|
.kind(Kind::GitRepoAnnouncement),
|
||||||
.map(|e| utils::event_into_repo(e, &repo_naddr.identifier))
|
)
|
||||||
.ok_or(N34Error::NotFoundRepo)
|
.await?
|
||||||
|
.map(|e| utils::event_into_repo(e, &c.identifier))
|
||||||
|
.ok_or(N34Error::NotFoundRepo)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the root issue or patch for a given event. If the event is already
|
/// Finds the root issue or patch for a given event. If the event is already
|
||||||
@@ -268,16 +283,10 @@ impl NostrClient {
|
|||||||
.map(|e| e.pubkey))
|
.map(|e| e.pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds read relays from the user to the given vector of relays.
|
/// Returns the read relays of the given user if found, otherwise empty
|
||||||
pub async fn read_relays_from_user(
|
/// vector
|
||||||
&self,
|
pub async fn read_relays_from_user(&self, user: PublicKey) -> Vec<RelayUrl> {
|
||||||
vector: Vec<RelayUrl>,
|
utils::add_read_relays(self.user_relays_list(user).await.ok().flatten().as_ref())
|
||||||
user: PublicKey,
|
|
||||||
) -> Vec<RelayUrl> {
|
|
||||||
utils::add_read_relays(
|
|
||||||
vector,
|
|
||||||
self.user_relays_list(user).await.ok().flatten().as_ref(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the given content and returns the details that inside it
|
/// Parse the given content and returns the details that inside it
|
||||||
@@ -300,14 +309,14 @@ impl NostrClient {
|
|||||||
|
|
||||||
for (user, relays) in &p_tagged_users {
|
for (user, relays) in &p_tagged_users {
|
||||||
self.add_relays(relays).await;
|
self.add_relays(relays).await;
|
||||||
write_relays = self.read_relays_from_user(write_relays, *user).await
|
write_relays.extend(self.read_relays_from_user(*user).await);
|
||||||
}
|
}
|
||||||
for (event_id, relays) in "es {
|
for (event_id, relays) in "es {
|
||||||
self.add_relays(relays).await;
|
self.add_relays(relays).await;
|
||||||
// Add the event author to the p-tagged users
|
// Add the event author to the p-tagged users
|
||||||
if let Ok(Some(author)) = self.event_author(*event_id).await {
|
if let Ok(Some(author)) = self.event_author(*event_id).await {
|
||||||
p_tagged_users.push((author, Vec::new()));
|
p_tagged_users.push((author, Vec::new()));
|
||||||
write_relays = self.read_relays_from_user(write_relays, author).await;
|
write_relays.extend(self.read_relays_from_user(author).await);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use nostr::{
|
|||||||
key::PublicKey,
|
key::PublicKey,
|
||||||
nips::{
|
nips::{
|
||||||
nip01::Coordinate,
|
nip01::Coordinate,
|
||||||
|
nip19::Nip19Coordinate,
|
||||||
nip21::Nip21,
|
nip21::Nip21,
|
||||||
nip34::{GitIssue, GitRepositoryAnnouncement},
|
nip34::{GitIssue, GitRepositoryAnnouncement},
|
||||||
},
|
},
|
||||||
@@ -76,9 +77,17 @@ impl EventBuilder {
|
|||||||
relays: Vec<RelayUrl>,
|
relays: Vec<RelayUrl>,
|
||||||
maintainers: Vec<PublicKey>,
|
maintainers: Vec<PublicKey>,
|
||||||
labels: Vec<String>,
|
labels: Vec<String>,
|
||||||
|
force_id: bool,
|
||||||
) -> N34Result<EventBuilder> {
|
) -> N34Result<EventBuilder> {
|
||||||
let repo_id = repo_id.trim();
|
let repo_id = repo_id.trim();
|
||||||
if repo_id.is_empty() || repo_id != repo_id.to_case(Case::Kebab) {
|
let kebab_repo_id = repo_id.to_case(Case::Kebab);
|
||||||
|
if repo_id.is_empty() || (!force_id && repo_id != kebab_repo_id) {
|
||||||
|
if repo_id != kebab_repo_id {
|
||||||
|
tracing::error!(
|
||||||
|
"The repo id should be `{kebab_repo_id}` (kebab-case). Use `--force-id` to \
|
||||||
|
override this check"
|
||||||
|
);
|
||||||
|
}
|
||||||
return Err(N34Error::InvalidRepoId);
|
return Err(N34Error::InvalidRepoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +102,7 @@ impl EventBuilder {
|
|||||||
euc: None,
|
euc: None,
|
||||||
maintainers,
|
maintainers,
|
||||||
})?
|
})?
|
||||||
|
.dedup_tags()
|
||||||
.tags(labels.into_iter().map(Tag::hashtag)),
|
.tags(labels.into_iter().map(Tag::hashtag)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -156,3 +166,26 @@ impl Token<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Utility functions for working with lists of NIP-19 coordinates
|
||||||
|
#[easy_ext::ext(NaddrsUtils)]
|
||||||
|
impl Vec<Nip19Coordinate> {
|
||||||
|
/// Converts these coordinate addresses to basic coordinates
|
||||||
|
pub fn into_coordinates(self) -> Vec<Coordinate> {
|
||||||
|
self.into_iter().map(|n| n.coordinate).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts all relay URLs from these coordinates
|
||||||
|
pub fn extract_relays(&self) -> Vec<RelayUrl> {
|
||||||
|
self.iter().flat_map(|n| n.relays.clone()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility functions for working with lists of repository announcement
|
||||||
|
#[easy_ext::ext(ReposUtils)]
|
||||||
|
impl Vec<GitRepositoryAnnouncement> {
|
||||||
|
/// Extracts all relay URLs from these reposotoies
|
||||||
|
pub fn extract_relays(&self) -> Vec<RelayUrl> {
|
||||||
|
self.iter().flat_map(|n| n.relays.clone()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
|
||||||
|
|
||||||
use std::{fmt, fs, str::FromStr, sync::atomic::Ordering};
|
use std::{
|
||||||
|
fmt,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
sync::atomic::Ordering,
|
||||||
|
};
|
||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
event::{Event, EventId, Kind, TagKind, TagStandard},
|
event::{Event, EventId, Kind, TagKind, TagStandard},
|
||||||
@@ -30,7 +36,10 @@ use nostr::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::traits::TagsExt;
|
use super::traits::TagsExt;
|
||||||
use crate::error::{N34Error, N34Result};
|
use crate::{
|
||||||
|
cli::{NOSTR_ADDRESS_FILE, parsers},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
/// Returns the value of the given tag
|
/// Returns the value of the given tag
|
||||||
fn tag_value(tag: &TagStandard) -> String {
|
fn tag_value(tag: &TagStandard) -> String {
|
||||||
@@ -117,9 +126,13 @@ pub fn new_nevent(event_id: EventId, relays: &[RelayUrl]) -> N34Result<String> {
|
|||||||
|
|
||||||
/// Creates a NIP-19 naddr string for a git repository announcement and up to 3
|
/// Creates a NIP-19 naddr string for a git repository announcement and up to 3
|
||||||
/// unique relay URLs.
|
/// unique relay URLs.
|
||||||
pub fn repo_naddr(pubk: PublicKey, relays: &[RelayUrl]) -> N34Result<String> {
|
pub fn repo_naddr(
|
||||||
|
repo_id: impl Into<String>,
|
||||||
|
pubk: PublicKey,
|
||||||
|
relays: &[RelayUrl],
|
||||||
|
) -> N34Result<String> {
|
||||||
Nip19Coordinate::new(
|
Nip19Coordinate::new(
|
||||||
Coordinate::new(Kind::GitRepoAnnouncement, pubk),
|
Coordinate::new(Kind::GitRepoAnnouncement, pubk).identifier(repo_id),
|
||||||
dedup(relays.iter().cloned()).iter().take(3),
|
dedup(relays.iter().cloned()).iter().take(3),
|
||||||
)
|
)
|
||||||
.expect("Valid relays")
|
.expect("Valid relays")
|
||||||
@@ -127,8 +140,10 @@ pub fn repo_naddr(pubk: PublicKey, relays: &[RelayUrl]) -> N34Result<String> {
|
|||||||
.map_err(N34Error::from)
|
.map_err(N34Error::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extends the given vector with write relays found in the event (if any).
|
/// Extracts write relay URLs from an event if present, otherwise returns an
|
||||||
pub fn add_write_relays(mut vector: Vec<RelayUrl>, event: Option<&Event>) -> Vec<RelayUrl> {
|
/// empty vector.
|
||||||
|
pub fn add_write_relays(event: Option<&Event>) -> Vec<RelayUrl> {
|
||||||
|
let mut vector = Vec::new();
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
vector.extend(
|
vector.extend(
|
||||||
nip65::extract_owned_relay_list(event.clone())
|
nip65::extract_owned_relay_list(event.clone())
|
||||||
@@ -138,8 +153,10 @@ pub fn add_write_relays(mut vector: Vec<RelayUrl>, event: Option<&Event>) -> Vec
|
|||||||
vector
|
vector
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extends the given vector with read relays found in the event (if any).
|
/// Extracts read relay URLs from an event if present, otherwise returns an
|
||||||
pub fn add_read_relays(mut vector: Vec<RelayUrl>, event: Option<&Event>) -> Vec<RelayUrl> {
|
/// empty vector.
|
||||||
|
pub fn add_read_relays(event: Option<&Event>) -> Vec<RelayUrl> {
|
||||||
|
let mut vector = Vec::new();
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
vector.extend(
|
vector.extend(
|
||||||
nip65::extract_owned_relay_list(event.clone())
|
nip65::extract_owned_relay_list(event.clone())
|
||||||
@@ -153,13 +170,17 @@ pub fn add_read_relays(mut vector: Vec<RelayUrl>, event: Option<&Event>) -> Vec<
|
|||||||
/// Opens the user's default editor ($EDITOR) to edit a temporary file with
|
/// Opens the user's default editor ($EDITOR) to edit a temporary file with
|
||||||
/// given suffix, then reads and returns the file contents. The temporary file
|
/// given suffix, then reads and returns the file contents. The temporary file
|
||||||
/// is automatically deleted.
|
/// is automatically deleted.
|
||||||
pub fn read_editor(file_suffix: &str) -> N34Result<String> {
|
pub fn read_editor(file_pre_content: Option<&str>, file_suffix: &str) -> N34Result<String> {
|
||||||
let Ok(editor) = std::env::var("EDITOR") else {
|
let Ok(editor) = std::env::var("EDITOR") else {
|
||||||
return Err(N34Error::EditorNotFound);
|
return Err(N34Error::EditorNotFound);
|
||||||
};
|
};
|
||||||
|
|
||||||
let temp_path = tempfile::NamedTempFile::with_suffix(file_suffix)?.into_temp_path();
|
let temp_path = tempfile::NamedTempFile::with_suffix(file_suffix)?.into_temp_path();
|
||||||
|
|
||||||
|
if let Some(pre_content) = file_pre_content {
|
||||||
|
fs::write(&temp_path, pre_content)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Disable the logs to not show up in a terminal text editor
|
// Disable the logs to not show up in a terminal text editor
|
||||||
crate::EDITOR_OPEN.store(true, Ordering::Relaxed);
|
crate::EDITOR_OPEN.store(true, Ordering::Relaxed);
|
||||||
let exit_status = std::process::Command::new(editor)
|
let exit_status = std::process::Command::new(editor)
|
||||||
@@ -186,12 +207,33 @@ pub fn read_editor(file_suffix: &str) -> N34Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the given content if it's `Option::Some` or call [`read_editor`]
|
/// Returns the given content if it's `Option::Some` or call [`read_editor`]
|
||||||
pub fn get_content<S>(content: Option<S>, file_suffix: &str) -> N34Result<String>
|
pub fn get_content(
|
||||||
where
|
content: Option<impl AsRef<str>>,
|
||||||
S: AsRef<str>,
|
quoted_content: Option<impl AsRef<str>>,
|
||||||
{
|
file_suffix: &str,
|
||||||
|
) -> N34Result<String> {
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
return Ok(content.as_ref().trim().to_owned());
|
return Ok(content.as_ref().trim().to_owned());
|
||||||
}
|
}
|
||||||
read_editor(file_suffix)
|
read_editor(
|
||||||
|
quoted_content.map(|s| s.as_ref().to_owned()).as_deref(),
|
||||||
|
file_suffix,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Path to the `nostr-address` file in current directory.
|
||||||
|
pub fn nostr_address_path() -> std::io::Result<PathBuf> {
|
||||||
|
std::env::current_dir().map(|p| p.join(NOSTR_ADDRESS_FILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the given coordinate is Some, return it. Otherwise, try to read and parse
|
||||||
|
/// the first non-comment line from the `nostr-address` file.
|
||||||
|
pub fn naddrs_or_file(
|
||||||
|
naddrs: Option<Vec<Nip19Coordinate>>,
|
||||||
|
address_file_path: &Path,
|
||||||
|
) -> N34Result<Vec<Nip19Coordinate>> {
|
||||||
|
if let Some(naddrs) = naddrs {
|
||||||
|
return Ok(naddrs);
|
||||||
|
}
|
||||||
|
parsers::parse_nostr_address_file(address_file_path)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user