Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aa3b62de0 | ||
|
|
a8015d618b | ||
|
|
2bcdb29a88 | ||
|
|
4a4c0dcd34 | ||
|
|
43ca689190 | ||
|
|
14ab9b38e2 | ||
|
|
802da70056 | ||
|
|
cff0b2f3b2 | ||
|
|
9c55bf8af1 | ||
|
|
904d14064f | ||
|
|
ad215002e2 | ||
|
|
67339a8a47 | ||
|
|
1d83e445a4 | ||
|
|
8bb4cf0da4 | ||
|
|
e17e75e53b | ||
|
|
67e25da8c0 | ||
|
|
bc8c6f3154 | ||
|
|
0ba58dcbd0 | ||
|
|
78113c7402 | ||
|
|
bd3ba1bdbf | ||
|
|
2630104e99 | ||
|
|
03d5c8020e | ||
|
|
c3aef2e0ad | ||
|
|
4e0ecdceaf | ||
|
|
f0c20c3677 | ||
|
|
e6943aefab | ||
|
|
353358f0ed |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/book
|
||||||
|
|||||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -5,6 +5,31 @@ 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.4.0] - 2025-08-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support signing using NIP-46 bunker - by Awiteb
|
||||||
|
- Keyring the secret key `n34 config keyring --enable` - by Awiteb
|
||||||
|
- New flag to `patch apply and merge` to mention patches - by Awiteb
|
||||||
|
- Sign using NIP-07 - by Awiteb
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Add `keyring`, `nostr-connect`, `nostr-keyring` and `url` to the dependencies - by Awiteb
|
||||||
|
- Remove `url` from `n34` dependencies - by Awiteb
|
||||||
|
- Upgrade to `nostr@0.43.0` - by Awiteb
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- N34 book - by Awiteb
|
||||||
|
- Fix status command docs - by Awiteb
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Move the trait extensions to `traits.rs` - by Awiteb
|
||||||
|
- Move `root` and `root-revision` to constants - by Awiteb
|
||||||
|
|
||||||
## [0.3.0] - 2025-07-05
|
## [0.3.0] - 2025-07-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -101,6 +126,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Initialize the project - by Awiteb
|
- Initialize the project - by Awiteb
|
||||||
|
|
||||||
|
[0.4.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.4.0
|
||||||
[0.3.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.3.0
|
[0.3.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.3.0
|
||||||
[0.2.0]: https://git.4rs.nl/awiteb/n34.git/tag/?h=v0.2.0
|
[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
|
||||||
|
|||||||
651
Cargo.lock
generated
651
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
58
Cargo.toml
58
Cargo.toml
@@ -1,11 +1,13 @@
|
|||||||
[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 code in Nostr"
|
||||||
version = "0.3.0"
|
version = "0.4.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>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
documentation = "https://n34.dev/commands.html"
|
||||||
|
homepage = "https://n34.dev"
|
||||||
repository = "https://git.4rs.nl/awiteb/n34.git"
|
repository = "https://git.4rs.nl/awiteb/n34.git"
|
||||||
keywords = ["nostr", "NIP-34", "CLI"]
|
keywords = ["nostr", "NIP-34", "CLI"]
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
@@ -13,26 +15,58 @@ rust-version = "1.88.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
clap = { version = "4.5.37", features = ["derive"] }
|
|
||||||
clap-verbosity-flag = { version = "3.0.2", default-features = false, features = [
|
|
||||||
"tracing",
|
|
||||||
] }
|
|
||||||
convert_case = "0.8.0"
|
convert_case = "0.8.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
easy-ext = "1.0.2"
|
easy-ext = "1.0.2"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
nostr = { version = "0.42.2", features = ["parser", "nip05"] }
|
nostr-browser-signer-proxy = "0.43.0"
|
||||||
nostr-sdk = "0.42.0"
|
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
tempfile = "3.20.0"
|
||||||
tempfile = "3.19.1"
|
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
|
toml = "0.9.4"
|
||||||
toml = "0.8.22"
|
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
|
|
||||||
|
[dependencies.clap]
|
||||||
|
features = ["derive"]
|
||||||
|
version = "4.5.42"
|
||||||
|
|
||||||
|
[dependencies.clap-verbosity-flag]
|
||||||
|
default-features = false
|
||||||
|
features = ["tracing"]
|
||||||
|
version = "3.0.3"
|
||||||
|
|
||||||
|
[dependencies.nostr]
|
||||||
|
default-features = false
|
||||||
|
features = ["std"]
|
||||||
|
version = "0.43.0"
|
||||||
|
|
||||||
|
[dependencies.nostr-connect]
|
||||||
|
default-features = false
|
||||||
|
version = "0.43.0"
|
||||||
|
|
||||||
|
[dependencies.nostr-keyring]
|
||||||
|
default-features = false
|
||||||
|
version = "0.43.0"
|
||||||
|
|
||||||
|
[dependencies.nostr-sdk]
|
||||||
|
default-features = false
|
||||||
|
version = "0.43.0"
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
default-features = false
|
||||||
|
features = ["http2", "json", "rustls-tls"]
|
||||||
|
version = "0.12.22"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
features = ["derive"]
|
||||||
|
version = "1.0.219"
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
features = ["macros", "rt-multi-thread"]
|
||||||
|
version = "1.47.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
71
README.md
71
README.md
@@ -1,9 +1,6 @@
|
|||||||
!!! warning "Work In Progress"
|
|
||||||
This project is still work in progress and not completed yet.
|
|
||||||
|
|
||||||
# n34
|
# n34
|
||||||
|
|
||||||
A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
A CLI to interact with NIP-34 and other stuff related to code in Nostr
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@@ -16,6 +13,10 @@ The primary goal of `n34` is to implement [NIP-34] (`git` stuff), but its
|
|||||||
flexible design allows for additional use cases beyond Git workflows. For more
|
flexible design allows for additional use cases beyond Git workflows. For more
|
||||||
details, see the following section.
|
details, see the following section.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Check the documentation at [n34.dev]
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [X] Repository announcements
|
- [X] Repository announcements
|
||||||
@@ -24,9 +25,12 @@ details, see the following section.
|
|||||||
- [X] Issues (Send, view and list)
|
- [X] Issues (Send, view and list)
|
||||||
- [X] Replies
|
- [X] Replies
|
||||||
- [X] Issues and patches status
|
- [X] Issues and patches status
|
||||||
|
- [ ] Pull requests ([nostr-protocol/nips#1966])
|
||||||
- [X] Gossip Model ([NIP-65])
|
- [X] Gossip Model ([NIP-65])
|
||||||
- [X] Proof of Work ([NIP-13])
|
- [X] Proof of Work ([NIP-13])
|
||||||
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
- [X] `nostr:` URI scheme, in the issue/reply content ([NIP-21])
|
||||||
|
- [X] Signing using bunker ([NIP-46])
|
||||||
|
- [X] Signing using [NIP-07] proxy ([nostr-browser-signer-proxy])
|
||||||
- [ ] Code Snippets ([NIP-C0])
|
- [ ] Code Snippets ([NIP-C0])
|
||||||
- [X] In device relays and repos bookmark (`sets` command)
|
- [X] In device relays and repos bookmark (`sets` command)
|
||||||
|
|
||||||
@@ -54,6 +58,59 @@ into a future-proof foundation for decentralized collaboration.
|
|||||||
- <https://nostr.org>
|
- <https://nostr.org>
|
||||||
- <https://nostr.how/en/what-is-nostr>
|
- <https://nostr.how/en/what-is-nostr>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You can install n34 either by cloning the repository and building it with Cargo,
|
||||||
|
or by using `cargo install` or Nix.
|
||||||
|
|
||||||
|
### Building from source
|
||||||
|
|
||||||
|
- Clone the repository:
|
||||||
|
```sh
|
||||||
|
git clone git://git.4rs.nl/awiteb/n34.git
|
||||||
|
cd n34
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build the release version:
|
||||||
|
```sh
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
The binary will be available at `target/release/n34`.
|
||||||
|
|
||||||
|
### Using cargo install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install n34
|
||||||
|
```
|
||||||
|
The binary will be installed to your Cargo binary directory (typically `~/.cargo/bin/n34`).
|
||||||
|
|
||||||
|
Make sure `~/.cargo/bin` is in your `PATH` environment variable to run the binary from anywhere.
|
||||||
|
|
||||||
|
### Using `nix build` (+v0.4)
|
||||||
|
|
||||||
|
- Clone the repository.
|
||||||
|
- Run the `nix build` command.
|
||||||
|
|
||||||
|
The binary will be available at `result/bin/n34`.
|
||||||
|
|
||||||
|
### Adding it to your [home-manager] (+v0.4)
|
||||||
|
|
||||||
|
- Add it as an input to your `flake.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
inputs = {
|
||||||
|
# Specify the version you want to install, or remove `?ref` for the unreleased
|
||||||
|
# version. You can also use any mirror; it doesn't have to be `git.4rs.nl`.
|
||||||
|
n34.url = "git+https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add it to your packages (ensure your home-manager `extraSpecialArgs` includes the `inputs`):
|
||||||
|
|
||||||
|
```nix
|
||||||
|
packages = [ inputs.n34.packages."${pkgs.system}".default ];
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions to `n34` are welcome! You can help by opening issues (such as bug
|
Contributions to `n34` are welcome! You can help by opening issues (such as bug
|
||||||
@@ -77,3 +134,9 @@ refer to the [LICENSE](LICENSE) file for more details.
|
|||||||
[NIP-13]: https://github.com/nostr-protocol/nips/blob/master/13.md
|
[NIP-13]: https://github.com/nostr-protocol/nips/blob/master/13.md
|
||||||
[NIP-21]: https://github.com/nostr-protocol/nips/blob/master/21.md
|
[NIP-21]: https://github.com/nostr-protocol/nips/blob/master/21.md
|
||||||
[NIP-C0]: https://github.com/nostr-protocol/nips/blob/master/C0.md
|
[NIP-C0]: https://github.com/nostr-protocol/nips/blob/master/C0.md
|
||||||
|
[NIP-46]: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||||
|
[NIP-07]: https://github.com/nostr-protocol/nips/blob/master/07.md
|
||||||
|
[nostr-protocol/nips#1966]: https://github.com/nostr-protocol/nips/pull/1966
|
||||||
|
[nostr-browser-signer-proxy]: https://crates.io/crates/nostr-browser-signer-proxy
|
||||||
|
[home-manager]: https://github.com/nix-community/home-manager
|
||||||
|
[n34.dev]: https://n34.dev
|
||||||
|
|||||||
10
book.toml
Normal file
10
book.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[book]
|
||||||
|
authors = ["Awiteb"]
|
||||||
|
language = "en"
|
||||||
|
multilingual = false
|
||||||
|
src = "docs"
|
||||||
|
title = "n34 documentation"
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
git-repository-icon = "fa-git"
|
||||||
|
git-repository-url = "https://git.4rs.nl/awiteb/n34.git"
|
||||||
34
docs/SUMMARY.md
Normal file
34
docs/SUMMARY.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# n34 documentation
|
||||||
|
|
||||||
|
- [Command Line Usage](commands.md)
|
||||||
|
- [Managing Repository and Relay Sets](sets/README.md)
|
||||||
|
- [Create a Set](sets/new.md)
|
||||||
|
- [Modify a Set](sets/update.md)
|
||||||
|
- [Show Sets](sets/show.md)
|
||||||
|
- [Remove a Set](sets/remove.md)
|
||||||
|
- [Manage Configuration](config/README.md)
|
||||||
|
- [Default PoW Difficulty](config/pow.md)
|
||||||
|
- [Fallback Relays](config/relays.md)
|
||||||
|
- [NIP-07 Browser Signer Proxy](config/nip07.md)
|
||||||
|
- [NIP-46 Bunker](config/bunker.md)
|
||||||
|
- [Secret Key Keyring](config/keyring.md)
|
||||||
|
- [Manage Repositories](repo/README.md)
|
||||||
|
- [Broadcast and Update a Git Repository](repo/announce.md)
|
||||||
|
- [View Git Repository Details](repo/view.md)
|
||||||
|
- [Issue Management](issue/README.md)
|
||||||
|
- [Create an Issue](issue/new.md)
|
||||||
|
- [View an Issue By ID](issue/view.md)
|
||||||
|
- [Reopen a Closed Issue](issue/reopen.md)
|
||||||
|
- [Closes an Open Issue](issue/close.md)
|
||||||
|
- [Resolves an Issue](issue/resolve.md)
|
||||||
|
- [List Repositories Issues](issue/list.md)
|
||||||
|
- [Patch Management](patch/README.md)
|
||||||
|
- [Send Patches to a Repository](patch/send.md)
|
||||||
|
- [Fetch a Patch By ID](patch/fetch.md)
|
||||||
|
- [Reopens a Closed or Drafted Patch](patch/reopen.md)
|
||||||
|
- [Closes an Open or Drafted Patch](patch/close.md)
|
||||||
|
- [Draft an Open Patch](patch/draft.md)
|
||||||
|
- [Apply an Open Patch](patch/apply.md)
|
||||||
|
- [Merge an Open Patch](patch/merge.md)
|
||||||
|
- [List Repositories Patches](patch/list.md)
|
||||||
|
- [Reply to Issues and Patches](reply.md)
|
||||||
66
docs/commands.md
Normal file
66
docs/commands.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Command-Line Usage
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
The `n34` command-line tool accepts the following options:
|
||||||
|
|
||||||
|
- `-s`, `--secret-key`: Your Nostr secret key (in `nsec` format), used for
|
||||||
|
signing events.
|
||||||
|
- `-b`, `--bunker-url`: The URL of a NIP-46 bunker service used for remote
|
||||||
|
signing of events.
|
||||||
|
- `-7`, `--nip07`: Enables signing events using the browser's NIP-07
|
||||||
|
extension. Listens on `127.0.0.1:51034`. You can configure the address with `n34
|
||||||
|
config nip07`
|
||||||
|
- `-r`, `--relays`: A relay to read from and write to. This option can be
|
||||||
|
specified multiple times to connect to several relays.
|
||||||
|
- `--pow`: Sets the Proof of Work difficulty required when creating events.
|
||||||
|
- `--config`: Specifies a custom path to the configuration file (Default:
|
||||||
|
`$HOME/.config/n34/config.toml`).
|
||||||
|
- `-v`, `--verbose...`: Increases the logging verbosity. Can be used multiple
|
||||||
|
times for more detail (e.g., `-v`, `-vv`).
|
||||||
|
|
||||||
|
**Note:** The `--secret-key` and `--bunker-url` options are mutually exclusive.
|
||||||
|
You must provide exactly one signing method.
|
||||||
|
|
||||||
|
## Multiple Repositories
|
||||||
|
|
||||||
|
Commands that interact with a repository, such as submitting an issue or a
|
||||||
|
patch, can accept multiple repository addresses (`naddr`). This feature is
|
||||||
|
useful for projects with multiple maintainers who each have their own repository
|
||||||
|
fork.
|
||||||
|
|
||||||
|
> **Important:** When you provide multiple repositories, `n34` does not
|
||||||
|
create a separate issue or patch for each one. Instead, it creates a single
|
||||||
|
event that references all of the specified repositories.
|
||||||
|
|
||||||
|
## The `nostr-address` File
|
||||||
|
|
||||||
|
The `nostr-address` file is a plain text file that stores a list of project
|
||||||
|
repository addresses. This allows the `n34` to find and use them
|
||||||
|
without requiring you to enter the addresses manually.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
- Each line must contain a single addressable event coordinate `naddr` which is
|
||||||
|
the repository address.
|
||||||
|
- Lines beginning with a `#` are treated as comments and are ignored.
|
||||||
|
- Empty lines are also ignored.
|
||||||
|
|
||||||
|
## Passing repositories
|
||||||
|
|
||||||
|
By default, `n34` will look for a `nostr-address` file to extract repositories
|
||||||
|
from it. This is why repositories are not required for commands like `patch
|
||||||
|
send` and `issue new`. You can also pass repositories using the `--repo`
|
||||||
|
option or the `<NADDR-NIP05-OR-SET>` argument for commands that accept them. The
|
||||||
|
supported formats for manual input are:
|
||||||
|
|
||||||
|
- A [NIP-19] addressable event coordinate `naddr`.
|
||||||
|
- A [NIP-05] identifier and repository name, in the format
|
||||||
|
`<nip05>/<repo-name>`.
|
||||||
|
- A set name that contains repository addresses.
|
||||||
|
|
||||||
|
You do not need to specify relays for these commands if your `naddr` or `NIP-05`
|
||||||
|
identifier already includes relays; `n34` will automatically extract them.
|
||||||
|
|
||||||
|
[NIP-19]: https://github.com/nostr-protocol/nips/blob/master/19.md
|
||||||
|
[NIP-05]: https://github.com/nostr-protocol/nips/blob/master/05.md
|
||||||
7
docs/config/README.md
Normal file
7
docs/config/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Manage Configuration
|
||||||
|
|
||||||
|
Configuration allows you to set default values for various command parameters,
|
||||||
|
such as fallback relays, Proof of Work (PoW) difficulty, a default bunker URL,
|
||||||
|
and your Nostr private key (keyring). This avoids the need to enter your private key,
|
||||||
|
bunker URL, relays, or PoW difficulty for every command, making `n34` more
|
||||||
|
convenient to use.
|
||||||
27
docs/config/bunker.md
Normal file
27
docs/config/bunker.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# NIP-46 Bunker
|
||||||
|
|
||||||
|
> `n34 config bunker` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Sets a URL of NIP-46 bunker server used for signing events
|
||||||
|
|
||||||
|
Usage: n34 config bunker [BUNKER_URL]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[BUNKER_URL] Nostr Connect URL for the bunker. Omit this to remove the current bunker URL
|
||||||
|
```
|
||||||
|
|
||||||
|
This command configures `n34` to use a remote signer ([NIP-46]), known as a
|
||||||
|
bunker, for all cryptographic operations.
|
||||||
|
|
||||||
|
When `n34` communicates with the bunker, it uses a persistent, locally-generated
|
||||||
|
keypair. You should add this keypair's public key to your bunker's list of
|
||||||
|
authorized applications. This allows `n34` to operate securely without needing
|
||||||
|
direct access to your main private key.
|
||||||
|
|
||||||
|
Once configured, actions such as fetching your public key or signing events are
|
||||||
|
delegated to the bunker. To remove the bunker configuration, run the command
|
||||||
|
again without providing a URL.
|
||||||
|
|
||||||
|
[NIP-46]: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||||
27
docs/config/keyring.md
Normal file
27
docs/config/keyring.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Secret Key Keyring
|
||||||
|
|
||||||
|
> `n34 config keyring` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Manages the secret key keyring, including enabling, disabling, or resetting it.
|
||||||
|
|
||||||
|
Usage: n34 config keyring <--enable|--disable|--reset>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--enable Enables the secret key keyring. You will be prompted for your key one last time to store it.
|
||||||
|
--disable Disables the secret key keyring. This removes the stored key and prevents new ones from being saved.
|
||||||
|
--reset Resets the keyring. This deletes the current key, allowing a new one to be stored on the next use.
|
||||||
|
```
|
||||||
|
|
||||||
|
To avoid entering your private key for every command, you can enable the keyring
|
||||||
|
to store it securely. First, run `n34 config keyring --enable`. The next time
|
||||||
|
you run an `n34` command that requires your private key, it will be saved
|
||||||
|
to your system's keyring. You will not need to enter it again for subsequent
|
||||||
|
commands.
|
||||||
|
|
||||||
|
To replace the stored key with a new one, use the `--reset` flag. To stop using
|
||||||
|
the keyring and remove the stored key, use the `--disable` flag.
|
||||||
|
|
||||||
|
`n34` uses your operating system's native secret management system. For example,
|
||||||
|
it uses `keyutils` on Linux and `Keychain` on macOS.
|
||||||
22
docs/config/nip07.md
Normal file
22
docs/config/nip07.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# NIP-07 Browser Signer Proxy
|
||||||
|
|
||||||
|
> `n34 config nip07`
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Manage the NIP-07 browser signer proxy by enabling or disabling it and configuring the `ip:port` address.
|
||||||
|
|
||||||
|
Usage: n34 config nip07 [OPTIONS] <--enable|--disable>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--enable Enable NIP-07 as the default signer
|
||||||
|
--disable Disable NIP-07 as the default signer
|
||||||
|
--addr <ADDR> Set the `ip:port` for the browser signer proxy (default: 127.0.0.1:51034)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use [NIP-07] (Browser Extension Signer) as your default signer. This is achieved
|
||||||
|
by running a proxy at the specified `ADDR`, which defaults to `127.0.0.1:51034`.
|
||||||
|
The proxy forwards `n34` requests to the browser signer and relays the responses
|
||||||
|
back.
|
||||||
|
|
||||||
|
[NIP-07]: https://github.com/nostr-protocol/nips/blob/master/07.md
|
||||||
20
docs/config/pow.md
Normal file
20
docs/config/pow.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Default PoW Difficulty
|
||||||
|
|
||||||
|
> `n34 config pow`
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Sets the default PoW difficulty (0 if not specified)
|
||||||
|
|
||||||
|
Usage: n34 config pow <DIFFICULTY>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<DIFFICULTY> The new default PoW difficulty
|
||||||
|
```
|
||||||
|
|
||||||
|
This command configures the default Proof of Work (PoW) difficulty for newly
|
||||||
|
created events. This setting is applied to most generated events, but it
|
||||||
|
intentionally skips patch events. Because patches can be numerous, calculating
|
||||||
|
PoW for each one would significantly slow down operations.
|
||||||
|
|
||||||
|
If you want to disable the PoW just make it 0.
|
||||||
21
docs/config/relays.md
Normal file
21
docs/config/relays.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Fallback Relays
|
||||||
|
|
||||||
|
> `n34 config relays` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Sets the default fallback relays if none provided. Use this relays for read and write
|
||||||
|
|
||||||
|
Usage: n34 config relays [OPTIONS] [RELAYS]...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[RELAYS]... List of relay URLs to append to fallback relays. If empty, removes all fallback relays
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--override Replace existing fallback relays instead of appending new ones
|
||||||
|
```
|
||||||
|
|
||||||
|
This command configures the default fallback relays, which `n34` uses to read
|
||||||
|
from and write to. To add relays, provide their URLs as arguments to append
|
||||||
|
them to the current list. Use the `--override` flag to replace the existing list
|
||||||
|
entirely. To clear all fallback relays, run the command without any arguments.
|
||||||
482
docs/index.html
Normal file
482
docs/index.html
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
|
||||||
|
<meta property="og:title" content="n34 - CLI for NIP-34 and Nostr Code Collaboration">
|
||||||
|
<meta property="og:description" content="An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.">
|
||||||
|
<meta property="og:url" content="https://n34.dev">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="n34 - CLI for NIP-34 and Nostr Code Collaboration">
|
||||||
|
<meta name="twitter:description" content="An open source CLI for sending and receiving Git issues, patches and comments over the Nostr protocol.">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>n34 - CLI for NIP-34 and Nostr Code Collaboration</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: radial-gradient(125% 125% at 50% 90%, #000000 40%, #072607 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
background: linear-gradient(135deg, #ffffff, #cccccc);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 28px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #0a0a0a;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.section p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #cccccc;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
.feature {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
transition: transform 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.feature:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
.feature h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.feature p {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.feature-list {
|
||||||
|
background: #111;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.feature-column ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.feature-column li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
.feature-check {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.feature-check.completed {
|
||||||
|
background: #22c55e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.feature-check.pending {
|
||||||
|
background: #374151;
|
||||||
|
color: #9ca3af;
|
||||||
|
border: 1px solid #4b5563;
|
||||||
|
}
|
||||||
|
.feature-check.completed::after {
|
||||||
|
content: "✓";
|
||||||
|
}
|
||||||
|
.feature-check.pending::after {
|
||||||
|
content: "○";
|
||||||
|
}
|
||||||
|
.feature-text {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.feature-text.pending {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.install-section {
|
||||||
|
background: #111;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
.install-section h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.code-block {
|
||||||
|
background: #000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid #333;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.code-block p {
|
||||||
|
color: #00ff00;
|
||||||
|
}
|
||||||
|
.code-block p::before {
|
||||||
|
content: "$ ";
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.code-block span::before {
|
||||||
|
content: "$ ";
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.code-block span {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.code-block pre {
|
||||||
|
color: #00ff00;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.links h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.links ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.links li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.links a {
|
||||||
|
color: #cccccc;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
.links a:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.links a:before {
|
||||||
|
content: "→ ";
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #1a4d1a;
|
||||||
|
color: #4ade80;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
margin-top: 80px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
.features {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.feature-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.install-section,
|
||||||
|
.feature-list {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero">
|
||||||
|
<div class="status-badge">✨ Looking for feedback</div>
|
||||||
|
<h1>n34</h1>
|
||||||
|
<p>
|
||||||
|
n34 is an open source command-line interface (CLI) tool for
|
||||||
|
sending and receiving Git issues, patches and comments over the
|
||||||
|
Nostr protocol. It supports creating, replying to, and managing
|
||||||
|
issues and patches, making Git collaboration decentralized and
|
||||||
|
censorship-resistant.
|
||||||
|
</p>
|
||||||
|
<div class="buttons">
|
||||||
|
<a href="https://n34.dev/commands.html" class="btn">Documentation</a>
|
||||||
|
<a href="https://git.4rs.nl/awiteb/n34.git" class="btn">Git Repository</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<h2>Key Features</h2>
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature">
|
||||||
|
<h3><span class="feature-icon">🔄</span>Complete Git Workflow</h3>
|
||||||
|
<p>Handle the full development lifecycle with patches, issues, replies, and status tracking, all through the decentralized Nostr protocol.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3><span class="feature-icon">🔗</span><a href="https://github.com/nostr-protocol/nips/blob/master/34.md" target="_blank" rel="noreferrer">NIP-34</a> Compliant</h3>
|
||||||
|
<p>Fully implements the <a href="https://github.com/nostr-protocol/nips/blob/master/34.md" target="_blank" rel="noreferrer">NIP-34</a> specification for Git repositories on Nostr, ensuring compatibility with the decentralized ecosystem.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3><span class="feature-icon">🛠️</span>Developer Friendly</h3>
|
||||||
|
<p>Intuitive CLI interface designed for developers who want to integrate Git workflows with Nostr seamlessly.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<h3><span class="feature-icon">🔐</span>Self-Sovereign</h3>
|
||||||
|
<p>No accounts, no passwords, no centralized servers. You control your identity and data through cryptographic keys.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="feature-list">
|
||||||
|
<h2>Feature Roadmap</h2>
|
||||||
|
<p style="text-align: center; margin-bottom: 30px; color: #cccccc;">Current implementation status and upcoming features</p>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<div class="feature-column">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Repository announcements</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check pending"></span>
|
||||||
|
<span class="feature-text pending">Repository state announcements</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Patches (Send, fetch and list)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Issues (Send, view and list)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Replies</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Issues and patches status</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check pending"></span>
|
||||||
|
<span class="feature-text pending">Pull requests (<a href="https://github.com/nostr-protocol/nips/pull/1966" target="_blank" rel="noreferrer">nostr-protocol/nips#1966</a>)</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="feature-column">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Gossip Model (<a href="https://github.com/nostr-protocol/nips/blob/master/65.md" target="_blank" rel="noreferrer">NIP-65</a>)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Proof of Work (<a href="https://github.com/nostr-protocol/nips/blob/master/13.md" target="_blank" rel="noreferrer">NIP-13</a>)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">nostr: URI scheme (<a href="https://github.com/nostr-protocol/nips/blob/master/21.md" target="_blank" rel="noreferrer">NIP-21</a>)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Signing using bunker (<a href="https://github.com/nostr-protocol/nips/blob/master/46.md" target="_blank" rel="noreferrer">NIP-46</a>)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text pending">Signing using <a href="https://github.com/nostr-protocol/nips/blob/master/07.md" target="_blank" rel="noreferrer">NIP-07</a> proxy (<a href="https://crates.io/crates/nostr-browser-signer-proxy" target="_blank" rel="noreferrer">nostr-browser-signer-proxy</a>)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">Secret key keyring</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check pending"></span>
|
||||||
|
<span class="feature-text pending">Code Snippets (<a href="https://github.com/nostr-protocol/nips/blob/master/C0.md" target="_blank" rel="noreferrer">NIP-C0</a>)</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="feature-check completed"></span>
|
||||||
|
<span class="feature-text">In device relays and repos bookmark</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="install-section">
|
||||||
|
<h2>Quick Start</h2>
|
||||||
|
<p style="text-align: center; margin-bottom: 20px; color: #cccccc;">Get started with n34 in seconds</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<p>cargo install n34</p>
|
||||||
|
<span># The execautable will be in ~/.cargo/bin/n34</span>
|
||||||
|
</div>
|
||||||
|
<p style="text-align: center; margin: 20px 0; color: #888;">or</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<p>git clone https://git.4rs.nl/awiteb/n34.git</p>
|
||||||
|
<p>cd n34 && cargo build --release</p>
|
||||||
|
<span># The execautable will be in target/release/n34</span>
|
||||||
|
</div>
|
||||||
|
<p style="text-align: center; margin: 20px 0; color: #888;">NixOS (Version 0.4.0 or later)</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<p>git clone https://git.4rs.nl/awiteb/n34.git</p>
|
||||||
|
<p>cd n34 && nix build</p>
|
||||||
|
<span># The execautable will be in result/bin/n34</span>
|
||||||
|
</div>
|
||||||
|
<p style="text-align: center; margin: 20px 0; color: #888;">home-manager</p>
|
||||||
|
<p style="text-align: center; margin-bottom: 20px; color: #cccccc;">Add this to your <code>flake.nix</code> inputs
|
||||||
|
<br>
|
||||||
|
Specify the version you want to install, or remove <code>?ref</code> for the unreleased version. You can also use any mirror; it doesn't have to be <code>git.4rs.nl</code>
|
||||||
|
</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>
|
||||||
|
inputs = {
|
||||||
|
n34.url = "git+https://git.4rs.nl/awiteb/n34.git?ref=refs/tags/vx.y.x";
|
||||||
|
};</pre>
|
||||||
|
</div>
|
||||||
|
<p style="text-align: center; margin-bottom: 20px; color: #cccccc;">And this in your home packages</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>packages = [ inputs.n34.packages."${pkgs.system}".default ];</pre>
|
||||||
|
</div>
|
||||||
|
<p style="text-align: center; margin-top: 20px; color: #cccccc;">Once installed, run <code style="background: #333; padding: 2px 6px; border-radius: 4px;">n34 --help</code> to see available commands.</p>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<h2>Why Nostr?</h2>
|
||||||
|
<p>
|
||||||
|
Nostr is fundamentally different from traditional platforms because it's not an application or service, it's a decentralized protocol. This means any tool or app can integrate with it, enabling open, permissionless collaboration
|
||||||
|
without relying on centralized gatekeepers. Unlike proprietary systems, Nostr doesn't require emails, passwords, or accounts. You interact directly through relays, whether you self-host your own or use public ones, ensuring no
|
||||||
|
single point of failure or control.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
What makes Nostr uniquely resilient is its design, the protocol itself is just a set of rules, not a company or product that can disappear. Your Git issues, patches, and comments persist as long as relays choose to store them,
|
||||||
|
immune to the whims of corporate shutdowns or policy changes. Nostr is infrastructure in its purest form, an idea that outlives any temporary implementation. n34 taps into a future-proof foundation for decentralized collaboration.
|
||||||
|
</p>
|
||||||
|
<div class="links">
|
||||||
|
<h3>More about Nostr</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://nostr.com">nostr.com</a></li>
|
||||||
|
<li><a href="https://nostr.org">nostr.org</a></li>
|
||||||
|
<li><a href="https://nostr.how/en/what-is-nostr">nostr.how/en/what-is-nostr</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Built with ❤️ for the decentralized future</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
docs/issue/README.md
Normal file
15
docs/issue/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Issue Management
|
||||||
|
|
||||||
|
Using `n34`, you can manage Git issues stored in Nostr relays, adhering to
|
||||||
|
the [NIP-34] standard. In Nostr, events are immutable, meaning their IDs are
|
||||||
|
derived from the SHA-256 hash of their timestamp, content, author, and tags.
|
||||||
|
As a result, issues cannot be edited directly. However, with `n34`, you can
|
||||||
|
create new issues, view existing ones, or update their status—such as closing,
|
||||||
|
resolving, or reopening them.
|
||||||
|
|
||||||
|
[NIP-34] introduces support for drafting issues, though this feature is not
|
||||||
|
currently implemented in `n34` due to the lack of a clear use case for drafting
|
||||||
|
issues. The inclusion of this functionality may stem from its shared use in both
|
||||||
|
issues and patches, suggesting it was primarily designed for patch management.
|
||||||
|
|
||||||
|
[NIP-34]: https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||||
19
docs/issue/close.md
Normal file
19
docs/issue/close.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Closes an Open Issue
|
||||||
|
|
||||||
|
> `n34 issue close` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Closes an open issue
|
||||||
|
|
||||||
|
Usage: n34 issue close [OPTIONS] <ISSUE_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<ISSUE_ID> The open issue id to close it
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1632` (Close status) for the specified issue. The issue have to
|
||||||
|
be open.
|
||||||
19
docs/issue/list.md
Normal file
19
docs/issue/list.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# List Repositories Issues
|
||||||
|
|
||||||
|
> `n34 issue list` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
List the repositories issues
|
||||||
|
|
||||||
|
Usage: n34 issue list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[NADDR-NIP05-OR-SET]... Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--limit <LIMIT> Maximum number of issues to list [default: 15]
|
||||||
|
```
|
||||||
|
|
||||||
|
List the repositories issues. By default `n34` will look for `nostr-address`
|
||||||
|
file and extract the repositories from it.
|
||||||
29
docs/issue/new.md
Normal file
29
docs/issue/new.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Create an Issue
|
||||||
|
|
||||||
|
> `n34 issue new` Command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Create a new repository issue
|
||||||
|
|
||||||
|
Usage: n34 issue new [OPTIONS] <--content <CONTENT>|--editor>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
-c, --content <CONTENT> Markdown content for the issue. Cannot be used together with the `--editor` flag
|
||||||
|
-e, --editor Opens the user's default editor to write issue content. The first line will be used as the issue subject
|
||||||
|
--subject <SUBJECT> The issue subject. Cannot be used together with the `--editor` flag
|
||||||
|
-l, --label <LABEL> Labels for the issue. Can be specified as arguments (-l bug) or hashtags in content (#bug)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the `n34 issue new` command to create a new issue in a repository. This
|
||||||
|
command supports the [NIP-21] (`nostr:` URI scheme) and hashtags within the
|
||||||
|
issue content. When you mention public keys in the content, they will be
|
||||||
|
included in the event tags. Additionally, using hashtags like `#bug` in the
|
||||||
|
issue body will automatically apply them as labels.
|
||||||
|
|
||||||
|
You must choose between the `--content` and `--editor` options. With
|
||||||
|
`--content`, you provide the issue content directly in the command. With
|
||||||
|
`--editor`, your default `$EDITOR` will open, allowing you to write the issue
|
||||||
|
content. The first line of the editor's output will be used as the issue
|
||||||
|
subject.
|
||||||
19
docs/issue/reopen.md
Normal file
19
docs/issue/reopen.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Reopen a Closed Issue
|
||||||
|
|
||||||
|
> `n34 issue reopen` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Reopens a closed issue
|
||||||
|
|
||||||
|
Usage: n34 issue reopen [OPTIONS] <ISSUE_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<ISSUE_ID> The ID of the closed issue to reopen
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1630` (Open status) for the specified issue. The issue have to
|
||||||
|
be closed.
|
||||||
18
docs/issue/resolve.md
Normal file
18
docs/issue/resolve.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Resolves an Issue
|
||||||
|
|
||||||
|
> `n34 issue resolve` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Resolves an issue
|
||||||
|
|
||||||
|
Usage: n34 issue resolve [OPTIONS] <ISSUE_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<ISSUE_ID> The issue id to resolve it
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1631` (Resolved status) event for the specified issue.
|
||||||
19
docs/issue/view.md
Normal file
19
docs/issue/view.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# View an Issue By ID
|
||||||
|
|
||||||
|
> `n34 issue view` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
View an issue by its ID
|
||||||
|
|
||||||
|
Usage: n34 issue view [OPTIONS] <ISSUE_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<ISSUE_ID> The issue id to view it
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Simply provide the issue ID in `note` or `nevent` format to retrieve and display
|
||||||
|
the issue details.
|
||||||
21
docs/patch/README.md
Normal file
21
docs/patch/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Patch Management
|
||||||
|
|
||||||
|
In `n34`, patch management is designed to give you complete control. You can
|
||||||
|
manually generate patch files using `git-format-patch` and then broadcast them
|
||||||
|
to Nostr relays. This ensures that you have full authority over the content
|
||||||
|
and structure of your patches, allowing for precise customization as per your
|
||||||
|
requirements.
|
||||||
|
|
||||||
|
Similarly, when fetching patches, `n34` provides them to you without
|
||||||
|
automatically applying, merging, or checking them. This empowers you to review
|
||||||
|
the patches at your own pace and decide whether to merge or apply them as
|
||||||
|
needed. You retain full control over the entire process, ensuring a tailored
|
||||||
|
approach to patch management.
|
||||||
|
|
||||||
|
## Patch Status Management
|
||||||
|
|
||||||
|
You can assign a status to original patches, but revision patches do not have
|
||||||
|
a specific status assigned to them. Instead, they inherit the status of the
|
||||||
|
original patch. However, if the original patch is marked as `Applied/Merged`,
|
||||||
|
the revision patch must be explicitly tagged to claim the same status. If not
|
||||||
|
tagged, the revision patch status will be `Closed`.
|
||||||
31
docs/patch/apply.md
Normal file
31
docs/patch/apply.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Apply an Open Patch
|
||||||
|
|
||||||
|
> `n34 patch apply` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Set an open patch status to applied
|
||||||
|
|
||||||
|
Usage: n34 patch apply [OPTIONS] <PATCH_ID> [APPLIED_COMMITS]...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PATCH_ID> The open patch id to apply it. Must be orignal root patch or revision root
|
||||||
|
[APPLIED_COMMITS]... The applied commits
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
--patches <PATCH-EVENT-ID> Patches that have been applied. Use this when only some patches have been applied, not all
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a kind `1631` event (Applied/Merged status) for the specified patch. The
|
||||||
|
patch must be in open status.
|
||||||
|
|
||||||
|
You can specify either an original patch or revision patch ID, but the status
|
||||||
|
event will only reference the original patch. Revision patches will be mentioned
|
||||||
|
in the event.
|
||||||
|
|
||||||
|
The `APPLIED_COMMITS` field serves to inform clients about the status of
|
||||||
|
specific commits, whether they have been applied or not. If you need to retrieve
|
||||||
|
the list of commits from a specific point (such as the tip of the master branch)
|
||||||
|
up to the `HEAD`, you can use the following Git command: `git log --pretty=%H
|
||||||
|
'origin/master..HEAD'`.
|
||||||
20
docs/patch/close.md
Normal file
20
docs/patch/close.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Closes an Open or Drafted Patch
|
||||||
|
|
||||||
|
> `n34 patch close` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Closes an open or drafted patch
|
||||||
|
|
||||||
|
Usage: n34 patch close [OPTIONS] <PATCH_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PATCH_ID> The open/drafted patch id to close it. Must be orignal root patch
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1632` (Close status) for the specified patch. The patch have to
|
||||||
|
be open or drafted.
|
||||||
|
|
||||||
19
docs/patch/draft.md
Normal file
19
docs/patch/draft.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Draft an Open Patch
|
||||||
|
|
||||||
|
> `n34 patch draft` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Converts an open patch to draft state
|
||||||
|
|
||||||
|
Usage: n34 patch draft [OPTIONS] <PATCH_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PATCH_ID> The open patch id to draft it. Must be orignal root patch
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1633` (Draft status) for the specified patch. The patch have to
|
||||||
|
be open.
|
||||||
21
docs/patch/fetch.md
Normal file
21
docs/patch/fetch.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Fetch a Patch By ID
|
||||||
|
|
||||||
|
> `n34 patch fetch` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Fetches a patch by its id
|
||||||
|
|
||||||
|
Usage: n34 patch fetch [OPTIONS] <PATCH_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PATCH_ID> The patch id to fetch it
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
-o, --output <PATH> Output directory for the patches. Default to the current directory
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches patches using their original patch ID. All fetched patches will be saved
|
||||||
|
to the specified output directory (current directory by default). You can then
|
||||||
|
apply or merge these patches into your branch as needed.
|
||||||
20
docs/patch/list.md
Normal file
20
docs/patch/list.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# List Repositories Patches
|
||||||
|
|
||||||
|
> `n34 patch list` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
List the repositories patches
|
||||||
|
|
||||||
|
Usage: n34 patch list [OPTIONS] [NADDR-NIP05-OR-SET]...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[NADDR-NIP05-OR-SET]... Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--limit <LIMIT> Maximum number of patches to list [default: 15]
|
||||||
|
```
|
||||||
|
|
||||||
|
List the repositories patches. By default `n34` will look for `nostr-address`
|
||||||
|
file and extract the repositories from it.
|
||||||
|
|
||||||
29
docs/patch/merge.md
Normal file
29
docs/patch/merge.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Merge an Open Patch
|
||||||
|
|
||||||
|
> `n34 patch merge` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Set an open patch status to merged
|
||||||
|
|
||||||
|
Usage: n34 patch merge [OPTIONS] <PATCH_ID> <MERGE_COMMIT>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PATCH_ID> The open patch id to merge it. Must be orignal root patch or revision root
|
||||||
|
<MERGE_COMMIT> The merge commit id
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
--patches <PATCH-EVENT-ID> Patches that have been merged. Use this when only some patches have been merged, not all
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a kind `1631` event (Applied/Merged status) for the specified patch. The
|
||||||
|
patch must be in open status.
|
||||||
|
|
||||||
|
You can specify either an original patch or revision patch ID, but the status
|
||||||
|
event will only reference the original patch. Revision patches will be mentioned
|
||||||
|
in the event.
|
||||||
|
|
||||||
|
You can get the `MERGE_COMMIT` commit using `git rev-parse HEAD` command if
|
||||||
|
the merge commit in the `HEAD` or use `HEAD~n` where the `n` is the number of
|
||||||
|
commits the merge commit before the HEAD
|
||||||
20
docs/patch/reopen.md
Normal file
20
docs/patch/reopen.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Reopens a Closed or Drafted Patch
|
||||||
|
|
||||||
|
> `n34 patch reopen` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Reopens a closed or drafted patch
|
||||||
|
|
||||||
|
Usage: n34 patch reopen [OPTIONS] <PATCH_ID>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PATCH_ID> The closed/drafted patch id to reopen it. Must be orignal root patch
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Issue a kind `1632` (Close status) for the specified patch. The patch have to
|
||||||
|
be closed or drafted.
|
||||||
|
|
||||||
22
docs/patch/send.md
Normal file
22
docs/patch/send.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Send Patches to a Repository
|
||||||
|
|
||||||
|
> `n34 patch send` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Send one or more patches to a repository
|
||||||
|
|
||||||
|
Usage: n34 patch send [OPTIONS] <PATCH-PATH>...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<PATCH-PATH>... List of patch files to send (space separated)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
--original-patch <EVENT-ID> Original patch ID if this is a revision of it
|
||||||
|
```
|
||||||
|
|
||||||
|
Send your generated patches to the repositories specified using the `--repo`
|
||||||
|
option or retrieved from the `nostr-address` file. When submitting a revision
|
||||||
|
of an existing patch, include the original patch ID to ensure it’s correctly
|
||||||
|
referenced in your revision patch event.
|
||||||
28
docs/reply.md
Normal file
28
docs/reply.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Reply to Issues and Patches
|
||||||
|
|
||||||
|
> `n34 reply` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Reply to issues and patches
|
||||||
|
|
||||||
|
Usage: n34 reply [OPTIONS] <--comment <COMMENT>|--editor> <nevent1-or-note1>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<nevent1-or-note1> The issue, patch, or comment to reply to
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--quote-to Quote the replied-to event in the editor
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
-c, --comment <COMMENT> The comment (cannot be used with --editor)
|
||||||
|
-e, --editor Open editor to write comment (cannot be used with --content)
|
||||||
|
```
|
||||||
|
|
||||||
|
Craft replies ([NIP-22] Comment) to issues, patches, or comments with ease
|
||||||
|
using the `n34 reply` command. You can either input your reply directly with
|
||||||
|
the `--comment` option or open an editor for a more detailed response using
|
||||||
|
`--editor`. Additionally, when using `--editor`, the `--quote-to` option
|
||||||
|
allows you to include the original content in your editor, enabling precise and
|
||||||
|
context-aware replies.
|
||||||
|
|
||||||
|
[NIP-22]: https://github.com/nostr-protocol/nips/blob/master/22.md
|
||||||
4
docs/repo/README.md
Normal file
4
docs/repo/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Manage Repositories
|
||||||
|
|
||||||
|
In `n34` you can manage your repositories. This includes announcing new ones,
|
||||||
|
viewing existing ones, and announcing state updates (coming soon).
|
||||||
32
docs/repo/announce.md
Normal file
32
docs/repo/announce.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Broadcast and Update a Git Repository
|
||||||
|
|
||||||
|
> `n34 repo announce` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Broadcast and update a git repository
|
||||||
|
|
||||||
|
Usage: n34 repo announce [OPTIONS] --id <REPO_ID>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--id <REPO_ID> Unique identifier for the repository in kebab-case
|
||||||
|
-n, --name <NAME> A name for the repository
|
||||||
|
-d, --description <DESCRIPTION> A description for the repository
|
||||||
|
-w, --web <WEB> Webpage URLs for the repository (if provided by the git server)
|
||||||
|
-c, --clone <CLONE> URLs for cloning the repository
|
||||||
|
-m, --maintainers <MAINTAINERS> Additional maintainers of the repository (besides yourself)
|
||||||
|
-l, --label <LABEL> Labels to categorize the repository. Can be specified multiple times
|
||||||
|
--force-id Skip kebab-case validation for the repository ID
|
||||||
|
--address-file If set, creates a `nostr-address` file to enable automatic address discovery by n34
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates an announcement event to publish your project. It can be
|
||||||
|
used to announce a new repository or update an existing one.
|
||||||
|
|
||||||
|
When updating, you must resubmit all repository fields, not just the fields
|
||||||
|
you wish to change. The command uses this information to build and publish a
|
||||||
|
completely new announcement event that will replace the old one.
|
||||||
|
|
||||||
|
It is recommended to use the `--address-file` flag. This option creates
|
||||||
|
a `nostr-address` file that enables `n34` to automatically discover the
|
||||||
|
repository's address, simplifying the workflow for contributors.
|
||||||
20
docs/repo/view.md
Normal file
20
docs/repo/view.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# View Git Repository Details
|
||||||
|
|
||||||
|
> `n34 repo view` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
View details of a nostr git repository
|
||||||
|
|
||||||
|
Usage: n34 repo view [NADDR-NIP05-OR-SET]...
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[NADDR-NIP05-OR-SET]... Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
This command prints repository details to standard output. If no arguments
|
||||||
|
are provided, it looks for a `nostr-address` file in the current directory
|
||||||
|
and displays the details for the address specified within it. See [passing
|
||||||
|
repositories] for details on accepted formats.
|
||||||
|
|
||||||
|
[passing repositories]: /commands.html#passing-repositories
|
||||||
11
docs/sets/README.md
Normal file
11
docs/sets/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Managing Repository and Relay Sets
|
||||||
|
|
||||||
|
Sets are a convenience feature for contributing to projects that do not have a
|
||||||
|
`nostr-address` file. Instead of manually specifying the project's repositories
|
||||||
|
and relays for every command, you can define them once as a named "set". You can
|
||||||
|
then reference this set by its name in commands. This allows you to use the set
|
||||||
|
as a shortcut for a list of relays (`--relays <set_name>`) or as the project's
|
||||||
|
address in commands like `issue` and `patch`.
|
||||||
|
|
||||||
|
Sets are defined in your configuration file. To use a specific configuration
|
||||||
|
file, pass its path using the `--config` option.
|
||||||
25
docs/sets/new.md
Normal file
25
docs/sets/new.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Create a Set
|
||||||
|
|
||||||
|
> `n34 sets new` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Create a new set
|
||||||
|
|
||||||
|
Usage: n34 sets new [OPTIONS] <NAME>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<NAME> Unique name for the set
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--set-relay <RELAYS> Optional relay to add it to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Each set requires a unique name, provided as the final argument to the command.
|
||||||
|
Use the `--set-relays`/`--sr` option to specify the relays for the new set;
|
||||||
|
this can be a relay URL or the name of an existing set whose relays you wish to
|
||||||
|
use. To add repositories, use the `--repo` option. Check [passing repositories]
|
||||||
|
format.
|
||||||
|
|
||||||
|
[passing repositories]: /commands.html#passing-repositories
|
||||||
24
docs/sets/remove.md
Normal file
24
docs/sets/remove.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Remove a Set
|
||||||
|
|
||||||
|
> `n34 sets remove` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Remove a set, or specific repos and relays within it
|
||||||
|
|
||||||
|
Usage: n34 sets remove [OPTIONS] <NAME>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<NAME> Set name to delete
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--set-relay <RELAYS> Specific relay to remove it from the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
```
|
||||||
|
|
||||||
|
Removes an entire set, or specific repositories and relays from it.
|
||||||
|
Without options, this command deletes the entire set.
|
||||||
|
|
||||||
|
See the [passing repositories] section for more details on supported formats.
|
||||||
|
|
||||||
|
[passing repositories]: /commands.html#passing-repositories
|
||||||
17
docs/sets/show.md
Normal file
17
docs/sets/show.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Show a Set
|
||||||
|
|
||||||
|
> `n34 sets show` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Show a single set or all the stored sets
|
||||||
|
|
||||||
|
Usage: n34 sets show [NAME]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
[NAME] Name of the set to display. If not provided, lists all available sets
|
||||||
|
```
|
||||||
|
|
||||||
|
Prints all set names to standard output, each followed by its associated repos
|
||||||
|
and relays. To view the details for a specific set, provide its name as an
|
||||||
|
argument.
|
||||||
25
docs/sets/update.md
Normal file
25
docs/sets/update.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Modify a Set
|
||||||
|
|
||||||
|
> `n34 sets update` command
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```
|
||||||
|
Modify an existing set
|
||||||
|
|
||||||
|
Usage: n34 sets update [OPTIONS] <NAME>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<NAME> Name of the set to update
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--set-relay <RELAYS> Add relay to the set, either as URL or set name to extract its relays. [aliases: `--sr`]
|
||||||
|
--repo <NADDR-NIP05-OR-SET> Repository address in `naddr` format (`naddr1...`), NIP-05 format (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`
|
||||||
|
--override Replace existing relays/repositories instead of adding to them
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this command to update an existing set by its name. By default, providing
|
||||||
|
relays via `--set-relay` or repositories via `--repo` will add them to the set's
|
||||||
|
existing entries. To replace the current relays and repositories with the new
|
||||||
|
values, use the `--override` flag.
|
||||||
|
|
||||||
|
[passing repositories]: /commands.html#passing-repositories
|
||||||
12
flake.lock
generated
12
flake.lock
generated
@@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751271578,
|
"lastModified": 1754498491,
|
||||||
"narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=",
|
"narHash": "sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df",
|
"rev": "c2ae88e026f9525daf89587f3cbee584b92b6134",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -62,11 +62,11 @@
|
|||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751683029,
|
"lastModified": 1754575663,
|
||||||
"narHash": "sha256-dYO5X5jK8bpQOeRAo8R5aUt6M/+Ji1cZgstZI7SQ2IA=",
|
"narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "9e5e62a33a929a67a5427fb7324a6f583dced0b2",
|
"rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
31
flake.nix
31
flake.nix
@@ -23,18 +23,45 @@
|
|||||||
devShells.default = mkShell {
|
devShells.default = mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
cargo-msrv
|
cargo-msrv
|
||||||
|
dbus
|
||||||
git-cliff
|
git-cliff
|
||||||
just
|
just
|
||||||
|
mdbook
|
||||||
|
nushell
|
||||||
|
pkg-config
|
||||||
taplo
|
taplo
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
(lib.hiPrio rust-bin.nightly."2025-07-05".rustfmt)
|
(lib.hiPrio rust-bin.nightly."2025-08-07".rustfmt)
|
||||||
rust-bin.stable.latest.default
|
rust-bin.stable.latest.default
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
buildInputs = [ ];
|
packages.default =
|
||||||
|
let
|
||||||
|
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||||
|
in
|
||||||
|
with pkgs;
|
||||||
|
rustPlatform.buildRustPackage {
|
||||||
|
pname = manifest.name;
|
||||||
|
version = manifest.version;
|
||||||
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
src = lib.cleanSource ./.;
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkg-config
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
dbus
|
||||||
|
];
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
inherit (manifest) description homepage;
|
||||||
|
license = lib.licenses.gpl3Plus;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
67
justfile
67
justfile
@@ -4,15 +4,20 @@
|
|||||||
# - Linux distribution
|
# - Linux distribution
|
||||||
# - just (Of course) <https://github.com/casey/just>
|
# - just (Of course) <https://github.com/casey/just>
|
||||||
# - cargo (For the build and tests) <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
# - cargo (For the build and tests) <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
||||||
|
# - mdbook (<https://rust-lang.github.io/mdBook>)
|
||||||
|
# - git-cliff (<https://git-cliff.org>)
|
||||||
|
# - taplo (<https://taplo.tamasfe.dev/>)
|
||||||
|
# - cargo-msrv (<https://github.com/foresterre/cargo-msrv>)
|
||||||
|
# - nushell (<https://nushell.sh>)
|
||||||
|
|
||||||
set quiet
|
set quiet
|
||||||
set unstable
|
set unstable
|
||||||
set shell := ["/usr/bin/env", "bash", "-c"]
|
set shell := ["/usr/bin/env", "bash", "-c"]
|
||||||
set script-interpreter := ["/usr/bin/env", "bash"]
|
set script-interpreter := ["/usr/bin/env", "nu"]
|
||||||
|
|
||||||
JUST_EXECUTABLE := "just -u -f " + justfile()
|
JUST_EXECUTABLE := "just -u -f " + justfile()
|
||||||
header := "Available tasks:\n"
|
header := "Available tasks:\n"
|
||||||
# Get the MSRV from the Cargo.toml
|
BOOK_DEST_DIR := "dest"
|
||||||
tag_change_body := '''{% for group, commits in commits | group_by(attribute="group") %}
|
tag_change_body := '''{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
|
||||||
{{ group | upper_first }}
|
{{ group | upper_first }}
|
||||||
@@ -32,7 +37,7 @@ ci: && msrv _done_ci
|
|||||||
cargo build -q
|
cargo build -q
|
||||||
echo "🔍 Checking code formatting..."
|
echo "🔍 Checking code formatting..."
|
||||||
cargo fmt -q -- --check
|
cargo fmt -q -- --check
|
||||||
RUST_LOG=none taplo fmt --check --config ./.taplo.toml || (echo "❌Toml files is not properly formatted" && exit 1)
|
RUST_LOG=none taplo fmt --check --config "./.taplo.toml" || (echo "❌Toml files is not properly formatted" && exit 1)
|
||||||
echo "🧹 Running linter checks..."
|
echo "🧹 Running linter checks..."
|
||||||
cargo clippy -q -- -D warnings
|
cargo clippy -q -- -D warnings
|
||||||
echo "🧪 Running tests..."
|
echo "🧪 Running tests..."
|
||||||
@@ -50,28 +55,50 @@ _done_ci:
|
|||||||
# Update the changelog
|
# Update the changelog
|
||||||
[script]
|
[script]
|
||||||
changelog:
|
changelog:
|
||||||
OLD_HASH=$(sha256sum CHANGELOG.md | head -c 64)
|
def get_hash [] { open "./CHANGELOG.md" | hash sha256 }
|
||||||
git-cliff > CHANGELOG.md
|
|
||||||
NEW_HASH=$(sha256sum CHANGELOG.md | head -c 64)
|
let old_hash = get_hash
|
||||||
if [[ $OLD_HASH != $NEW_HASH ]]; then
|
git-cliff out> "CHANGELOG.md"
|
||||||
git add CHANGELOG.md
|
|
||||||
git commit -m 'chore(changelog): Update the changelog'
|
if old_hash != get_hash {
|
||||||
echo 'The changes have been added to the changelog file and committed'
|
git add "CHANGELOG.md"
|
||||||
else
|
git commit -m "chore(changelog): Update the changelog"
|
||||||
echo 'No changes have been added to the changelog'
|
print "The changes have been added to the changelog file and committed"
|
||||||
fi
|
} else {
|
||||||
|
print "No changes have been added to the changelog"
|
||||||
|
}
|
||||||
|
|
||||||
# Releases a new version of n34. Requires a clean file tree with no uncommitted changes.
|
# Releases a new version of n34. Requires a clean file tree with no uncommitted changes.
|
||||||
[script]
|
[script]
|
||||||
release version:
|
release version:
|
||||||
set -e
|
let tag_msg = "Version {{ version }}" + (git-cliff --strip all --unreleased --body '{{ tag_change_body }}')
|
||||||
TAG_MSG="Version {{ version }}$(git-cliff --strip all --unreleased --body '{{ tag_change_body }}')"
|
mut cargo_file = open "Cargo.toml"
|
||||||
sed -i "s/^version\s*= \".*\"/version = \"{{ version }}\"/" ./Cargo.toml
|
|
||||||
taplo fmt --config ./.taplo.toml ./Cargo.toml
|
$cargo_file.package.version = "{{ version }}"
|
||||||
|
$cargo_file | save -f "Cargo.toml"
|
||||||
|
|
||||||
|
RUST_LOG=none taplo fmt --config "./.taplo.toml"
|
||||||
{{ JUST_EXECUTABLE }} ci
|
{{ JUST_EXECUTABLE }} ci
|
||||||
git-cliff -t "v{{ version }}" > CHANGELOG.md
|
git-cliff -t "v{{ version }}" out> "./CHANGELOG.md"
|
||||||
git add .
|
git add .
|
||||||
git commit -m 'chore: Bump the version to `v{{ version }}`'
|
git commit -m "chore: Bump the version to `v{{ version }}`"
|
||||||
git tag -s -m "$TAG_MSG" "v{{ version }}"
|
git tag -s -m $tag_msg "v{{ version }}"
|
||||||
git push origin master --tags
|
git push origin master --tags
|
||||||
cargo publish
|
cargo publish
|
||||||
|
|
||||||
|
# Deploy the book to Github Pages
|
||||||
|
deploy:
|
||||||
|
mdbook build --dest-dir {{ BOOK_DEST_DIR }}
|
||||||
|
cd {{ BOOK_DEST_DIR }}
|
||||||
|
git init .
|
||||||
|
git checkout -B gh-pages
|
||||||
|
touch ".nojekyll"
|
||||||
|
echo "n34.dev" > "CNAME"
|
||||||
|
|
||||||
|
git add .
|
||||||
|
git commit -m "Deploy the book to github pages"
|
||||||
|
git remote add origin "git@github.com:TheAwiteb/n34-book"
|
||||||
|
git push origin gh-pages -f
|
||||||
|
cd ..
|
||||||
|
rm -fr {{ BOOK_DEST_DIR }}
|
||||||
|
|
||||||
|
|||||||
46
src/cli/commands/config/bunker.rs
Normal file
46
src/cli/commands/config/bunker.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// 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::nip46::NostrConnectURI;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{CliOptions, traits::CommandRunner},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct BunkerArgs {
|
||||||
|
/// Nostr Connect URL for the bunker. Omit this to remove the current bunker
|
||||||
|
/// URL.
|
||||||
|
bunker_url: Option<NostrConnectURI>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl CommandRunner for BunkerArgs {
|
||||||
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
|
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||||
|
if let Some(ref bunker_url) = self.bunker_url
|
||||||
|
&& !bunker_url.is_bunker()
|
||||||
|
{
|
||||||
|
return Err(N34Error::NotBunkerUrl);
|
||||||
|
}
|
||||||
|
options.config.bunker_url = self.bunker_url;
|
||||||
|
options.config.dump()
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/cli/commands/config/keyring.rs
Normal file
63
src/cli/commands/config/keyring.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// 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::{ArgGroup, Args};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{Cli, CliOptions, traits::CommandRunner},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[clap(
|
||||||
|
group(
|
||||||
|
ArgGroup::new("options")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct KeyringArgs {
|
||||||
|
/// Turns on secret key keyring. Requires entering the key once when
|
||||||
|
/// enabled.
|
||||||
|
#[arg(long, group = "options")]
|
||||||
|
enable: bool,
|
||||||
|
/// Turns off secret key keyring. Removes any existing key and prevents
|
||||||
|
/// storing new ones.
|
||||||
|
#[arg(long, group = "options")]
|
||||||
|
disable: bool,
|
||||||
|
/// Deletes current key and stores the next provided key.
|
||||||
|
#[arg(long, group = "options")]
|
||||||
|
reset: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for KeyringArgs {
|
||||||
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
|
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||||
|
let keyring = nostr_keyring::NostrKeyring::new(Cli::N34_KEYRING_SERVICE_NAME);
|
||||||
|
|
||||||
|
if self.enable {
|
||||||
|
options.config.keyring_secret_key = true;
|
||||||
|
} else if self.disable {
|
||||||
|
options.config.keyring_secret_key = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.reset || self.disable {
|
||||||
|
let _ = keyring.delete(Cli::USER_KEY_PAIR_ENTRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.config.dump()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,12 @@
|
|||||||
// 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>.
|
||||||
|
|
||||||
|
/// `config bunker` subcommand
|
||||||
|
mod bunker;
|
||||||
|
/// `config keyring` subcommand
|
||||||
|
mod keyring;
|
||||||
|
/// `config nip07` subcommand
|
||||||
|
mod nip07;
|
||||||
/// `config pow` subcommand
|
/// `config pow` subcommand
|
||||||
mod pow;
|
mod pow;
|
||||||
/// `config relays` subcommand
|
/// `config relays` subcommand
|
||||||
@@ -21,6 +27,9 @@ mod relays;
|
|||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use self::bunker::BunkerArgs;
|
||||||
|
use self::keyring::KeyringArgs;
|
||||||
|
use self::nip07::Nip07Args;
|
||||||
use self::pow::PowArgs;
|
use self::pow::PowArgs;
|
||||||
use self::relays::RelaysArgs;
|
use self::relays::RelaysArgs;
|
||||||
use super::CliOptions;
|
use super::CliOptions;
|
||||||
@@ -34,10 +43,18 @@ pub enum ConfigSubcommands {
|
|||||||
/// Sets the default fallback relays if none provided. Use this relays for
|
/// Sets the default fallback relays if none provided. Use this relays for
|
||||||
/// read and write.
|
/// read and write.
|
||||||
Relays(RelaysArgs),
|
Relays(RelaysArgs),
|
||||||
|
/// Sets a URL of NIP-46 bunker server used for signing events.
|
||||||
|
Bunker(BunkerArgs),
|
||||||
|
/// Managing the secret key keyring, including enabling, disabling, or
|
||||||
|
/// resetting it.
|
||||||
|
Keyring(KeyringArgs),
|
||||||
|
/// Controls the NIP-07 browser signer proxy, turning it on or off, and
|
||||||
|
/// configures the `ip:port` address.
|
||||||
|
Nip07(Nip07Args),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandRunner for ConfigSubcommands {
|
impl CommandRunner for ConfigSubcommands {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
crate::run_command!(self, options, & Pow Relays)
|
crate::run_command!(self, options, & Pow Relays Bunker Keyring Nip07)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/cli/commands/config/nip07.rs
Normal file
58
src/cli/commands/config/nip07.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// 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::net::SocketAddr;
|
||||||
|
|
||||||
|
use clap::{ArgGroup, Args};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{CliOptions, options_state::DEFAULT_NIP07_PROXY_ADDR, traits::CommandRunner},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[clap(
|
||||||
|
group(
|
||||||
|
ArgGroup::new("options")
|
||||||
|
.required(true)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct Nip07Args {
|
||||||
|
/// Enable NIP-07 as the default signer.
|
||||||
|
#[arg(long, group = "options")]
|
||||||
|
enable: bool,
|
||||||
|
/// Disable NIP-07 as the default signer.
|
||||||
|
#[arg(long, group = "options", group = "disable_options")]
|
||||||
|
disable: bool,
|
||||||
|
/// Set the default `ip:port` for the browser signer proxy.
|
||||||
|
#[arg(long, group = "disable_options")]
|
||||||
|
addr: Option<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for Nip07Args {
|
||||||
|
const NEED_SIGNER: bool = false;
|
||||||
|
|
||||||
|
async fn run(self, mut options: CliOptions) -> N34Result<()> {
|
||||||
|
if self.enable {
|
||||||
|
let addr = self.addr.unwrap_or(DEFAULT_NIP07_PROXY_ADDR);
|
||||||
|
options.config.nip07 = Some(addr)
|
||||||
|
} else {
|
||||||
|
options.config.nip07 = None
|
||||||
|
}
|
||||||
|
|
||||||
|
options.config.dump()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ pub enum IssueSubcommands {
|
|||||||
Reopen(ReopenArgs),
|
Reopen(ReopenArgs),
|
||||||
/// Closes an open issue.
|
/// Closes an open issue.
|
||||||
Close(CloseArgs),
|
Close(CloseArgs),
|
||||||
/// Resolves an open issue.
|
/// Resolves an issue.
|
||||||
Resolve(ResolveArgs),
|
Resolve(ResolveArgs),
|
||||||
/// List the repositories issues.
|
/// List the repositories issues.
|
||||||
List(ListArgs),
|
List(ListArgs),
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
CommandRunner,
|
CommandRunner,
|
||||||
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::NaddrOrSet,
|
||||||
},
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{
|
nostr_utils::{
|
||||||
@@ -101,7 +102,7 @@ impl CommandRunner for NewArgs {
|
|||||||
)?)?;
|
)?)?;
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = options.pubkey().await?;
|
let user_pubk = client.pubkey().await?;
|
||||||
let coordinates = naddrs.clone().into_coordinates();
|
let coordinates = naddrs.clone().into_coordinates();
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
let repos = client.fetch_repos(coordinates.as_slice()).await?;
|
let repos = client.fetch_repos(coordinates.as_slice()).await?;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub struct ResolveArgs {
|
|||||||
/// If omitted, looks for a `nostr-address` file.
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
naddrs: Option<Vec<NaddrOrSet>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// The open issue id to resolve it
|
/// The issue id to resolve it
|
||||||
issue_id: NostrEvent,
|
issue_id: NostrEvent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ use nostr::{event::Kind, filter::Filter};
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
types::{NaddrOrSet, NostrEvent, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
nostr_utils::{
|
nostr_utils::{
|
||||||
|
|||||||
@@ -28,9 +28,14 @@ pub mod repo;
|
|||||||
pub mod sets;
|
pub mod sets;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::{ArgGroup, Args, Parser};
|
use clap::{Args, Parser};
|
||||||
use nostr::key::{Keys, PublicKey, SecretKey};
|
use nostr::key::{Keys, SecretKey};
|
||||||
|
use nostr::nips::nip46::NostrConnectURI;
|
||||||
|
use nostr::signer::{IntoNostrSigner, NostrSigner};
|
||||||
|
use nostr_connect::client::NostrConnect;
|
||||||
|
|
||||||
use self::config::ConfigSubcommands;
|
use self::config::ConfigSubcommands;
|
||||||
use self::issue::IssueSubcommands;
|
use self::issue::IssueSubcommands;
|
||||||
@@ -39,8 +44,11 @@ use self::reply::ReplyArgs;
|
|||||||
use self::repo::RepoSubcommands;
|
use self::repo::RepoSubcommands;
|
||||||
use self::sets::SetsSubcommands;
|
use self::sets::SetsSubcommands;
|
||||||
use super::CliConfig;
|
use super::CliConfig;
|
||||||
|
use super::options_state::OptionsState;
|
||||||
use super::types::RelayOrSet;
|
use super::types::RelayOrSet;
|
||||||
use super::{parsers, traits::CommandRunner};
|
use super::{parsers, traits::CommandRunner};
|
||||||
|
use crate::cli::Cli;
|
||||||
|
use crate::cli::types::EchoAuthUrl;
|
||||||
use crate::error::{N34Error, N34Result};
|
use crate::error::{N34Error, N34Result};
|
||||||
|
|
||||||
/// Default path used when no path is provided via command line arguments.
|
/// Default path used when no path is provided via command line arguments.
|
||||||
@@ -48,19 +56,22 @@ use crate::error::{N34Error, N34Result};
|
|||||||
/// This is a workaround since Clap doesn't support lazy evaluation of defaults.
|
/// 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";
|
pub const DEFAULT_FALLBACK_PATH: &str = "I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_LAZY_DEFAULT";
|
||||||
|
|
||||||
|
/// How long to wait for bunker response (3 minutes).
|
||||||
|
const BUNKER_TIMEOUT: Duration = Duration::from_secs(60 * 3);
|
||||||
|
|
||||||
/// The command-line interface options
|
/// The command-line interface options
|
||||||
#[derive(Args, Clone)]
|
#[derive(Args)]
|
||||||
#[clap(
|
|
||||||
group(
|
|
||||||
ArgGroup::new("signer")
|
|
||||||
.args(&["secret_key"])
|
|
||||||
.required(false)
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
pub struct CliOptions {
|
pub struct CliOptions {
|
||||||
/// Your Nostr secret key
|
/// Your Nostr secret key
|
||||||
#[arg(short, long)]
|
#[arg(short, long, group = "signer")]
|
||||||
pub secret_key: Option<SecretKey>,
|
pub secret_key: Option<SecretKey>,
|
||||||
|
/// NIP-46 bunker url used for signing events
|
||||||
|
#[arg(short, long, group = "signer", value_parser = parsers::parse_bunker_url)]
|
||||||
|
pub bunker_url: Option<NostrConnectURI>,
|
||||||
|
/// Enables signing events using the browser's NIP-07 extension. Listens on
|
||||||
|
/// `127.0.0.1:51034`.
|
||||||
|
#[arg(short = '7', long, group = "signer")]
|
||||||
|
pub nip07: bool,
|
||||||
/// Fallbacks relay to write and read from it. Multiple relays can be
|
/// Fallbacks relay to write and read from it. Multiple relays can be
|
||||||
/// passed.
|
/// passed.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
@@ -73,6 +84,10 @@ pub struct CliOptions {
|
|||||||
hide_default_value = true, value_parser = parsers::parse_config_path
|
hide_default_value = true, value_parser = parsers::parse_config_path
|
||||||
)]
|
)]
|
||||||
pub config: CliConfig,
|
pub config: CliConfig,
|
||||||
|
/// The state of options. Some values that are used by them but should not
|
||||||
|
/// be entered via the CLI
|
||||||
|
#[arg(skip)]
|
||||||
|
pub state: OptionsState,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// N34 commands
|
/// N34 commands
|
||||||
@@ -109,24 +124,42 @@ pub enum Commands {
|
|||||||
|
|
||||||
|
|
||||||
impl CliOptions {
|
impl CliOptions {
|
||||||
/// Gets the public key of the user.
|
/// Returns the signer
|
||||||
pub async fn pubkey(&self) -> N34Result<PublicKey> {
|
pub async fn signer(&self) -> N34Result<Option<Arc<dyn NostrSigner + 'static>>> {
|
||||||
if let Some(sk) = &self.secret_key {
|
if self.nip07 {
|
||||||
return Ok(Keys::new(sk.clone()).public_key());
|
self.state.browser_signer_proxy.start().await?;
|
||||||
}
|
|
||||||
unreachable!(
|
println!(
|
||||||
"This method should only be called when a signer is required. If this panic occurs, \
|
"Browser signer proxy started at: {}",
|
||||||
it indicates a bug where the command failed to properly require a signer (which is \
|
self.state.browser_signer_proxy.url()
|
||||||
the default behavior)"
|
);
|
||||||
)
|
|
||||||
|
// FIXME: Use `BrowserSignerProxy::is_session_active` after it release
|
||||||
|
// nostr@0.44.0
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
|
|
||||||
|
return Ok(Some(
|
||||||
|
self.state.browser_signer_proxy.clone().into_nostr_signer(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the signer
|
|
||||||
pub fn signer(&self) -> Option<impl nostr::signer::IntoNostrSigner> {
|
|
||||||
if let Some(sk) = &self.secret_key {
|
if let Some(sk) = &self.secret_key {
|
||||||
return Some(Keys::new(sk.clone()));
|
return Ok(Some(Keys::new(sk.clone()).into_nostr_signer()));
|
||||||
}
|
}
|
||||||
None
|
|
||||||
|
if let Some(ref bunker_url) = self.bunker_url {
|
||||||
|
let mut nostrconnect = NostrConnect::new(
|
||||||
|
bunker_url.clone(),
|
||||||
|
Cli::n34_keypair()?,
|
||||||
|
BUNKER_TIMEOUT,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("It's a bunker url and not a client");
|
||||||
|
|
||||||
|
nostrconnect.auth_url_handler(EchoAuthUrl);
|
||||||
|
return Ok(Some(nostrconnect.into_nostr_signer()));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an error if there are no relays.
|
/// Returns an error if there are no relays.
|
||||||
@@ -139,11 +172,11 @@ impl CliOptions {
|
|||||||
|
|
||||||
/// Returns an error if there are no signers
|
/// Returns an error if there are no signers
|
||||||
pub fn ensure_signer(&self) -> N34Result<()> {
|
pub fn ensure_signer(&self) -> N34Result<()> {
|
||||||
if self.secret_key.is_none() {
|
if !self.config.keyring_secret_key && self.secret_key.is_none() && self.bunker_url.is_none()
|
||||||
Err(N34Error::SignerRequired)
|
{
|
||||||
} else {
|
return Err(N34Error::SignerRequired);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +184,7 @@ impl fmt::Debug for CliOptions {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("CliOptions")
|
f.debug_struct("CliOptions")
|
||||||
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
||||||
|
.field("bunker_url", &self.bunker_url.as_ref().map(|_| "*******"))
|
||||||
.field("relays", &self.relays)
|
.field("relays", &self.relays)
|
||||||
.field("pow", &self.pow)
|
.field("pow", &self.pow)
|
||||||
.field("config", &self.config)
|
.field("config", &self.config)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use super::PatchStatus;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::{CommandRunner, VecNostrEventExt},
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
@@ -38,6 +38,10 @@ pub struct ApplyArgs {
|
|||||||
/// The open patch id to apply it. Must be orignal root patch or
|
/// The open patch id to apply it. Must be orignal root patch or
|
||||||
/// revision root
|
/// revision root
|
||||||
patch_id: NostrEvent,
|
patch_id: NostrEvent,
|
||||||
|
/// Patches that have been applied. Use this when only some patches have
|
||||||
|
/// been applied, not all.
|
||||||
|
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
|
||||||
|
applied_patches: Vec<NostrEvent>,
|
||||||
/// The applied commits
|
/// The applied commits
|
||||||
#[arg(num_args = 1..)]
|
#[arg(num_args = 1..)]
|
||||||
applied_commits: Vec<Sha1Hash>,
|
applied_commits: Vec<Sha1Hash>,
|
||||||
@@ -51,6 +55,7 @@ impl CommandRunner for ApplyArgs {
|
|||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::MergedApplied,
|
PatchStatus::MergedApplied,
|
||||||
Some(either::Either::Right(self.applied_commits)),
|
Some(either::Either::Right(self.applied_commits)),
|
||||||
|
self.applied_patches.into_event_ids(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_merged_or_applied() {
|
if patch_status.is_merged_or_applied() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ impl CommandRunner for CloseArgs {
|
|||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::Closed,
|
PatchStatus::Closed,
|
||||||
None,
|
None,
|
||||||
|
Vec::new(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_closed() {
|
if patch_status.is_closed() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ impl CommandRunner for DraftArgs {
|
|||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::Draft,
|
PatchStatus::Draft,
|
||||||
None,
|
None,
|
||||||
|
Vec::new(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_drafted() {
|
if patch_status.is_drafted() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ use nostr::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
types::{NaddrOrSet, NostrEvent, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
nostr_utils::{
|
nostr_utils::{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use super::PatchStatus;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
traits::{CommandRunner, VecNostrEventExt},
|
||||||
types::{NaddrOrSet, NostrEvent},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
@@ -38,6 +38,10 @@ pub struct MergeArgs {
|
|||||||
/// The open patch id to merge it. Must be orignal root patch or
|
/// The open patch id to merge it. Must be orignal root patch or
|
||||||
/// revision root
|
/// revision root
|
||||||
patch_id: NostrEvent,
|
patch_id: NostrEvent,
|
||||||
|
/// Patches that have been merged. Use this when only some patches have been
|
||||||
|
/// merged, not all.
|
||||||
|
#[arg(long = "patches", value_name = "PATCH-EVENT-ID")]
|
||||||
|
merged_patches: Vec<NostrEvent>,
|
||||||
/// The merge commit id
|
/// The merge commit id
|
||||||
merge_commit: Sha1Hash,
|
merge_commit: Sha1Hash,
|
||||||
}
|
}
|
||||||
@@ -50,6 +54,7 @@ impl CommandRunner for MergeArgs {
|
|||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::MergedApplied,
|
PatchStatus::MergedApplied,
|
||||||
Some(either::Either::Left(self.merge_commit)),
|
Some(either::Either::Left(self.merge_commit)),
|
||||||
|
self.merged_patches.into_event_ids(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_merged_or_applied() {
|
if patch_status.is_merged_or_applied() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ mod merge;
|
|||||||
mod reopen;
|
mod reopen;
|
||||||
/// `patch send` subcommand
|
/// `patch send` subcommand
|
||||||
mod send;
|
mod send;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt,
|
fmt,
|
||||||
@@ -66,6 +68,15 @@ static PATCH_VERSION_NUMBER_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|||||||
Regex::new(r"\[PATCH\s+(?:v(?<version>\d+)\s*)?(?<number>\d+)/(?:\d+)").unwrap()
|
Regex::new(r"\[PATCH\s+(?:v(?<version>\d+)\s*)?(?<number>\d+)/(?:\d+)").unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Content of the hashtag representing the root patch.
|
||||||
|
pub const ROOT_HASHTAG_CONTENT: &str = "root";
|
||||||
|
/// Content of the hashtag representing the root revision patch.
|
||||||
|
pub const REVISION_ROOT_HASHTAG_CONTENT: &str = "root-revision";
|
||||||
|
/// The content of the hashtag used by `ngit-cli` to represent a root revision
|
||||||
|
/// patch before the commit 6ae42e67d9da36f6c2e1356acba30a3a62112bc7. This was a
|
||||||
|
/// typo.
|
||||||
|
pub const LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT: &str = "revision-root";
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum PatchSubcommands {
|
pub enum PatchSubcommands {
|
||||||
/// Send one or more patches to a repository.
|
/// Send one or more patches to a repository.
|
||||||
@@ -74,7 +85,7 @@ pub enum PatchSubcommands {
|
|||||||
Fetch(FetchArgs),
|
Fetch(FetchArgs),
|
||||||
/// Closes an open or drafted patch.
|
/// Closes an open or drafted patch.
|
||||||
Close(CloseArgs),
|
Close(CloseArgs),
|
||||||
/// Converts the closed or open patch to draft state.
|
/// Converts an open patch to draft state.
|
||||||
Draft(DraftArgs),
|
Draft(DraftArgs),
|
||||||
/// Reopens a closed or drafted patch.
|
/// Reopens a closed or drafted patch.
|
||||||
Reopen(ReopenArgs),
|
Reopen(ReopenArgs),
|
||||||
@@ -286,346 +297,3 @@ fn patch_file_name(subject: &str) -> N34Result<String> {
|
|||||||
.trim()
|
.trim()
|
||||||
.replace("--", "-"))
|
.replace("--", "-"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_normal() {
|
|
||||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
|
||||||
Subject: [PATCH] chore: a to abc
|
|
||||||
|
|
||||||
Abc patch
|
|
||||||
---
|
|
||||||
src/nostr_utils/mod.rs | 1 +
|
|
||||||
1files changed, 3 insertions(+), 1 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
|
||||||
index 4120f5a..e68783c 100644
|
|
||||||
--- a/src/nostr_utils/mod.rs
|
|
||||||
+++ b/src/nostr_utils/mod.rs
|
|
||||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
|
||||||
|
|
||||||
- a
|
|
||||||
+ abc
|
|
||||||
--
|
|
||||||
2.49.0"#;
|
|
||||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
|
||||||
assert_eq!(patch.subject, "[PATCH] chore: a to abc");
|
|
||||||
assert_eq!(patch.body, "Abc patch");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_normal_with_patch_in_content() {
|
|
||||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
|
||||||
Subject: [PATCH] chore: Subject in subject
|
|
||||||
|
|
||||||
A good test patch
|
|
||||||
---
|
|
||||||
src/nostr_utils/mod.rs | 1 +
|
|
||||||
1files changed, 3 insertions(+), 1 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
|
||||||
index 4120f5a..e68783c 100644
|
|
||||||
--- a/src/nostr_utils/mod.rs
|
|
||||||
+++ b/src/nostr_utils/mod.rs
|
|
||||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
|
||||||
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
|
||||||
Subject: [PATCH] chore: What a subject
|
|
||||||
|
|
||||||
hi
|
|
||||||
---
|
|
||||||
--
|
|
||||||
2.49.0"#;
|
|
||||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
|
||||||
assert_eq!(patch.subject, "[PATCH] chore: Subject in subject");
|
|
||||||
assert_eq!(patch.body, "A good test patch");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_multiline_subject() {
|
|
||||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
|
||||||
Subject: [PATCH] chore: Some long subject yes so long one Some long subject yes
|
|
||||||
so long one
|
|
||||||
|
|
||||||
Abc patch
|
|
||||||
---
|
|
||||||
src/nostr_utils/mod.rs | 1 +
|
|
||||||
1files changed, 3 insertions(+), 1 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
|
||||||
index 4120f5a..e68783c 100644
|
|
||||||
--- a/src/nostr_utils/mod.rs
|
|
||||||
+++ b/src/nostr_utils/mod.rs
|
|
||||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
|
||||||
|
|
||||||
- a
|
|
||||||
+ abc
|
|
||||||
--
|
|
||||||
2.49.0"#;
|
|
||||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
patch.subject,
|
|
||||||
"[PATCH] chore: Some long subject yes so long one Some long subject yes so long one"
|
|
||||||
);
|
|
||||||
assert_eq!(patch.body, "Abc patch");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_multiline_body() {
|
|
||||||
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 27 May 2025 19:20:42 +0000
|
|
||||||
Subject: [PATCH] chore: a to abc
|
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
|
||||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
|
||||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
|
||||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
|
||||||
aspernatur.
|
|
||||||
---
|
|
||||||
src/nostr_utils/mod.rs | 1 +
|
|
||||||
1files changed, 3 insertions(+), 1 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
|
||||||
index 4120f5a..e68783c 100644
|
|
||||||
--- a/src/nostr_utils/mod.rs
|
|
||||||
+++ b/src/nostr_utils/mod.rs
|
|
||||||
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
|
||||||
|
|
||||||
- a
|
|
||||||
+ abc
|
|
||||||
--
|
|
||||||
2.49.0"#;
|
|
||||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
|
||||||
assert_eq!(patch.subject, "[PATCH] chore: a to abc");
|
|
||||||
assert_eq!(
|
|
||||||
patch.body,
|
|
||||||
"Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
|
||||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
|
||||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
|
||||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
|
||||||
aspernatur."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_cover_letter() {
|
|
||||||
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
|
||||||
Subject: [PATCH v2 0/7] feat: Some test just a test
|
|
||||||
|
|
||||||
Cover body
|
|
||||||
|
|
||||||
Awiteb (1):
|
|
||||||
chore: Update `README.md`
|
|
||||||
|
|
||||||
README.md | 2 +-
|
|
||||||
|
|
||||||
|
|
||||||
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
|
||||||
--
|
|
||||||
2.49.0"#;
|
|
||||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
|
||||||
assert_eq!(patch.subject, "[PATCH v2 0/7] feat: Some test just a test");
|
|
||||||
assert_eq!(
|
|
||||||
patch.body,
|
|
||||||
"Cover body
|
|
||||||
|
|
||||||
Awiteb (1):
|
|
||||||
chore: Update `README.md`
|
|
||||||
|
|
||||||
README.md | 2 +-
|
|
||||||
|
|
||||||
|
|
||||||
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_multiline_cover_subject() {
|
|
||||||
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
|
||||||
Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some
|
|
||||||
test just a test
|
|
||||||
|
|
||||||
Cover body
|
|
||||||
|
|
||||||
Awiteb (1):
|
|
||||||
chore: Update `README.md`
|
|
||||||
|
|
||||||
README.md | 2 +-
|
|
||||||
|
|
||||||
|
|
||||||
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
|
||||||
--
|
|
||||||
2.49.0"#;
|
|
||||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
patch.subject,
|
|
||||||
"[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a \
|
|
||||||
test"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
patch.body,
|
|
||||||
"Cover body
|
|
||||||
|
|
||||||
Awiteb (1):
|
|
||||||
chore: Update `README.md`
|
|
||||||
|
|
||||||
README.md | 2 +-
|
|
||||||
|
|
||||||
|
|
||||||
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_multiline_cover_body() {
|
|
||||||
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Awiteb <a@4rs.nl>
|
|
||||||
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
|
||||||
Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some
|
|
||||||
test just a test
|
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
|
||||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
|
||||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
|
||||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
|
||||||
aspernatur.
|
|
||||||
|
|
||||||
Awiteb (1):
|
|
||||||
chore: Update `README.md`
|
|
||||||
|
|
||||||
README.md | 2 +-
|
|
||||||
|
|
||||||
|
|
||||||
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
|
||||||
--
|
|
||||||
2.49.0"#;
|
|
||||||
let patch = GitPatch::from_str(patch_content).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
patch.subject,
|
|
||||||
"[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a \
|
|
||||||
test"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
patch.body,
|
|
||||||
"Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
|
||||||
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
|
||||||
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
|
||||||
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
|
||||||
aspernatur.
|
|
||||||
|
|
||||||
Awiteb (1):
|
|
||||||
chore: Update `README.md`
|
|
||||||
|
|
||||||
README.md | 2 +-
|
|
||||||
|
|
||||||
|
|
||||||
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn normal_patch_filename() {
|
|
||||||
let mut patch = GitPatch {
|
|
||||||
inner: String::new(),
|
|
||||||
subject: String::new(),
|
|
||||||
body: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
patch.subject = "[PATCH v2 0/3] feat: Some test just a test".to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from("v2-0000-cover-letter.patch")
|
|
||||||
);
|
|
||||||
patch.subject = "[PATCH 0/3] feat: Some test just a test".to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from("0000-cover-letter.patch")
|
|
||||||
);
|
|
||||||
patch.subject = "[PATCH v2 1/3] feat: Some test just a test".to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from("v2-0001-feat-some-test-just-a-test.patch")
|
|
||||||
);
|
|
||||||
patch.subject = "[PATCH v42 1/3] feat: Some test just a test".to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from("v42-0001-feat-some-test-just-a-test.patch")
|
|
||||||
);
|
|
||||||
patch.subject = "[PATCH v42 23/30] feat: Some test just a test".to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from("v42-0023-feat-some-test-just-a-test.patch")
|
|
||||||
);
|
|
||||||
patch.subject = "[PATCH 1/3] feat: Some test just a test".to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from("0001-feat-some-test-just-a-test.patch")
|
|
||||||
);
|
|
||||||
patch.subject = "[PATCH 32/50] feat: Some test just a test".to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from("0032-feat-some-test-just-a-test.patch")
|
|
||||||
);
|
|
||||||
patch.subject = "[PATCH v100 32/50] feat: some long subject some long subject some long \
|
|
||||||
subject some long subject"
|
|
||||||
.to_owned();
|
|
||||||
assert_eq!(
|
|
||||||
patch.filename("").unwrap(),
|
|
||||||
PathBuf::from(
|
|
||||||
"v100-0032-feat-some-long-subject-some-long-subject-some-long-subject.patch"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_filename_without_patch() {
|
|
||||||
let mut patch = GitPatch {
|
|
||||||
inner: String::new(),
|
|
||||||
subject: "[RFC v5 1/2] Something".to_owned(),
|
|
||||||
body: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(patch.filename("").is_err());
|
|
||||||
patch.subject = "Something".to_owned();
|
|
||||||
assert!(patch.filename("").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_filename_without_number() {
|
|
||||||
let mut patch = GitPatch {
|
|
||||||
inner: String::new(),
|
|
||||||
subject: "[PATCH v5 /2] Something".to_owned(),
|
|
||||||
body: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(patch.filename("").is_err());
|
|
||||||
patch.subject = "[PATCH v5 2/] Something".to_owned();
|
|
||||||
assert!(patch.filename("").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn patch_filename_without_version() {
|
|
||||||
let patch = GitPatch {
|
|
||||||
inner: String::new(),
|
|
||||||
subject: "[PATCH 1/2] Something".to_owned(),
|
|
||||||
body: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(patch.filename("").is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ impl CommandRunner for ReopenArgs {
|
|||||||
self.naddrs,
|
self.naddrs,
|
||||||
PatchStatus::Open,
|
PatchStatus::Open,
|
||||||
None,
|
None,
|
||||||
|
Vec::new(),
|
||||||
|patch_status| {
|
|patch_status| {
|
||||||
if patch_status.is_open() {
|
if patch_status.is_open() {
|
||||||
return Err(N34Error::InvalidStatus(
|
return Err(N34Error::InvalidStatus(
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ use super::GitPatch;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
traits::CommandRunner,
|
patch::{REVISION_ROOT_HASHTAG_CONTENT, ROOT_HASHTAG_CONTENT},
|
||||||
types::{NaddrOrSet, NostrEvent, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
traits::{CommandRunner, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::{NaddrOrSet, NostrEvent},
|
||||||
},
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{
|
nostr_utils::{
|
||||||
@@ -72,8 +73,8 @@ impl CommandRunner for SendArgs {
|
|||||||
|
|
||||||
let repo_coordinates = naddrs.clone().into_coordinates();
|
let repo_coordinates = naddrs.clone().into_coordinates();
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let user_pubk = options.pubkey().await?;
|
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
|
let user_pubk = client.pubkey().await?;
|
||||||
|
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
if let Some(original_patch) = &self.original_patch {
|
if let Some(original_patch) = &self.original_patch {
|
||||||
@@ -241,14 +242,17 @@ async fn make_patch(
|
|||||||
event_builder =
|
event_builder =
|
||||||
event_builder.tag(utils::event_reply_tag(&root_id, write_relay, Marker::Root));
|
event_builder.tag(utils::event_reply_tag(&root_id, write_relay, Marker::Root));
|
||||||
} else {
|
} else {
|
||||||
event_builder = event_builder.tag(Tag::hashtag("root"));
|
event_builder = event_builder.tag(Tag::hashtag(ROOT_HASHTAG_CONTENT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles the case where there is a patch to reply to but no root. This
|
||||||
|
// indicates we are processing a revision, as the root revision should reply
|
||||||
|
// directly to the original patch.
|
||||||
if let Some(reply_to_id) = reply_to {
|
if let Some(reply_to_id) = reply_to {
|
||||||
if root.is_none() {
|
if root.is_none() {
|
||||||
event_builder = event_builder.tags([
|
event_builder = event_builder.tags([
|
||||||
utils::event_reply_tag(&reply_to_id, write_relay, Marker::Reply),
|
utils::event_reply_tag(&reply_to_id, write_relay, Marker::Reply),
|
||||||
Tag::hashtag("root-revision"),
|
Tag::hashtag(REVISION_ROOT_HASHTAG_CONTENT),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
event_builder = event_builder.tag(utils::event_reply_tag(
|
event_builder = event_builder.tag(utils::event_reply_tag(
|
||||||
|
|||||||
351
src/cli/commands/patch/tests.rs
Normal file
351
src/cli/commands/patch/tests.rs
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
// 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 super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_normal() {
|
||||||
|
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||||
|
Subject: [PATCH] chore: a to abc
|
||||||
|
|
||||||
|
Abc patch
|
||||||
|
---
|
||||||
|
src/nostr_utils/mod.rs | 1 +
|
||||||
|
1files changed, 3 insertions(+), 1 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||||
|
index 4120f5a..e68783c 100644
|
||||||
|
--- a/src/nostr_utils/mod.rs
|
||||||
|
+++ b/src/nostr_utils/mod.rs
|
||||||
|
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||||
|
|
||||||
|
- a
|
||||||
|
+ abc
|
||||||
|
--
|
||||||
|
2.49.0"#;
|
||||||
|
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||||
|
assert_eq!(patch.subject, "[PATCH] chore: a to abc");
|
||||||
|
assert_eq!(patch.body, "Abc patch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_normal_with_patch_in_content() {
|
||||||
|
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||||
|
Subject: [PATCH] chore: Subject in subject
|
||||||
|
|
||||||
|
A good test patch
|
||||||
|
---
|
||||||
|
src/nostr_utils/mod.rs | 1 +
|
||||||
|
1files changed, 3 insertions(+), 1 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||||
|
index 4120f5a..e68783c 100644
|
||||||
|
--- a/src/nostr_utils/mod.rs
|
||||||
|
+++ b/src/nostr_utils/mod.rs
|
||||||
|
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||||
|
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||||
|
Subject: [PATCH] chore: What a subject
|
||||||
|
|
||||||
|
hi
|
||||||
|
---
|
||||||
|
--
|
||||||
|
2.49.0"#;
|
||||||
|
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||||
|
assert_eq!(patch.subject, "[PATCH] chore: Subject in subject");
|
||||||
|
assert_eq!(patch.body, "A good test patch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_multiline_subject() {
|
||||||
|
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||||
|
Subject: [PATCH] chore: Some long subject yes so long one Some long subject yes
|
||||||
|
so long one
|
||||||
|
|
||||||
|
Abc patch
|
||||||
|
---
|
||||||
|
src/nostr_utils/mod.rs | 1 +
|
||||||
|
1files changed, 3 insertions(+), 1 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||||
|
index 4120f5a..e68783c 100644
|
||||||
|
--- a/src/nostr_utils/mod.rs
|
||||||
|
+++ b/src/nostr_utils/mod.rs
|
||||||
|
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||||
|
|
||||||
|
- a
|
||||||
|
+ abc
|
||||||
|
--
|
||||||
|
2.49.0"#;
|
||||||
|
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
patch.subject,
|
||||||
|
"[PATCH] chore: Some long subject yes so long one Some long subject yes so long one"
|
||||||
|
);
|
||||||
|
assert_eq!(patch.body, "Abc patch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_multiline_body() {
|
||||||
|
let patch_content = r#"From 24e8522268ad675996fc3b35209ce23951236bdc Mon Sep 17 00:00:00 2001
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 27 May 2025 19:20:42 +0000
|
||||||
|
Subject: [PATCH] chore: a to abc
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||||
|
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||||
|
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||||
|
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||||
|
aspernatur.
|
||||||
|
---
|
||||||
|
src/nostr_utils/mod.rs | 1 +
|
||||||
|
1files changed, 3 insertions(+), 1 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/nostr_utils/mod.rs b/src/nostr_utils/mod.rs
|
||||||
|
index 4120f5a..e68783c 100644
|
||||||
|
--- a/src/nostr_utils/mod.rs
|
||||||
|
+++ b/src/nostr_utils/mod.rs
|
||||||
|
@@ -103,31 +103,9 @@ impl CommandRunner for NewArgs {
|
||||||
|
|
||||||
|
- a
|
||||||
|
+ abc
|
||||||
|
--
|
||||||
|
2.49.0"#;
|
||||||
|
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||||
|
assert_eq!(patch.subject, "[PATCH] chore: a to abc");
|
||||||
|
assert_eq!(
|
||||||
|
patch.body,
|
||||||
|
"Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||||
|
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||||
|
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||||
|
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||||
|
aspernatur."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_cover_letter() {
|
||||||
|
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
||||||
|
Subject: [PATCH v2 0/7] feat: Some test just a test
|
||||||
|
|
||||||
|
Cover body
|
||||||
|
|
||||||
|
Awiteb (1):
|
||||||
|
chore: Update `README.md`
|
||||||
|
|
||||||
|
README.md | 2 +-
|
||||||
|
|
||||||
|
|
||||||
|
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
||||||
|
--
|
||||||
|
2.49.0"#;
|
||||||
|
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||||
|
assert_eq!(patch.subject, "[PATCH v2 0/7] feat: Some test just a test");
|
||||||
|
assert_eq!(
|
||||||
|
patch.body,
|
||||||
|
"Cover body
|
||||||
|
|
||||||
|
Awiteb (1):
|
||||||
|
chore: Update `README.md`
|
||||||
|
|
||||||
|
README.md | 2 +-
|
||||||
|
|
||||||
|
|
||||||
|
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_multiline_cover_subject() {
|
||||||
|
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
||||||
|
Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some
|
||||||
|
test just a test
|
||||||
|
|
||||||
|
Cover body
|
||||||
|
|
||||||
|
Awiteb (1):
|
||||||
|
chore: Update `README.md`
|
||||||
|
|
||||||
|
README.md | 2 +-
|
||||||
|
|
||||||
|
|
||||||
|
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
||||||
|
--
|
||||||
|
2.49.0"#;
|
||||||
|
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
patch.subject,
|
||||||
|
"[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
patch.body,
|
||||||
|
"Cover body
|
||||||
|
|
||||||
|
Awiteb (1):
|
||||||
|
chore: Update `README.md`
|
||||||
|
|
||||||
|
README.md | 2 +-
|
||||||
|
|
||||||
|
|
||||||
|
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_multiline_cover_body() {
|
||||||
|
let patch_content = r#"From 864f3018f62ab2e1265edb670d5493dafe7d2cb2 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Awiteb <a@4rs.nl>
|
||||||
|
Date: Tue, 3 Jun 2025 08:41:12 +0000
|
||||||
|
Subject: [PATCH v2 0/7] feat: Some test just a test some test just a test some
|
||||||
|
test just a test
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||||
|
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||||
|
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||||
|
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||||
|
aspernatur.
|
||||||
|
|
||||||
|
Awiteb (1):
|
||||||
|
chore: Update `README.md`
|
||||||
|
|
||||||
|
README.md | 2 +-
|
||||||
|
|
||||||
|
|
||||||
|
base-commit: f670859b92d525874fd621452080c8479964ac6a
|
||||||
|
--
|
||||||
|
2.49.0"#;
|
||||||
|
let patch = GitPatch::from_str(patch_content).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
patch.subject,
|
||||||
|
"[PATCH v2 0/7] feat: Some test just a test some test just a test some test just a test"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
patch.body,
|
||||||
|
"Lorem ipsum dolor sit amet. 33 laborum galisum aut fugiat dicta vel accusamus
|
||||||
|
aliquam vel quisquam fuga in incidunt voluptas a aliquid neque ab iure pariatur.
|
||||||
|
Et molestiae vero a consectetur laborum et accusantium sequi. Et ratione
|
||||||
|
atque et molestiae dolorem in asperiores amet id dolor corporis in adipisci
|
||||||
|
aspernatur.
|
||||||
|
|
||||||
|
Awiteb (1):
|
||||||
|
chore: Update `README.md`
|
||||||
|
|
||||||
|
README.md | 2 +-
|
||||||
|
|
||||||
|
|
||||||
|
base-commit: f670859b92d525874fd621452080c8479964ac6a"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_patch_filename() {
|
||||||
|
let mut patch = GitPatch {
|
||||||
|
inner: String::new(),
|
||||||
|
subject: String::new(),
|
||||||
|
body: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
patch.subject = "[PATCH v2 0/3] feat: Some test just a test".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("v2-0000-cover-letter.patch")
|
||||||
|
);
|
||||||
|
patch.subject = "[PATCH 0/3] feat: Some test just a test".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("0000-cover-letter.patch")
|
||||||
|
);
|
||||||
|
patch.subject = "[PATCH v2 1/3] feat: Some test just a test".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("v2-0001-feat-some-test-just-a-test.patch")
|
||||||
|
);
|
||||||
|
patch.subject = "[PATCH v42 1/3] feat: Some test just a test".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("v42-0001-feat-some-test-just-a-test.patch")
|
||||||
|
);
|
||||||
|
patch.subject = "[PATCH v42 23/30] feat: Some test just a test".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("v42-0023-feat-some-test-just-a-test.patch")
|
||||||
|
);
|
||||||
|
patch.subject = "[PATCH 1/3] feat: Some test just a test".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("0001-feat-some-test-just-a-test.patch")
|
||||||
|
);
|
||||||
|
patch.subject = "[PATCH 32/50] feat: Some test just a test".to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("0032-feat-some-test-just-a-test.patch")
|
||||||
|
);
|
||||||
|
patch.subject = "[PATCH v100 32/50] feat: some long subject some long subject some long \
|
||||||
|
subject some long subject"
|
||||||
|
.to_owned();
|
||||||
|
assert_eq!(
|
||||||
|
patch.filename("").unwrap(),
|
||||||
|
PathBuf::from("v100-0032-feat-some-long-subject-some-long-subject-some-long-subject.patch")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_filename_without_patch() {
|
||||||
|
let mut patch = GitPatch {
|
||||||
|
inner: String::new(),
|
||||||
|
subject: "[RFC v5 1/2] Something".to_owned(),
|
||||||
|
body: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(patch.filename("").is_err());
|
||||||
|
patch.subject = "Something".to_owned();
|
||||||
|
assert!(patch.filename("").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_filename_without_number() {
|
||||||
|
let mut patch = GitPatch {
|
||||||
|
inner: String::new(),
|
||||||
|
subject: "[PATCH v5 /2] Something".to_owned(),
|
||||||
|
body: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(patch.filename("").is_err());
|
||||||
|
patch.subject = "[PATCH v5 2/] Something".to_owned();
|
||||||
|
assert!(patch.filename("").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn patch_filename_without_version() {
|
||||||
|
let patch = GitPatch {
|
||||||
|
inner: String::new(),
|
||||||
|
subject: "[PATCH 1/2] Something".to_owned(),
|
||||||
|
body: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(patch.filename("").is_ok());
|
||||||
|
}
|
||||||
@@ -27,7 +27,10 @@ use nostr::{
|
|||||||
|
|
||||||
use super::{CliOptions, CommandRunner};
|
use super::{CliOptions, CommandRunner};
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::types::{NaddrOrSet, NostrEvent, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
cli::{
|
||||||
|
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::{NaddrOrSet, NostrEvent},
|
||||||
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
nostr_utils::{
|
nostr_utils::{
|
||||||
NostrClient,
|
NostrClient,
|
||||||
@@ -78,8 +81,7 @@ impl CommandRunner for ReplyArgs {
|
|||||||
let nostr_address_path = utils::nostr_address_path()?;
|
let nostr_address_path = utils::nostr_address_path()?;
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = options.pubkey().await?;
|
let user_pubk = client.pubkey().await?;
|
||||||
|
|
||||||
let repo_naddrs = if let Some(naddrs) = self.naddrs.flat_naddrs(&options.config.sets)? {
|
let repo_naddrs = if let Some(naddrs) = self.naddrs.flat_naddrs(&options.config.sets)? {
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
Some(naddrs)
|
Some(naddrs)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use futures::future;
|
|||||||
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, types::RelayOrSetVecExt},
|
cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, traits::RelayOrSetVecExt},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
||||||
};
|
};
|
||||||
@@ -83,7 +83,7 @@ impl CommandRunner for AnnounceArgs {
|
|||||||
async fn run(mut self, options: CliOptions) -> N34Result<()> {
|
async fn run(mut self, options: CliOptions) -> N34Result<()> {
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = options.pubkey().await?;
|
let user_pubk = client.pubkey().await?;
|
||||||
let relays_list = client.user_relays_list(user_pubk).await?;
|
let relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
client
|
client
|
||||||
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
.add_relays(&utils::add_read_relays(relays_list.as_ref()))
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ use crate::error::N34Result;
|
|||||||
pub enum RepoSubcommands {
|
pub enum RepoSubcommands {
|
||||||
/// View details of a nostr git repository
|
/// View details of a nostr git repository
|
||||||
View(ViewArgs),
|
View(ViewArgs),
|
||||||
/// Publish information about a git repository to Nostr for collaboration
|
/// Broadcast and update a git repository
|
||||||
/// and feedback. Can also be used to update an existing repository's
|
|
||||||
/// details.
|
|
||||||
Announce(AnnounceArgs),
|
Announce(AnnounceArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
CommandRunner,
|
CommandRunner,
|
||||||
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
types::NaddrOrSet,
|
||||||
},
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ use crate::{
|
|||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
ConfigError,
|
ConfigError,
|
||||||
MutRepoRelaySetsExt,
|
traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
traits::CommandRunner,
|
types::{NaddrOrSet, RelayOrSet},
|
||||||
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
|
||||||
},
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ use clap::Args;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
MutRepoRelaySetsExt,
|
traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
traits::CommandRunner,
|
types::{NaddrOrSet, RelayOrSet},
|
||||||
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
|
||||||
},
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ use clap::Args;
|
|||||||
use nostr::nips::nip19::ToBech32;
|
use nostr::nips::nip19::ToBech32;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, RepoRelaySet, RepoRelaySetsExt, traits::CommandRunner},
|
cli::{
|
||||||
|
CliOptions,
|
||||||
|
RepoRelaySet,
|
||||||
|
traits::{CommandRunner, RepoRelaySetsExt},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ use clap::Args;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
cli::{
|
||||||
CliOptions,
|
CliOptions,
|
||||||
MutRepoRelaySetsExt,
|
traits::{CommandRunner, MutRepoRelaySetsExt, NaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
traits::CommandRunner,
|
types::{NaddrOrSet, RelayOrSet},
|
||||||
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
|
||||||
},
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use nostr::{
|
|||||||
filter::Filter,
|
filter::Filter,
|
||||||
hashes::sha1::Hash as Sha1Hash,
|
hashes::sha1::Hash as Sha1Hash,
|
||||||
nips::{nip10::Marker, nip19::ToBech32},
|
nips::{nip10::Marker, nip19::ToBech32},
|
||||||
|
types::RelayUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -31,7 +32,7 @@ use super::{
|
|||||||
types::{NaddrOrSet, NostrEvent},
|
types::{NaddrOrSet, NostrEvent},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::types::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
cli::traits::{OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -49,13 +50,13 @@ pub async fn issue_status_command(
|
|||||||
new_status: IssueStatus,
|
new_status: IssueStatus,
|
||||||
check_fn: impl FnOnce(&IssueStatus) -> N34Result<()>,
|
check_fn: impl FnOnce(&IssueStatus) -> N34Result<()>,
|
||||||
) -> N34Result<()> {
|
) -> N34Result<()> {
|
||||||
let user_pkey = options.pubkey().await?;
|
|
||||||
let naddrs = utils::naddrs_or_file(
|
let naddrs = utils::naddrs_or_file(
|
||||||
naddrs.flat_naddrs(&options.config.sets)?,
|
naddrs.flat_naddrs(&options.config.sets)?,
|
||||||
&utils::nostr_address_path()?,
|
&utils::nostr_address_path()?,
|
||||||
)?;
|
)?;
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
|
let user_pubk = client.pubkey().await?;
|
||||||
client
|
client
|
||||||
.add_relays(&[naddrs.extract_relays(), issue_id.relays].concat())
|
.add_relays(&[naddrs.extract_relays(), issue_id.relays].concat())
|
||||||
.await;
|
.await;
|
||||||
@@ -97,10 +98,10 @@ pub async fn issue_status_command(
|
|||||||
.map(|c| Tag::coordinate(c, relay_hint.clone())),
|
.map(|c| Tag::coordinate(c, relay_hint.clone())),
|
||||||
)
|
)
|
||||||
.dedup_tags()
|
.dedup_tags()
|
||||||
.build(user_pkey);
|
.build(user_pubk);
|
||||||
|
|
||||||
let event_id = status_event.id.expect("There is an id");
|
let event_id = status_event.id.expect("There is an id");
|
||||||
let user_relays_list = client.user_relays_list(user_pkey).await?;
|
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
let write_relays = [
|
let write_relays = [
|
||||||
relays,
|
relays,
|
||||||
naddrs.extract_relays(),
|
naddrs.extract_relays(),
|
||||||
@@ -130,15 +131,16 @@ pub async fn patch_status_command(
|
|||||||
naddrs: Option<Vec<NaddrOrSet>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
new_status: PatchStatus,
|
new_status: PatchStatus,
|
||||||
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
|
merge_or_applied_commits: Option<Either<Sha1Hash, Vec<Sha1Hash>>>,
|
||||||
|
merge_or_applied_patches: Vec<EventId>,
|
||||||
check_fn: impl FnOnce(&PatchStatus) -> N34Result<()>,
|
check_fn: impl FnOnce(&PatchStatus) -> N34Result<()>,
|
||||||
) -> N34Result<()> {
|
) -> N34Result<()> {
|
||||||
let user_pkey = options.pubkey().await?;
|
|
||||||
let naddrs = utils::naddrs_or_file(
|
let naddrs = utils::naddrs_or_file(
|
||||||
naddrs.flat_naddrs(&options.config.sets)?,
|
naddrs.flat_naddrs(&options.config.sets)?,
|
||||||
&utils::nostr_address_path()?,
|
&utils::nostr_address_path()?,
|
||||||
)?;
|
)?;
|
||||||
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
let relays = options.relays.clone().flat_relays(&options.config.sets)?;
|
||||||
let client = NostrClient::init(&options, &relays).await;
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
|
let user_pubk = client.pubkey().await?;
|
||||||
client
|
client
|
||||||
.add_relays(&[naddrs.extract_relays(), patch_id.relays].concat())
|
.add_relays(&[naddrs.extract_relays(), patch_id.relays].concat())
|
||||||
.await;
|
.await;
|
||||||
@@ -209,28 +211,26 @@ pub async fn patch_status_command(
|
|||||||
.tags(commits.into_iter().map(Tag::reference));
|
.tags(commits.into_iter().map(Tag::reference));
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = if let Some(root_revision) = root_revision {
|
if let Some(root_revision) = root_revision {
|
||||||
status_builder = status_builder.tag(utils::event_reply_tag(
|
status_builder = status_builder.tag(utils::event_reply_tag(
|
||||||
&root_revision,
|
&root_revision,
|
||||||
relay_hint.as_ref(),
|
relay_hint.as_ref(),
|
||||||
Marker::Reply,
|
Marker::Reply,
|
||||||
));
|
));
|
||||||
root_revision
|
|
||||||
} else {
|
|
||||||
root_patch
|
|
||||||
};
|
|
||||||
let patches = client.fetch_patch_series(root, patch_event.pubkey).await?;
|
|
||||||
status_builder = status_builder.tags(
|
|
||||||
patches
|
|
||||||
.into_iter()
|
|
||||||
.map(|p| utils::event_reply_tag(&p.id, relay_hint.as_ref(), Marker::Mention)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_event = status_builder.dedup_tags().build(user_pkey);
|
if !merge_or_applied_patches.is_empty() {
|
||||||
|
status_builder = status_builder.tags(
|
||||||
|
build_patches_quote(client.clone(), relay_hint.clone(), merge_or_applied_patches)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_event = status_builder.dedup_tags().build(user_pubk);
|
||||||
|
|
||||||
let event_id = status_event.id.expect("There is an id");
|
let event_id = status_event.id.expect("There is an id");
|
||||||
let user_relays_list = client.user_relays_list(user_pkey).await?;
|
let user_relays_list = client.user_relays_list(user_pubk).await?;
|
||||||
let write_relays = [
|
let write_relays = [
|
||||||
relays,
|
relays,
|
||||||
naddrs.extract_relays(),
|
naddrs.extract_relays(),
|
||||||
@@ -279,7 +279,6 @@ pub async fn list_patches_and_issues(
|
|||||||
.add_relays(&client.read_relays_from_users(&authorized_pubkeys).await)
|
.add_relays(&client.read_relays_from_users(&authorized_pubkeys).await)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
||||||
let kind = if list_patches {
|
let kind = if list_patches {
|
||||||
Kind::GitPatch
|
Kind::GitPatch
|
||||||
} else {
|
} else {
|
||||||
@@ -352,7 +351,6 @@ pub async fn list_patches_and_issues(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Returns a tuple of (root_id, patch_id) if this is a valid root or revision
|
/// Returns a tuple of (root_id, patch_id) if this is a valid root or revision
|
||||||
/// patch.
|
/// patch.
|
||||||
fn get_patch_root_revision(patch_event: &Event) -> N34Result<(EventId, Option<EventId>)> {
|
fn get_patch_root_revision(patch_event: &Event) -> N34Result<(EventId, Option<EventId>)> {
|
||||||
@@ -401,3 +399,40 @@ fn format_patch_and_issue(event: &Event, status: Either<PatchStatus, IssueStatus
|
|||||||
event.id.to_bech32().expect("Infallible")
|
event.id.to_bech32().expect("Infallible")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a list of tags for quoting patches in merge/applied status events.
|
||||||
|
async fn build_patches_quote(
|
||||||
|
client: NostrClient,
|
||||||
|
relay_hint: Option<RelayUrl>,
|
||||||
|
patches: Vec<EventId>,
|
||||||
|
) -> Vec<Tag> {
|
||||||
|
let client = Arc::new(client);
|
||||||
|
let relay_hint = Arc::new(relay_hint);
|
||||||
|
|
||||||
|
future::join_all(patches.into_iter().map(|eid| {
|
||||||
|
let task_relay = Arc::clone(&relay_hint);
|
||||||
|
let task_client = Arc::clone(&client);
|
||||||
|
|
||||||
|
async move {
|
||||||
|
Tag::custom(
|
||||||
|
TagKind::q(),
|
||||||
|
[
|
||||||
|
eid.to_hex(),
|
||||||
|
task_relay
|
||||||
|
.as_ref()
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
task_client
|
||||||
|
.event_author(eid)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|p| p.to_hex())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,15 +14,17 @@
|
|||||||
// 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::{collections::HashSet, fs, path::PathBuf};
|
use std::{collections::HashSet, fs, net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
nips::nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
nips::{nip19::Nip19Coordinate, nip46::NostrConnectURI},
|
||||||
types::RelayUrl,
|
types::RelayUrl,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
|
||||||
|
|
||||||
use crate::error::{N34Error, N34Result};
|
use crate::{
|
||||||
|
cli::traits::{MutRepoRelaySetsExt, RepoRelaySetsExt},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
/// Errors that can occur when working with configuration files.
|
/// Errors that can occur when working with configuration files.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@@ -63,6 +65,20 @@ pub struct CliConfig {
|
|||||||
/// List of fallback relays used if no fallback relays was provided.
|
/// List of fallback relays used if no fallback relays was provided.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub fallback_relays: Option<Vec<RelayUrl>>,
|
pub fallback_relays: Option<Vec<RelayUrl>>,
|
||||||
|
/// Default Nostr bunker URL used for signing events.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
deserialize_with = "super::parsers::de_bunker_url",
|
||||||
|
serialize_with = "super::parsers::ser_bunker_url"
|
||||||
|
)]
|
||||||
|
pub bunker_url: Option<NostrConnectURI>,
|
||||||
|
/// Whether to use the system keyring to store the secret key.
|
||||||
|
#[serde(default)]
|
||||||
|
pub keyring_secret_key: bool,
|
||||||
|
/// Signs events using the browser's NIP-07 extension.
|
||||||
|
#[serde(default)]
|
||||||
|
pub nip07: Option<SocketAddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A named group of repositories and relays.
|
/// A named group of repositories and relays.
|
||||||
@@ -74,8 +90,8 @@ pub struct RepoRelaySet {
|
|||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "HashSet::is_empty",
|
skip_serializing_if = "HashSet::is_empty",
|
||||||
serialize_with = "ser_naddrs",
|
serialize_with = "super::parsers::ser_naddrs",
|
||||||
deserialize_with = "de_naddrs"
|
deserialize_with = "super::parsers::de_naddrs"
|
||||||
)]
|
)]
|
||||||
pub naddrs: HashSet<Nip19Coordinate>,
|
pub naddrs: HashSet<Nip19Coordinate>,
|
||||||
/// Relay URLs in this group.
|
/// Relay URLs in this group.
|
||||||
@@ -152,163 +168,3 @@ impl RepoRelaySet {
|
|||||||
self.naddrs.retain(|n| seen.insert(n.coordinate.clone()));
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub mod config;
|
|||||||
pub mod defaults;
|
pub mod defaults;
|
||||||
/// Macros for CLI application.
|
/// Macros for CLI application.
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
|
/// Represents the state used for CLI options.
|
||||||
|
pub mod options_state;
|
||||||
/// CLI arguments parsers
|
/// CLI arguments parsers
|
||||||
pub mod parsers;
|
pub mod parsers;
|
||||||
/// CLI traits
|
/// CLI traits
|
||||||
@@ -31,14 +33,24 @@ pub mod traits;
|
|||||||
/// Common helper types used throughout the CLI.
|
/// Common helper types used throughout the CLI.
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap_verbosity_flag::Verbosity;
|
use clap_verbosity_flag::Verbosity;
|
||||||
|
use nostr::key::Keys;
|
||||||
|
use nostr::key::SecretKey;
|
||||||
|
use nostr_browser_signer_proxy::BrowserSignerProxy;
|
||||||
|
use nostr_browser_signer_proxy::BrowserSignerProxyOptions;
|
||||||
|
use nostr_keyring::KeyringError;
|
||||||
|
use nostr_keyring::NostrKeyring;
|
||||||
use types::RelayOrSet;
|
use types::RelayOrSet;
|
||||||
|
|
||||||
pub use self::commands::*;
|
pub use self::commands::*;
|
||||||
pub use self::config::*;
|
pub use self::config::*;
|
||||||
use self::traits::CommandRunner;
|
use self::traits::CommandRunner;
|
||||||
|
use crate::cli::options_state::BROWSER_SIGNER_PROXY_TIMEOUT;
|
||||||
|
use crate::error::N34Error;
|
||||||
use crate::error::N34Result;
|
use crate::error::N34Result;
|
||||||
|
use crate::nostr_utils::traits::NostrKeyringErrorUtils;
|
||||||
|
|
||||||
/// Header message, used in the help message
|
/// Header message, used in the help message
|
||||||
const HEADER: &str = r#"Copyright (C) 2025 Awiteb <a@4rs.nl>
|
const HEADER: &str = r#"Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
@@ -51,6 +63,7 @@ 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>."#;
|
||||||
|
|
||||||
|
|
||||||
/// Name of the file storing the repository address
|
/// Name of the file storing the repository address
|
||||||
pub const NOSTR_ADDRESS_FILE: &str = "nostr-address";
|
pub const NOSTR_ADDRESS_FILE: &str = "nostr-address";
|
||||||
|
|
||||||
@@ -71,10 +84,58 @@ pub struct Cli {
|
|||||||
|
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
|
/// Keyring service name of n34
|
||||||
|
pub const N34_KEYRING_SERVICE_NAME: &str = "n34";
|
||||||
|
/// Keyring entry name of the n34 keypair
|
||||||
|
pub const N34_KEY_PAIR_ENTRY: &str = "n34_keypair";
|
||||||
|
/// Keyring entry name of the user secret key
|
||||||
|
pub const USER_KEY_PAIR_ENTRY: &str = "user_keypair";
|
||||||
|
|
||||||
/// Executes the command
|
/// Executes the command
|
||||||
pub async fn run(self) -> N34Result<()> {
|
pub async fn run(self) -> N34Result<()> {
|
||||||
self.command.run(self.options).await
|
self.command.run(self.options).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the n34 keypair from the keyring or generates and stores a new one
|
||||||
|
/// if none exists.
|
||||||
|
pub fn n34_keypair() -> N34Result<Keys> {
|
||||||
|
let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);
|
||||||
|
|
||||||
|
match keyring.get(Self::N34_KEY_PAIR_ENTRY) {
|
||||||
|
Ok(keys) => Ok(keys),
|
||||||
|
Err(nostr_keyring::Error::Keyring(KeyringError::NoEntry)) => {
|
||||||
|
let new_keys = Keys::generate();
|
||||||
|
keyring.set(Self::N34_KEY_PAIR_ENTRY, &new_keys)?;
|
||||||
|
Ok(new_keys)
|
||||||
|
}
|
||||||
|
Err(err) => Err(N34Error::Keyring(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the user's keypair from the keyring. If no key exists and one
|
||||||
|
/// is provided, stores and returns it. If no key exists and none is
|
||||||
|
/// provided, returns an error.
|
||||||
|
pub fn user_keypair(secret_key: Option<SecretKey>) -> N34Result<Keys> {
|
||||||
|
let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME);
|
||||||
|
let keyring_key = keyring.get(Self::USER_KEY_PAIR_ENTRY);
|
||||||
|
|
||||||
|
if let Err(ref err) = keyring_key
|
||||||
|
&& err.is_keyring_no_entry()
|
||||||
|
&& let Some(secret_key) = secret_key
|
||||||
|
{
|
||||||
|
let keypair = Keys::new(secret_key);
|
||||||
|
keyring.set(Self::USER_KEY_PAIR_ENTRY, &keypair)?;
|
||||||
|
return Ok(keypair);
|
||||||
|
}
|
||||||
|
|
||||||
|
keyring_key.map_err(|err| {
|
||||||
|
if err.is_keyring_no_entry() {
|
||||||
|
N34Error::SecretKeyKeyringWithoutEntry
|
||||||
|
} else {
|
||||||
|
N34Error::Keyring(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the CLI configuration by applying fallback values from config if
|
/// Processes the CLI configuration by applying fallback values from config if
|
||||||
@@ -88,5 +149,30 @@ pub fn post_cli(mut cli: Cli) -> N34Result<Cli> {
|
|||||||
cli.options.relays = relays.iter().cloned().map(RelayOrSet::Relay).collect();
|
cli.options.relays = relays.iter().cloned().map(RelayOrSet::Relay).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically sets the signer based on the configuration if no signer
|
||||||
|
// is provided.
|
||||||
|
if !cli.options.nip07
|
||||||
|
&& cli.options.bunker_url.is_none()
|
||||||
|
&& (cli.options.secret_key.is_none() || cli.options.config.keyring_secret_key)
|
||||||
|
{
|
||||||
|
if let Some(addr) = cli.options.config.nip07 {
|
||||||
|
cli.options.nip07 = true;
|
||||||
|
cli.options.state.browser_signer_proxy = BrowserSignerProxy::new(
|
||||||
|
BrowserSignerProxyOptions::default()
|
||||||
|
.timeout(BROWSER_SIGNER_PROXY_TIMEOUT)
|
||||||
|
.ip_addr(addr.ip())
|
||||||
|
.port(addr.port()),
|
||||||
|
);
|
||||||
|
} else if let Some(bunker_url) = &cli.options.config.bunker_url {
|
||||||
|
cli.options.bunker_url = Some(bunker_url.clone());
|
||||||
|
} else if cli.options.config.keyring_secret_key {
|
||||||
|
cli.options.secret_key = Some(
|
||||||
|
Cli::user_keypair(cli.options.secret_key)?
|
||||||
|
.secret_key()
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(cli)
|
Ok(cli)
|
||||||
}
|
}
|
||||||
|
|||||||
56
src/cli/options_state.rs
Normal file
56
src/cli/options_state.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// 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::{
|
||||||
|
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use nostr_browser_signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};
|
||||||
|
|
||||||
|
/// The default socket address used for the NIP-07 signer proxy, set to
|
||||||
|
/// localhost on port 51034.
|
||||||
|
pub const DEFAULT_NIP07_PROXY_ADDR: SocketAddr =
|
||||||
|
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 51034));
|
||||||
|
|
||||||
|
/// How long to wait for the proxy response (3 minutes).
|
||||||
|
pub const BROWSER_SIGNER_PROXY_TIMEOUT: Duration = Duration::from_secs(60 * 3);
|
||||||
|
|
||||||
|
|
||||||
|
/// Represents the state used for CLI options.
|
||||||
|
pub struct OptionsState {
|
||||||
|
/// The browser signer proxy, will be used if `--nip07` is enabled
|
||||||
|
pub browser_signer_proxy: BrowserSignerProxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OptionsState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
browser_signer_proxy: default_browser_signer_proxy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the default browser signer proxy
|
||||||
|
#[inline]
|
||||||
|
fn default_browser_signer_proxy() -> BrowserSignerProxy {
|
||||||
|
BrowserSignerProxy::new(
|
||||||
|
BrowserSignerProxyOptions::default()
|
||||||
|
.timeout(BROWSER_SIGNER_PROXY_TIMEOUT)
|
||||||
|
.ip_addr(DEFAULT_NIP07_PROXY_ADDR.ip())
|
||||||
|
.port(DEFAULT_NIP07_PROXY_ADDR.port()),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,14 +15,19 @@
|
|||||||
// 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::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
Kind,
|
Kind,
|
||||||
nips::nip19::{FromBech32, Nip19Coordinate},
|
nips::{
|
||||||
|
nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
||||||
|
nip46::NostrConnectURI,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
use super::CliConfig;
|
use super::CliConfig;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -69,3 +74,61 @@ pub fn parse_config_path(config_path: &str) -> N34Result<CliConfig> {
|
|||||||
|
|
||||||
CliConfig::load(path)
|
CliConfig::load(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a bunker URL and checks if it's a valid Nostr Connect URI.
|
||||||
|
/// Returns an error if the URL is not a valid bunker URL.
|
||||||
|
pub fn parse_bunker_url(bunker_url: &str) -> N34Result<NostrConnectURI> {
|
||||||
|
match NostrConnectURI::parse(bunker_url) {
|
||||||
|
Ok(url) if url.is_bunker() => Ok(url),
|
||||||
|
_ => Err(N34Error::NotBunkerUrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes a set of NIP-19 coordinates as a list of bech32 strings.
|
||||||
|
pub 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes a list of bech32 strings into a set of NIP-19 coordinates.
|
||||||
|
pub 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ser_bunker_url<S>(
|
||||||
|
bunker_url: &Option<NostrConnectURI>,
|
||||||
|
serializer: S,
|
||||||
|
) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
bunker_url
|
||||||
|
.as_ref()
|
||||||
|
.map(|u| u.to_string())
|
||||||
|
.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn de_bunker_url<'de, D>(deserializer: D) -> Result<Option<NostrConnectURI>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Option::<String>::deserialize(deserializer)?
|
||||||
|
.map(|u| parse_bunker_url(&u))
|
||||||
|
.transpose()
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,8 +14,17 @@
|
|||||||
// 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 nostr::{event::EventId, nips::nip19::Nip19Coordinate, types::RelayUrl};
|
||||||
|
|
||||||
use super::CliOptions;
|
use super::CliOptions;
|
||||||
use crate::error::N34Result;
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
ConfigError,
|
||||||
|
RepoRelaySet,
|
||||||
|
types::{NaddrOrSet, NostrEvent, RelayOrSet},
|
||||||
|
},
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
/// A trait defining the interface for command runners in the CLI.
|
/// A trait defining the interface for command runners in the CLI.
|
||||||
pub trait CommandRunner {
|
pub trait CommandRunner {
|
||||||
@@ -29,3 +38,189 @@ pub trait CommandRunner {
|
|||||||
/// Executes the command and returns a Result indicating success or failure.
|
/// Executes the command and returns a Result indicating success or failure.
|
||||||
fn run(self, options: CliOptions) -> impl Future<Output = N34Result<()>> + Send;
|
fn run(self, options: CliOptions) -> impl Future<Output = N34Result<()>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(VecNostrEventExt)]
|
||||||
|
impl Vec<NostrEvent> {
|
||||||
|
/// Extracts `EventId` from each `NostrEvent` and collects them into a
|
||||||
|
/// `Vec<EventId>`.
|
||||||
|
pub fn into_event_ids(self) -> Vec<EventId> {
|
||||||
|
self.into_iter().map(|e| e.event_id).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(NaddrOrSetVecExt)]
|
||||||
|
impl Vec<NaddrOrSet> {
|
||||||
|
/// Converts this vector of [`NaddrOrSet`] into a flat vector of
|
||||||
|
/// [`Nip19Coordinate`] 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
|
||||||
|
/// [`Nip19Coordinate`] 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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])
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,17 +19,19 @@ use std::str::FromStr;
|
|||||||
use nostr::{
|
use nostr::{
|
||||||
event::{EventId, Kind},
|
event::{EventId, Kind},
|
||||||
nips::{
|
nips::{
|
||||||
self,
|
|
||||||
nip01::Coordinate,
|
nip01::Coordinate,
|
||||||
|
nip05::{Nip05Address, Nip05Profile},
|
||||||
nip19::{self, FromBech32, Nip19Coordinate},
|
nip19::{self, FromBech32, Nip19Coordinate},
|
||||||
},
|
},
|
||||||
types::RelayUrl,
|
types::RelayUrl,
|
||||||
|
util::BoxedFuture,
|
||||||
};
|
};
|
||||||
|
use nostr_connect::client::AuthUrlHandler;
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
use super::{RepoRelaySetsExt, parsers};
|
use super::parsers;
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::RepoRelaySet,
|
cli::{RepoRelaySet, traits::RepoRelaySetsExt},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,6 +63,21 @@ pub struct NostrEvent {
|
|||||||
pub relays: Vec<RelayUrl>,
|
pub relays: Vec<RelayUrl>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EchoAuthUrl;
|
||||||
|
|
||||||
|
impl AuthUrlHandler for EchoAuthUrl {
|
||||||
|
fn on_auth_url(
|
||||||
|
&self,
|
||||||
|
auth_url: nostr::Url,
|
||||||
|
) -> BoxedFuture<'_, Result<(), Box<dyn std::error::Error>>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
println!("The bunker requires authentication. Please open this URL: {auth_url}");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NaddrOrSet {
|
impl NaddrOrSet {
|
||||||
/// Returns the naddr if `Naddr` or try to get the relays from the set.
|
/// 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.
|
/// Returns error if the set naddrs are empty or the set not found.
|
||||||
@@ -174,63 +191,28 @@ impl FromStr for NostrEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[easy_ext::ext(NaddrOrSetVecExt)]
|
|
||||||
impl Vec<NaddrOrSet> {
|
|
||||||
/// Converts this vector of [`NaddrOrSet`] into a flat vector of
|
|
||||||
/// [`Nip19Coordinate`] 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
|
|
||||||
/// [`Nip19Coordinate`] 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<NaddrOrSet, String> {
|
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<NaddrOrSet, String> {
|
||||||
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
|
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
|
||||||
|
|
||||||
let nip5_profile = tokio::task::block_in_place(|| {
|
let nip5_address =
|
||||||
|
Nip05Address::parse(&format!("{username}@{domain}")).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
let nip5_json = tokio::task::block_in_place(|| {
|
||||||
Handle::current().block_on(async {
|
Handle::current().block_on(async {
|
||||||
nips::nip05::profile(format!("{username}@{domain}"), None)
|
reqwest::get(nip5_address.url().as_str())
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string())?
|
||||||
|
.text()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.to_string())
|
.map_err(|err| err.to_string())
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(NaddrOrSet::Naddr(
|
let nip5_profile =
|
||||||
Nip19Coordinate::new(
|
Nip05Profile::from_raw_json(&nip5_address, &nip5_json).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
Ok(NaddrOrSet::Naddr(Nip19Coordinate::new(
|
||||||
Coordinate::new(Kind::GitRepoAnnouncement, nip5_profile.public_key).identifier(repo_id),
|
Coordinate::new(Kind::GitRepoAnnouncement, nip5_profile.public_key).identifier(repo_id),
|
||||||
nip5_profile.relays,
|
nip5_profile.relays,
|
||||||
)
|
)))
|
||||||
.expect("The relays is `RelayUrl`"),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/error.rs
25
src/error.rs
@@ -14,9 +14,12 @@
|
|||||||
// 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::process::ExitCode;
|
use std::{net::AddrParseError, process::ExitCode};
|
||||||
|
|
||||||
use nostr::event::{Kind, builder::Error as EventBuilderError};
|
use nostr::{
|
||||||
|
event::{Kind, builder::Error as EventBuilderError},
|
||||||
|
signer::SignerError,
|
||||||
|
};
|
||||||
use nostr_sdk::client::Error as ClientError;
|
use nostr_sdk::client::Error as ClientError;
|
||||||
|
|
||||||
use crate::cli::ConfigError;
|
use crate::cli::ConfigError;
|
||||||
@@ -42,6 +45,14 @@ 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("Signer Error: {0}")]
|
||||||
|
SignerError(#[from] SignerError),
|
||||||
|
#[error("Invalid Browser Signer Proxy Address: {0}")]
|
||||||
|
Addr(#[from] AddrParseError),
|
||||||
|
#[error("Browser Signer Proxy Error: {0}")]
|
||||||
|
BrowserSignerProxy(#[from] nostr_browser_signer_proxy::Error),
|
||||||
|
#[error("Keyring error: {0}")]
|
||||||
|
Keyring(#[from] nostr_keyring::Error),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Config(#[from] ConfigError),
|
Config(#[from] ConfigError),
|
||||||
#[error("No editor specified in the `EDITOR` environment variable")]
|
#[error("No editor specified in the `EDITOR` environment variable")]
|
||||||
@@ -85,7 +96,8 @@ pub enum N34Error {
|
|||||||
#[error("One naddr is required for this command")]
|
#[error("One naddr is required for this command")]
|
||||||
EmptyNaddrs,
|
EmptyNaddrs,
|
||||||
#[error(
|
#[error(
|
||||||
"This command requires a signer to sign events. Use `--secret-key` to provide a signer"
|
"This command requires a signer to sign events. Use `--secret-key`, `--nip07` or \
|
||||||
|
`--bunker-url` to provide a signer"
|
||||||
)]
|
)]
|
||||||
SignerRequired,
|
SignerRequired,
|
||||||
#[error(
|
#[error(
|
||||||
@@ -124,6 +136,13 @@ pub enum N34Error {
|
|||||||
RevisionRootNotFound,
|
RevisionRootNotFound,
|
||||||
#[error("Invalid status for the issue/patch: {0}")]
|
#[error("Invalid status for the issue/patch: {0}")]
|
||||||
InvalidStatus(String),
|
InvalidStatus(String),
|
||||||
|
#[error("Not valid bunker URL")]
|
||||||
|
NotBunkerUrl,
|
||||||
|
#[error(
|
||||||
|
"No secret key found in the keyring. Please use the secret key at least once while \
|
||||||
|
keyring is enabled to store it"
|
||||||
|
)]
|
||||||
|
SecretKeyKeyringWithoutEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl N34Error {
|
impl N34Error {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ use nostr::{
|
|||||||
parser::NostrParser,
|
parser::NostrParser,
|
||||||
types::RelayUrl,
|
types::RelayUrl,
|
||||||
};
|
};
|
||||||
use nostr_sdk::Client;
|
use nostr_sdk::{Client, ClientOptions};
|
||||||
use traits::TokenUtils;
|
use traits::TokenUtils;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -62,9 +62,10 @@ pub struct ContentDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A client for interacting with the Nostr relays
|
/// A client for interacting with the Nostr relays
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct NostrClient {
|
pub struct NostrClient {
|
||||||
/// The underlying Nostr client implementation
|
/// The underlying Nostr client implementation
|
||||||
client: Client,
|
pub client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentDetails {
|
impl ContentDetails {
|
||||||
@@ -110,9 +111,10 @@ 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, relays: &[RelayUrl]) -> Self {
|
pub async fn init(options: &CliOptions, relays: &[RelayUrl]) -> Self {
|
||||||
let mut client_builder = Client::builder();
|
let mut client_builder =
|
||||||
|
Client::builder().opts(ClientOptions::new().verify_subscriptions(true));
|
||||||
|
|
||||||
if let Some(signer) = options.signer() {
|
if let Ok(Some(signer)) = options.signer().await {
|
||||||
client_builder = client_builder.signer(signer);
|
client_builder = client_builder.signer(signer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +124,16 @@ impl NostrClient {
|
|||||||
client
|
client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//// Returns the users public key
|
||||||
|
pub async fn pubkey(&self) -> N34Result<PublicKey> {
|
||||||
|
self.client
|
||||||
|
.signer()
|
||||||
|
.await?
|
||||||
|
.get_public_key()
|
||||||
|
.await
|
||||||
|
.map_err(N34Error::SignerError)
|
||||||
|
}
|
||||||
|
|
||||||
/// Add relays and connect to them
|
/// Add relays and connect to them
|
||||||
pub async fn add_relays(&self, relays: &[RelayUrl]) {
|
pub async fn add_relays(&self, relays: &[RelayUrl]) {
|
||||||
if relays.is_empty() {
|
if relays.is_empty() {
|
||||||
@@ -323,7 +335,6 @@ impl NostrClient {
|
|||||||
return Ok(PatchStatus::Closed);
|
return Ok(PatchStatus::Closed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(root_status)
|
Ok(root_status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ use nostr::{
|
|||||||
parser::Token,
|
parser::Token,
|
||||||
types::{RelayUrl, Url},
|
types::{RelayUrl, Url},
|
||||||
};
|
};
|
||||||
|
use nostr_keyring::KeyringError;
|
||||||
|
|
||||||
use crate::cli::issue::ISSUE_ALT_PREFIX;
|
use crate::cli::issue::ISSUE_ALT_PREFIX;
|
||||||
|
use crate::cli::patch::{
|
||||||
|
LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT,
|
||||||
|
REVISION_ROOT_HASHTAG_CONTENT,
|
||||||
|
ROOT_HASHTAG_CONTENT,
|
||||||
|
};
|
||||||
use crate::error::{N34Error, N34Result};
|
use crate::error::{N34Error, N34Result};
|
||||||
|
|
||||||
|
|
||||||
@@ -245,17 +251,20 @@ impl Event {
|
|||||||
&& self
|
&& self
|
||||||
.tags
|
.tags
|
||||||
.filter(TagKind::t())
|
.filter(TagKind::t())
|
||||||
.any(|t| t.content() == Some("root"))
|
.any(|t| t.content() == Some(ROOT_HASHTAG_CONTENT))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the patch is patch-revision or not
|
/// Returns whether the patch is patch-revision or not
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_revision_patch(&self) -> bool {
|
pub fn is_revision_patch(&self) -> bool {
|
||||||
self.kind == Kind::GitPatch
|
self.kind == Kind::GitPatch
|
||||||
&& self
|
&& self.tags.filter(TagKind::t()).any(|t| {
|
||||||
.tags
|
[
|
||||||
.filter(TagKind::t())
|
Some(REVISION_ROOT_HASHTAG_CONTENT),
|
||||||
.any(|t| t.content() == Some("root-revision"))
|
Some(LEGACY_NGIT_REVISION_ROOT_HASHTAG_CONTENT),
|
||||||
|
]
|
||||||
|
.contains(&t.content())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the root patch ID from a patch-revision event by finding the `e`
|
/// Gets the root patch ID from a patch-revision event by finding the `e`
|
||||||
@@ -300,3 +309,12 @@ impl Event {
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(NostrKeyringErrorUtils)]
|
||||||
|
impl nostr_keyring::Error {
|
||||||
|
/// Checks if the error indicates a missing keyring entry.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_keyring_no_entry(&self) -> bool {
|
||||||
|
matches!(self, nostr_keyring::Error::Keyring(KeyringError::NoEntry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -151,9 +151,8 @@ pub fn repo_naddr(
|
|||||||
) -> N34Result<String> {
|
) -> N34Result<String> {
|
||||||
Nip19Coordinate::new(
|
Nip19Coordinate::new(
|
||||||
Coordinate::new(Kind::GitRepoAnnouncement, pubk).identifier(repo_id),
|
Coordinate::new(Kind::GitRepoAnnouncement, pubk).identifier(repo_id),
|
||||||
dedup(relays.iter().cloned()).iter().take(3),
|
dedup(relays.iter().cloned()).into_iter().take(3),
|
||||||
)
|
)
|
||||||
.expect("Valid relays")
|
|
||||||
.to_bech32()
|
.to_bech32()
|
||||||
.map_err(N34Error::from)
|
.map_err(N34Error::from)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user