From 003200b9ce962496484cab4b079247a8da72a5c5 Mon Sep 17 00:00:00 2001 From: Eric T Date: Tue, 19 Aug 2025 22:37:48 +0100 Subject: [PATCH 001/105] Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 711c6e4f3..831673431 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,6 @@ nostr-java implements the Nostr protocol. A complete index of current Nostr Impl ## Development Guidelines - Run `mvn -q verify` from the repository root before committing. -- Use `rg` for code searches instead of `ls -R` or `grep -R`. - PR titles and commit messages must follow the `type: description` format. - Allowed types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`, `build`, `perf`, `style`. - The description must be a concise verb + object phrase (e.g., `refactor: update auth middleware to async`). From 9372d84dab83bf40f782f5c12b461a4b85cfe177 Mon Sep 17 00:00:00 2001 From: Eric T Date: Wed, 20 Aug 2025 23:56:12 +0100 Subject: [PATCH 002/105] docs: clarify PR branch --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 831673431..76b3267f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,7 @@ nostr-java implements the Nostr protocol. A complete index of current Nostr Impl ## Development Guidelines - Run `mvn -q verify` from the repository root before committing. +- Submit pull requests against the `develop` branch. - PR titles and commit messages must follow the `type: description` format. - Allowed types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`, `build`, `perf`, `style`. - The description must be a concise verb + object phrase (e.g., `refactor: update auth middleware to async`). From c8217fabb01b7d1212ae1137738c7cb5dd4d2f33 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 00:05:00 +0100 Subject: [PATCH 003/105] docs: reinforce PR submission guidelines --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 3d0450a94..d970d91d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,7 @@ The URL format for the NIPs is https://github.com/nostr-protocol/nips/blob/maste ## Pull Requests -- Use the pull request template at `.github/pull_request_template.md` and fill out all sections. +- Always follow the PR submission guidelines and use the pull request template at `.github/pull_request_template.md`, filling out all sections. - Summarize the changes made and describe how they were tested. - Include any limitations or known issues in the description. - Add a "Network Access" section summarizing blocked domains if network requests were denied. From af49981856db084306bb6fdd4133f65d8d7cd58b Mon Sep 17 00:00:00 2001 From: erict875 Date: Thu, 21 Aug 2025 02:00:38 +0100 Subject: [PATCH 004/105] remove GitHub action for assigning Copilot reviewer --- .github/workflows/assign-copilot-review.yml | 24 --------------------- 1 file changed, 24 deletions(-) delete mode 100644 .github/workflows/assign-copilot-review.yml diff --git a/.github/workflows/assign-copilot-review.yml b/.github/workflows/assign-copilot-review.yml deleted file mode 100644 index e17276e4a..000000000 --- a/.github/workflows/assign-copilot-review.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Assign Copilot Reviewer - -on: - pull_request_target: - branches: [develop] - types: [opened, reopened, ready_for_review] - -permissions: - pull-requests: write - -jobs: - assign: - runs-on: ubuntu-latest - steps: - - name: Request review from GitHub Copilot - uses: actions/github-script@v7 - with: - script: | - await github.rest.pulls.requestReviewers({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.pull_request.number, - reviewers: ['github-copilot'] - }); From 152b6c1caa942871065d887e691b7dc77390aa60 Mon Sep 17 00:00:00 2001 From: erict875 Date: Thu, 21 Aug 2025 03:38:57 +0100 Subject: [PATCH 005/105] docs: update AGENTS.md with additional guidelines for new features and PR standards --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index d970d91d3..7b9e1b3cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,6 +120,7 @@ The URL format for the NIPs is https://github.com/nostr-protocol/nips/blob/maste - Include any limitations or known issues in the description. - Add a "Network Access" section summarizing blocked domains if network requests were denied. - Ensure all new features, modules, or dependencies are properly documented in the `README.md` file. + ## PR Quality Gate - PR summaries must reference modified files with file path citations (e.g. `F:path/to/file.java†L1-L2`). @@ -131,3 +132,4 @@ The URL format for the NIPs is https://github.com/nostr-protocol/nips/blob/maste - When TODOs or placeholders remain, include a Notes section. - Review AI-generated changes with developer expertise, ensuring you understand why the code works and that it remains resilient, scalable, and secure. - Use `rg` for search instead of `ls -R` or `grep -R`. +- Ensure all new features are compliant with the protocol specification provided above. From c2bd047fc346a1ec11a449ddd97a52b30fc39b3a Mon Sep 17 00:00:00 2001 From: erict875 Date: Thu, 21 Aug 2025 03:46:32 +0100 Subject: [PATCH 006/105] docs: update AGENTS.md to include guideline for describing test methods --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 7b9e1b3cd..9846c17b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,6 +120,7 @@ The URL format for the NIPs is https://github.com/nostr-protocol/nips/blob/maste - Include any limitations or known issues in the description. - Add a "Network Access" section summarizing blocked domains if network requests were denied. - Ensure all new features, modules, or dependencies are properly documented in the `README.md` file. +- Add a comment on top of every test method to describe the test in plain English. ## PR Quality Gate From 56995d91a4f1529488480048e2fb64c344be2287 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 11:01:50 +0100 Subject: [PATCH 007/105] ci: show build log errors --- .github/workflows/ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de887e8c7..e5cd07e69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,14 @@ jobs: distribution: 'temurin' cache: 'maven' - name: Build with Maven - run: ./mvnw -q verify + run: ./mvnw -q verify |& tee build.log + - name: Show build log + if: failure() + run: | + echo "Build error" + grep '^\[ERROR\]' build.log || true + echo "Build log tail" + tail -n 200 build.log - name: Upload surefire reports if: always() uses: actions/upload-artifact@v4 @@ -41,4 +48,4 @@ jobs: if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} From 2bec13feb916341e17fe3717d8fcc9228d3e346b Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 11:06:17 +0100 Subject: [PATCH 008/105] ci: surface build errors in failure issues --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de887e8c7..38908808e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,10 +5,14 @@ on: branches: [main, develop] pull_request: branches: [main, develop] + workflow_dispatch: jobs: build: runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - uses: actions/checkout@v5 - uses: actions/setup-java@v4 @@ -17,7 +21,10 @@ jobs: distribution: 'temurin' cache: 'maven' - name: Build with Maven - run: ./mvnw -q verify + id: build + run: | + set -o pipefail + ./mvnw -q verify | tee build.log - name: Upload surefire reports if: always() uses: actions/upload-artifact@v4 @@ -41,4 +48,22 @@ jobs: if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} + - name: Create issue on failure + if: failure() && github.ref == 'refs/heads/develop' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const file = fs.readFileSync('build.log', 'utf8').split('\\n'); + const errors = file.filter(line => line.startsWith('[ERROR]')) + .slice(-20) + .join('\\n'); + const log = file.slice(-50).join('\\n'); + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `CI build failed for ${context.sha.slice(0,7)}`, + body: `Build failed for commit ${context.sha} in workflow run ${context.runId}.\\n\\nBuild error:\\n\\n\u0060\u0060\u0060\\n${errors}\\n\u0060\u0060\u0060\\n\\nLast lines of build log:\\n\\n\u0060\u0060\u0060\\n${log}\\n\u0060\u0060\u0060`, + labels: ['ci'] + }); From 1d688bd66e53e7346b1d66fd8a8d239990fdecee Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 14:58:08 +0100 Subject: [PATCH 009/105] ci: add codex label step --- .github/workflows/issue-labeler.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml index bf5be697c..658b7e7a3 100644 --- a/.github/workflows/issue-labeler.yml +++ b/.github/workflows/issue-labeler.yml @@ -17,3 +17,13 @@ jobs: configuration-path: .github/labeler.yml include-title: 1 repo-token: ${{ github.token }} + - if: github.actor == 'github-actions[bot]' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['codex'] + }) From 1c069d8b074b9ddb8596cca8e739bdc6a9ff7c37 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 15:15:39 +0100 Subject: [PATCH 010/105] docs: list NIP titles --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ff133f683..c889a4359 100644 --- a/README.md +++ b/README.md @@ -21,27 +21,27 @@ Examples are located in the [`nostr-java-examples`](./nostr-java-examples) modul ## Supported NIPs The API currently implements the following [NIPs](https://github.com/nostr-protocol/nips): -- [NIP-1](https://github.com/nostr-protocol/nips/blob/master/01.md) -- [NIP-2](https://github.com/nostr-protocol/nips/blob/master/02.md) -- [NIP-3](https://github.com/nostr-protocol/nips/blob/master/03.md) -- [NIP-4](https://github.com/nostr-protocol/nips/blob/master/04.md) -- [NIP-5](https://github.com/nostr-protocol/nips/blob/master/05.md) -- [NIP-8](https://github.com/nostr-protocol/nips/blob/master/08.md) -- [NIP-9](https://github.com/nostr-protocol/nips/blob/master/09.md) -- [NIP-12](https://github.com/nostr-protocol/nips/blob/master/12.md) -- [NIP-14](https://github.com/nostr-protocol/nips/blob/master/14.md) -- [NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md) -- [NIP-20](https://github.com/nostr-protocol/nips/blob/master/20.md) -- [NIP-23](https://github.com/nostr-protocol/nips/blob/master/23.md) -- [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) -- [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md) -- [NIP-30](https://github.com/nostr-protocol/nips/blob/master/30.md) -- [NIP-32](https://github.com/nostr-protocol/nips/blob/master/32.md) -- [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) -- [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) -- [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) -- [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md) -- [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) -- [NIP-60](https://github.com/nostr-protocol/nips/blob/master/60.md) -- [NIP-61](https://github.com/nostr-protocol/nips/blob/master/61.md) -- [NIP-99](https://github.com/nostr-protocol/nips/blob/master/99.md) +- [NIP-1](https://github.com/nostr-protocol/nips/blob/master/01.md) - Basic protocol flow description +- [NIP-2](https://github.com/nostr-protocol/nips/blob/master/02.md) - Follow List +- [NIP-3](https://github.com/nostr-protocol/nips/blob/master/03.md) - OpenTimestamps Attestations for Events +- [NIP-4](https://github.com/nostr-protocol/nips/blob/master/04.md) - Encrypted Direct Message +- [NIP-5](https://github.com/nostr-protocol/nips/blob/master/05.md) - Mapping Nostr keys to DNS-based internet identifiers +- [NIP-8](https://github.com/nostr-protocol/nips/blob/master/08.md) - Handling Mentions +- [NIP-9](https://github.com/nostr-protocol/nips/blob/master/09.md) - Event Deletion Request +- [NIP-12](https://github.com/nostr-protocol/nips/blob/master/12.md) - Generic Tag Queries +- [NIP-14](https://github.com/nostr-protocol/nips/blob/master/14.md) - Subject tag in Text events +- [NIP-15](https://github.com/nostr-protocol/nips/blob/master/15.md) - Nostr Marketplace +- [NIP-20](https://github.com/nostr-protocol/nips/blob/master/20.md) - Command Results +- [NIP-23](https://github.com/nostr-protocol/nips/blob/master/23.md) - Long-form Content +- [NIP-25](https://github.com/nostr-protocol/nips/blob/master/25.md) - Reactions +- [NIP-28](https://github.com/nostr-protocol/nips/blob/master/28.md) - Public Chat +- [NIP-30](https://github.com/nostr-protocol/nips/blob/master/30.md) - Custom Emoji +- [NIP-32](https://github.com/nostr-protocol/nips/blob/master/32.md) - Labeling +- [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) - Expiration Timestamp +- [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) - Authentication of clients to relays +- [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) - Encrypted Payloads (Versioned) +- [NIP-46](https://github.com/nostr-protocol/nips/blob/master/46.md) - Nostr Remote Signing +- [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) - Lightning Zaps +- [NIP-60](https://github.com/nostr-protocol/nips/blob/master/60.md) - Cashu Wallets +- [NIP-61](https://github.com/nostr-protocol/nips/blob/master/61.md) - Nutzaps +- [NIP-99](https://github.com/nostr-protocol/nips/blob/master/99.md) - Classified Listings From 05d7a916ac19b20316a1bf2cd0c003f7d49e8b1d Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 15:59:57 +0100 Subject: [PATCH 011/105] ci: simplify PR title regex --- .github/workflows/pr-quality-gate.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-quality-gate.yml b/.github/workflows/pr-quality-gate.yml index a13329f6a..4d0ddd1ec 100644 --- a/.github/workflows/pr-quality-gate.yml +++ b/.github/workflows/pr-quality-gate.yml @@ -48,9 +48,9 @@ jobs: // 2) Title and commits follow type: description (verb + object) const title = prData.title.trim(); const types = ['feat','fix','docs','refactor','test','chore','ci','build','perf','style']; - const naming = `^(${types.join('|')}):\\s+[A-Z][^\\s]*\\s+.+`; - const titleOK = new RegExp(naming).test(title); - const commitsOK = commits.every(c => new RegExp(naming).test(c.commit.message.split('\\n')[0])); + const naming = new RegExp(`^(${types.join('|')}):`); + const titleOK = naming.test(title); + const commitsOK = commits.every(c => naming.test(c.commit.message.split('\\n')[0])); // 3) Description “why now?” + links to issue const body = (prData.body || '').trim(); const hasIssueLink = /#[0-9]+|https?:\/\/github\.com\/.+\/issues\/[0-9]+/i.test(body); From b51d5c56c4577a32a8352fc7228120833cbf7fd2 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 16:05:56 +0100 Subject: [PATCH 012/105] feat: harden NIP-05 validator HTTP handling --- .../src/main/java/nostr/api/NIP05.java | 6 +- .../util/http/DefaultHttpClientProvider.java | 18 ++ .../nostr/util/http/HttpClientProvider.java | 19 ++ .../nostr/util/validator/Nip05Validator.java | 150 ++++++++----- .../util/validator/Nip05ValidatorTest.java | 199 +++++++++++++++++- 5 files changed, 328 insertions(+), 64 deletions(-) create mode 100644 nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java create mode 100644 nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java diff --git a/nostr-java-api/src/main/java/nostr/api/NIP05.java b/nostr-java-api/src/main/java/nostr/api/NIP05.java index cd110b664..530d62232 100644 --- a/nostr-java-api/src/main/java/nostr/api/NIP05.java +++ b/nostr-java-api/src/main/java/nostr/api/NIP05.java @@ -44,7 +44,11 @@ public NIP05 createInternetIdentifierMetadataEvent(@NonNull UserProfile profile) private String getContent(UserProfile profile) { try { - String jsonString = MAPPER_BLACKBIRD.writeValueAsString(new Nip05Validator(profile.getNip05(), profile.getPublicKey().toString())); + String jsonString = MAPPER_BLACKBIRD.writeValueAsString( + Nip05Validator.builder() + .nip05(profile.getNip05()) + .publicKey(profile.getPublicKey().toString()) + .build()); return escapeJsonString(jsonString); } catch (JsonProcessingException ex) { throw new RuntimeException(ex); diff --git a/nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java b/nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java new file mode 100644 index 000000000..0f087a5af --- /dev/null +++ b/nostr-java-util/src/main/java/nostr/util/http/DefaultHttpClientProvider.java @@ -0,0 +1,18 @@ +package nostr.util.http; + +import java.net.http.HttpClient; +import java.time.Duration; + +/** + * Default implementation of {@link HttpClientProvider} using Java's HTTP client. + */ +public class DefaultHttpClientProvider implements HttpClientProvider { + + @Override + public HttpClient create(Duration connectTimeout) { + return HttpClient.newBuilder() + .connectTimeout(connectTimeout) + .build(); + } +} + diff --git a/nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java b/nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java new file mode 100644 index 000000000..ede1681ef --- /dev/null +++ b/nostr-java-util/src/main/java/nostr/util/http/HttpClientProvider.java @@ -0,0 +1,19 @@ +package nostr.util.http; + +import java.net.http.HttpClient; +import java.time.Duration; + +/** + * Provides {@link HttpClient} instances with configurable timeouts. + */ +public interface HttpClientProvider { + + /** + * Create a new {@link HttpClient} with the given connect timeout. + * + * @param connectTimeout the connection timeout + * @return configured HttpClient instance + */ + HttpClient create(Duration connectTimeout); +} + diff --git a/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java b/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java index 22762cb1e..d121c2904 100644 --- a/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java +++ b/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java @@ -3,70 +3,113 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.module.blackbird.BlackbirdModule; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import com.fasterxml.jackson.annotation.JsonIgnore; import nostr.util.NostrException; +import nostr.util.http.DefaultHttpClientProvider; +import nostr.util.http.HttpClientProvider; import java.io.IOException; +import java.net.IDN; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Locale; import java.util.Map; +import java.util.regex.Pattern; /** + * Validator for NIP-05 identifiers. * * @author squirrel */ @Builder -@RequiredArgsConstructor @Data @Slf4j public class Nip05Validator { private final String nip05; private final String publicKey; - - private static final String LOCAL_PART_PATTERN = "^[a-zA-Z0-9-_\\.]+$"; - - // TODO: refactor + @Builder.Default + @JsonIgnore + private final Duration connectTimeout = Duration.ofSeconds(5); + @Builder.Default + @JsonIgnore + private final Duration requestTimeout = Duration.ofSeconds(5); + @Builder.Default + @JsonIgnore + private final HttpClientProvider httpClientProvider = new DefaultHttpClientProvider(); + + private static final Pattern LOCAL_PART_PATTERN = Pattern.compile("^[a-zA-Z0-9-_\\.]+$"); + private static final Pattern DOMAIN_PATTERN = Pattern.compile("^[A-Za-z0-9.-]+(:\\d{1,5})?$"); + private static final ObjectMapper MAPPER_BLACKBIRD = JsonMapper.builder().addModule(new BlackbirdModule()).build(); + + /** + * Validate the nip05 identifier by checking the public key + * registered on the remote server. + * + * @throws NostrException if validation fails + */ public void validate() throws NostrException { - if (this.nip05 != null) { - var splited = nip05.split("@"); - var localPart = splited[0]; - var domain = splited[1]; + if (this.nip05 == null) { + return; + } + String[] split = nip05.trim().split("@"); + if (split.length != 2) { + throw new NostrException("Invalid nip05 identifier format."); + } + String localPart = split[0].trim(); + String domainPart = split[1].trim(); - if (!localPart.matches(LOCAL_PART_PATTERN)) { - throw new NostrException("Invalid syntax in nip05 attribute."); - } + if (!LOCAL_PART_PATTERN.matcher(localPart).matches()) { + throw new NostrException("Invalid syntax in nip05 attribute."); + } + if (!DOMAIN_PATTERN.matcher(domainPart).matches()) { + throw new NostrException("Invalid domain syntax in nip05 attribute."); + } - // Verify the public key + localPart = localPart.toLowerCase(Locale.ROOT); + String host; + int port = -1; + String[] hostPort = domainPart.split(":", 2); + host = IDN.toASCII(hostPort[0].toLowerCase(Locale.ROOT)); + if (hostPort.length == 2) { try { - log.debug("Validating {}@{}", localPart, domain); - validatePublicKey(domain, localPart); - } catch (URISyntaxException ex) { - log.error("Validation error", ex); - throw new NostrException(ex); + port = Integer.parseInt(hostPort[1]); + } catch (NumberFormatException ex) { + throw new NostrException("Invalid port in domain.", ex); + } + if (port < 0 || port > 65535) { + throw new NostrException("Invalid port in domain."); } } + + validatePublicKey(host, port, localPart); } - // TODO: refactor - private void validatePublicKey(String domain, String localPart) throws NostrException, URISyntaxException { + private void validatePublicKey(String host, int port, String localPart) throws NostrException { + HttpClient client = httpClientProvider.create(connectTimeout); - String strUrl = "https:///.well-known/nostr.json?name=" - .replace("", domain) - .replace("", localPart); + URI uri; + try { + uri = new URI("https", null, host, port, "/.well-known/nostr.json", + "name=" + URLEncoder.encode(localPart, StandardCharsets.UTF_8), null); + } catch (URISyntaxException ex) { + log.error("Validation error", ex); + throw new NostrException("Invalid URI for host " + host + ": " + ex.getMessage(), ex); + } - HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() - .uri(new URI(strUrl)) + .uri(uri) .GET() + .timeout(requestTimeout) .build(); HttpResponse response; @@ -77,47 +120,42 @@ private void validatePublicKey(String domain, String localPart) throws NostrExce Thread.currentThread().interrupt(); } log.error("HTTP request error", ex); - throw new NostrException(String.format("Failed to connect to %s: %s", strUrl, ex.getMessage())); + throw new NostrException(String.format("Error querying %s: %s", uri, ex.getMessage()), ex); } - if (response.statusCode() == 200) { - StringBuilder content = new StringBuilder(response.body()); + if (response.statusCode() != 200) { + throw new NostrException(String.format("Unexpected HTTP status %d from %s", response.statusCode(), uri)); + } - String pubKey = getPublicKey(content, localPart); - log.debug("Public key for {} returned by the server: [{}]", localPart, pubKey); + String pubKey = getPublicKey(response.body(), localPart); + log.debug("Public key for {} returned by the server: [{}]", localPart, pubKey); - if (pubKey != null && !pubKey.equals(publicKey)) { - throw new NostrException(String.format("Public key mismatch. Expected %s - Received: %s", publicKey, pubKey)); - } - return; + if (pubKey == null) { + throw new NostrException(String.format("No NIP-05 record for '%s' at %s", localPart, uri)); + } + if (!pubKey.equals(publicKey)) { + throw new NostrException(String.format("Public key mismatch. Expected %s - Received: %s", publicKey, pubKey)); } - - throw new NostrException(String.format("Failed to connect to %s. Status: %d", strUrl, response.statusCode())); } - @SneakyThrows - private String getPublicKey(StringBuilder content, String localPart) { - - ObjectMapper MAPPER_BLACKBIRD = JsonMapper.builder().addModule(new BlackbirdModule()).build(); - Nip05Content nip05Content = MAPPER_BLACKBIRD.readValue(content.toString(), Nip05Content.class); + private String getPublicKey(String content, String localPart) throws NostrException { + Nip05Content nip05Content; + try { + nip05Content = MAPPER_BLACKBIRD.readValue(content, Nip05Content.class); + } catch (IOException ex) { + throw new NostrException("Invalid NIP-05 response: " + ex.getMessage(), ex); + } - // Access the decoded data Map names = nip05Content.getNames(); - for (Map.Entry entry : names.entrySet()) { - String name = entry.getKey(); - String hash = entry.getValue(); - if (name.equals(localPart)) { - return hash; - } + if (names == null) { + return null; } - return null; + return names.get(localPart); } @Data - @AllArgsConstructor public static final class Nip05Obj { - private String name; - private String nip05; + private final String name; + private final String nip05; } - } diff --git a/nostr-java-util/src/test/java/nostr/util/validator/Nip05ValidatorTest.java b/nostr-java-util/src/test/java/nostr/util/validator/Nip05ValidatorTest.java index 6599c84e3..ee619e476 100644 --- a/nostr-java-util/src/test/java/nostr/util/validator/Nip05ValidatorTest.java +++ b/nostr-java-util/src/test/java/nostr/util/validator/Nip05ValidatorTest.java @@ -1,16 +1,26 @@ package nostr.util.validator; import nostr.util.NostrException; +import nostr.util.http.HttpClientProvider; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.lang.reflect.Method; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; public class Nip05ValidatorTest { + /* Ensures validation fails for illegal characters in the local-part. */ @Test public void testInvalidLocalPart() { Nip05Validator validator = Nip05Validator.builder() @@ -20,8 +30,9 @@ public void testInvalidLocalPart() { assertThrows(NostrException.class, validator::validate); } + /* Ensures domains containing schemes are rejected. */ @Test - public void testUnknownDomain() { + public void testInvalidDomain() { Nip05Validator validator = Nip05Validator.builder() .nip05("user@http://example.com") .publicKey("pub") @@ -29,18 +40,192 @@ public void testUnknownDomain() { assertThrows(NostrException.class, validator::validate); } + /* Validates that a matching public key passes successfully. */ + @Test + public void testSuccessfulValidation() { + HttpResponse resp = new MockHttpResponse(200, "{\"names\":{\"alice\":\"pub\"}}"); + HttpClient client = new MockHttpClient(resp); + Nip05Validator validator = Nip05Validator.builder() + .nip05("alice@example.com") + .publicKey("pub") + .httpClientProvider(new FixedHttpClientProvider(client)) + .build(); + assertDoesNotThrow(validator::validate); + } + + /* Detects when the returned public key does not match the expected one. */ + @Test + public void testMismatchedPublicKey() { + HttpResponse resp = new MockHttpResponse(200, "{\"names\":{\"alice\":\"wrong\"}}"); + HttpClient client = new MockHttpClient(resp); + Nip05Validator validator = Nip05Validator.builder() + .nip05("alice@example.com") + .publicKey("pub") + .httpClientProvider(new FixedHttpClientProvider(client)) + .build(); + assertThrows(NostrException.class, validator::validate); + } + + /* Propagates network failures with descriptive messages. */ + @Test + public void testNetworkFailure() { + HttpClient client = new MockHttpClient(new IOException("boom")); + Nip05Validator validator = Nip05Validator.builder() + .nip05("alice@example.com") + .publicKey("pub") + .httpClientProvider(new FixedHttpClientProvider(client)) + .build(); + assertThrows(NostrException.class, validator::validate); + } + + /* Verifies JSON parsing logic of the getPublicKey helper. */ @Test public void testGetPublicKeyViaReflection() throws Exception { Nip05Validator validator = Nip05Validator.builder() .nip05("user@example.com") .publicKey("pub") .build(); - Method m = Nip05Validator.class.getDeclaredMethod("getPublicKey", StringBuilder.class, String.class); + Method m = Nip05Validator.class.getDeclaredMethod("getPublicKey", String.class, String.class); m.setAccessible(true); String json = "{\"names\":{\"alice\":\"abc\"}}"; - String result = (String) m.invoke(validator, new StringBuilder(json), "alice"); + String result = (String) m.invoke(validator, json, "alice"); assertEquals("abc", result); - String missing = (String) m.invoke(validator, new StringBuilder(json), "bob"); + String missing = (String) m.invoke(validator, json, "bob"); assertNull(missing); } + + private static class FixedHttpClientProvider implements HttpClientProvider { + private final HttpClient client; + FixedHttpClientProvider(HttpClient client) { this.client = client; } + @Override + public HttpClient create(Duration connectTimeout) { return client; } + } + + private static class MockHttpClient extends HttpClient { + private final HttpResponse response; + private final IOException exception; + + MockHttpClient(HttpResponse response) { + this.response = response; + this.exception = null; + } + + MockHttpClient(IOException exception) { + this.response = null; + this.exception = exception; + } + + @Override + public Optional cookieHandler() { + return Optional.empty(); + } + + @Override + public Optional connectTimeout() { + return Optional.empty(); + } + + @Override + public Redirect followRedirects() { + return Redirect.NEVER; + } + + @Override + public Optional proxy() { + return Optional.empty(); + } + + @Override + public javax.net.ssl.SSLContext sslContext() { + return null; + } + + @Override + public javax.net.ssl.SSLParameters sslParameters() { + return null; + } + + @Override + public Optional authenticator() { + return Optional.empty(); + } + + @Override + public Optional executor() { + return Optional.empty(); + } + + @Override + public HttpClient.Version version() { + return HttpClient.Version.HTTP_1_1; + } + + @Override + public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) throws IOException { + if (exception != null) { + throw exception; + } + return (HttpResponse) response; + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { + return CompletableFuture.failedFuture(new UnsupportedOperationException()); + } + + @Override + public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) { + return CompletableFuture.failedFuture(new UnsupportedOperationException()); + } + } + + private static class MockHttpResponse implements HttpResponse { + private final int statusCode; + private final String body; + + MockHttpResponse(int statusCode, String body) { + this.statusCode = statusCode; + this.body = body; + } + + @Override + public int statusCode() { + return statusCode; + } + + @Override + public String body() { + return body; + } + + @Override + public HttpRequest request() { + return HttpRequest.newBuilder().uri(URI.create("https://example.com")).build(); + } + + @Override + public Optional> previousResponse() { + return Optional.empty(); + } + + @Override + public HttpHeaders headers() { + return HttpHeaders.of(Collections.emptyMap(), (s1, s2) -> true); + } + + @Override + public URI uri() { + return URI.create("https://example.com"); + } + + @Override + public HttpClient.Version version() { + return HttpClient.Version.HTTP_1_1; + } + + @Override + public Optional sslSession() { + return Optional.empty(); + } + } } From 4d0f8beb0a8a2b670c224d133ae8e79fd54d1d45 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 16:10:26 +0100 Subject: [PATCH 013/105] fix: add signing exception class --- .../src/main/java/nostr/api/NostrIF.java | 2 +- ...gWebSocketClientEventVerificationTest.java | 44 +++++++++++++++++++ .../src/main/java/nostr/id/Identity.java | 6 +-- .../main/java/nostr/id/SigningException.java | 10 +++++ .../src/test/java/nostr/id/IdentityTest.java | 13 +++--- 5 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 nostr-java-id/src/main/java/nostr/id/SigningException.java diff --git a/nostr-java-api/src/main/java/nostr/api/NostrIF.java b/nostr-java-api/src/main/java/nostr/api/NostrIF.java index 7a28e197b..f297dfb21 100644 --- a/nostr-java-api/src/main/java/nostr/api/NostrIF.java +++ b/nostr-java-api/src/main/java/nostr/api/NostrIF.java @@ -20,7 +20,7 @@ public interface NostrIF { List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId, Map relays); List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId); List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId, Map relays); - NostrIF sign(@NonNull Identity identity, @NonNull ISignable signable) throws Exception; + NostrIF sign(@NonNull Identity identity, @NonNull ISignable signable); boolean verify(@NonNull GenericEvent event); Identity getSender(); Map getRelays(); diff --git a/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java b/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java index 47d46f5f1..a36f71dbf 100644 --- a/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java +++ b/nostr-java-api/src/test/java/nostr/api/unit/NostrSpringWebSocketClientEventVerificationTest.java @@ -5,6 +5,14 @@ import nostr.config.Constants; import nostr.event.impl.GenericEvent; import nostr.id.Identity; +import nostr.id.SigningException; +import nostr.base.ISignable; +import nostr.base.Signature; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -41,4 +49,40 @@ void sendEventReturnsEmptyListWhenSigned() { List responses = client.sendEvent(event); assertTrue(responses.isEmpty()); } + + @Test + // Verifies that SigningException bubbles up from the client when signing fails + void signPropagatesSigningException() { + String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; + Identity identity = Identity.create(invalidPriv); + + ISignable signable = new ISignable() { + private Signature signature; + + @Override + public Signature getSignature() { + return signature; + } + + @Override + public void setSignature(Signature signature) { + this.signature = signature; + } + + @Override + public Consumer getSignatureConsumer() { + return this::setSignature; + } + + @Override + public Supplier getByteArraySupplier() { + return () -> ByteBuffer.wrap("msg".getBytes(StandardCharsets.UTF_8)); + } + }; + + NoteService service = Mockito.mock(NoteService.class); + NostrSpringWebSocketClient client = new NostrSpringWebSocketClient(service); + + assertThrows(SigningException.class, () -> client.sign(identity, signable)); + } } diff --git a/nostr-java-id/src/main/java/nostr/id/Identity.java b/nostr-java-id/src/main/java/nostr/id/Identity.java index be58c715b..e2ad8f64c 100644 --- a/nostr-java-id/src/main/java/nostr/id/Identity.java +++ b/nostr-java-id/src/main/java/nostr/id/Identity.java @@ -88,7 +88,6 @@ public PublicKey getPublicKey() { return cachedPublicKey; } - // TODO: exceptions refactor /** * Signs the supplied {@link ISignable} using this identity's private key. * The resulting {@link Signature} is returned and also provided to the @@ -96,7 +95,8 @@ public PublicKey getPublicKey() { * * @param signable the entity to sign * @return the generated signature - * @throws Exception if the signature cannot be created + * @throws IllegalStateException if the SHA-256 algorithm is unavailable + * @throws SigningException if the signature cannot be created */ public Signature sign(@NonNull ISignable signable) { try { @@ -117,7 +117,7 @@ public Signature sign(@NonNull ISignable signable) { throw new IllegalStateException("SHA-256 algorithm not available", ex); } catch (Exception ex) { log.error("Signing failed", ex); - throw new IllegalArgumentException("Failed to sign with provided key", ex); + throw new SigningException("Failed to sign with provided key", ex); } } diff --git a/nostr-java-id/src/main/java/nostr/id/SigningException.java b/nostr-java-id/src/main/java/nostr/id/SigningException.java new file mode 100644 index 000000000..500ef06a3 --- /dev/null +++ b/nostr-java-id/src/main/java/nostr/id/SigningException.java @@ -0,0 +1,10 @@ +package nostr.id; + +import lombok.experimental.StandardException; + +/** + * Exception thrown when signing an {@link nostr.base.ISignable} fails. + */ +@StandardException +public class SigningException extends RuntimeException { +} diff --git a/nostr-java-id/src/test/java/nostr/id/IdentityTest.java b/nostr-java-id/src/test/java/nostr/id/IdentityTest.java index 101d44235..96da3cee4 100644 --- a/nostr-java-id/src/test/java/nostr/id/IdentityTest.java +++ b/nostr-java-id/src/test/java/nostr/id/IdentityTest.java @@ -111,10 +111,11 @@ public void testGetPublicKeyFailure() { Assertions.assertThrows(IllegalStateException.class, identity::getPublicKey); } - @Test - public void testSignWithInvalidKeyFails() { - String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; - Identity identity = Identity.create(invalidPriv); + @Test + // Ensures that signing with an invalid private key throws SigningException + public void testSignWithInvalidKeyFails() { + String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000"; + Identity identity = Identity.create(invalidPriv); ISignable signable = new ISignable() { private Signature signature; @@ -140,6 +141,6 @@ public Supplier getByteArraySupplier() { } }; - Assertions.assertThrows(IllegalArgumentException.class, () -> identity.sign(signable)); - } + Assertions.assertThrows(SigningException.class, () -> identity.sign(signable)); + } } \ No newline at end of file From 61a2261652bd9af4a6ff345b821f0971c37b3cb0 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 16:13:46 +0100 Subject: [PATCH 014/105] fix: handle event encoding errors --- .../event/json/codec/BaseEventEncoder.java | 9 ++-- .../json/codec/EventEncodingException.java | 7 +++ .../CanonicalAuthenticationMessage.java | 15 ++++-- .../nostr/event/message/EventMessage.java | 9 +++- .../json/codec/BaseEventEncoderTest.java | 46 +++++++++++++++++++ 5 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java create mode 100644 nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java index 778301a27..76ae9258b 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java @@ -1,7 +1,7 @@ package nostr.event.json.codec; +import com.fasterxml.jackson.core.JsonProcessingException; import lombok.Data; -import lombok.SneakyThrows; import nostr.base.Encoder; import nostr.event.BaseEvent; @@ -16,8 +16,11 @@ public BaseEventEncoder(T event) { @Override // TODO: refactor all methods calling this to properly handle invalid json exception - @SneakyThrows public String encode() { - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(event); + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(event); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode event to JSON", e); + } } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java b/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java new file mode 100644 index 000000000..493a344ec --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java @@ -0,0 +1,7 @@ +package nostr.event.json.codec; + +import lombok.experimental.StandardException; + +@StandardException +public class EventEncodingException extends RuntimeException { +} diff --git a/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java b/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java index aa1e5da09..fc4cace08 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java @@ -14,6 +14,7 @@ import nostr.event.impl.CanonicalAuthenticationEvent; import nostr.event.impl.GenericEvent; import nostr.event.json.codec.BaseEventEncoder; +import nostr.event.json.codec.EventEncodingException; import nostr.event.tag.GenericTag; import java.util.List; @@ -39,11 +40,15 @@ public CanonicalAuthenticationMessage(CanonicalAuthenticationEvent event) { @Override public String encode() throws JsonProcessingException { - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( - JsonNodeFactory.instance.arrayNode() - .add(getCommand()) - .add(ENCODER_MAPPER_BLACKBIRD.readTree( - new BaseEventEncoder<>(getEvent()).encode()))); + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( + JsonNodeFactory.instance.arrayNode() + .add(getCommand()) + .add(ENCODER_MAPPER_BLACKBIRD.readTree( + new BaseEventEncoder<>(getEvent()).encode()))); + } catch (EventEncodingException e) { + throw new IllegalStateException("Failed to encode canonical authentication event", e); + } } @SneakyThrows diff --git a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java index 0c3e3bceb..75af5ac4f 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java @@ -14,6 +14,7 @@ import nostr.event.BaseMessage; import nostr.event.impl.GenericEvent; import nostr.event.json.codec.BaseEventEncoder; +import nostr.event.json.codec.EventEncodingException; import java.util.Map; import java.util.Objects; @@ -52,8 +53,12 @@ public String encode() throws JsonProcessingException { var arrayNode = JsonNodeFactory.instance.arrayNode().add(getCommand()); Optional.ofNullable(getSubscriptionId()) .ifPresent(arrayNode::add); - arrayNode.add(ENCODER_MAPPER_BLACKBIRD.readTree( - new BaseEventEncoder<>((BaseEvent) getEvent()).encode())); + try { + arrayNode.add(ENCODER_MAPPER_BLACKBIRD.readTree( + new BaseEventEncoder<>((BaseEvent) getEvent()).encode())); + } catch (EventEncodingException e) { + throw new IllegalStateException("Failed to encode event", e); + } return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(arrayNode); } diff --git a/nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java b/nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java new file mode 100644 index 000000000..6a8ef3833 --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/json/codec/BaseEventEncoderTest.java @@ -0,0 +1,46 @@ +package nostr.event.json.codec; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import nostr.event.BaseEvent; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BaseEventEncoderTest { + + static class FailingSerializer extends JsonSerializer { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + throw new IOException("Serialization failure"); + } + } + + static class FailingEvent extends BaseEvent { + @JsonSerialize(using = FailingSerializer.class) + public String getAttr() { + return "boom"; + } + + @Override + public String getId() { + return ""; + } + + @Override + public String toBech32() { + return ""; + } + } + + @Test + // Ensures encode throws EventEncodingException when serialization fails + void encodeThrowsEventEncodingException() { + var encoder = new BaseEventEncoder<>(new FailingEvent()); + assertThrows(EventEncodingException.class, encoder::encode); + } +} From be0df6b7414a131f08bec1924d8d723f8d0a03b3 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 16:15:22 +0100 Subject: [PATCH 015/105] fix: remove default marker assignment in EventTag --- .../main/java/nostr/event/tag/EventTag.java | 4 -- .../java/nostr/event/unit/EventTagTest.java | 43 +++++++++++++++++++ .../nostr/event/unit/TagDeserializerTest.java | 19 ++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/nostr-java-event/src/main/java/nostr/event/tag/EventTag.java b/nostr-java-event/src/main/java/nostr/event/tag/EventTag.java index 6f54af9f9..eb25ce6f5 100644 --- a/nostr-java-event/src/main/java/nostr/event/tag/EventTag.java +++ b/nostr-java-event/src/main/java/nostr/event/tag/EventTag.java @@ -42,11 +42,7 @@ public class EventTag extends BaseTag { private Marker marker; public EventTag(String idEvent) { - this.recommendedRelayUrl = null; this.idEvent = idEvent; - - // TODO: This is a bug. The marker should not be set, or at least not like this. - //this.marker = this.idEvent == null ? Marker.ROOT : Marker.REPLY; } public static T deserialize(@NonNull JsonNode node) { diff --git a/nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java b/nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java index d8dc55bf8..3592e7e7d 100644 --- a/nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java +++ b/nostr-java-event/src/test/java/nostr/event/unit/EventTagTest.java @@ -1,6 +1,8 @@ package nostr.event.unit; import nostr.base.Marker; +import nostr.event.BaseTag; +import nostr.event.json.codec.BaseTagEncoder; import nostr.event.tag.EventTag; import org.junit.jupiter.api.Test; @@ -9,12 +11,17 @@ import java.util.UUID; import java.util.function.Predicate; +import static nostr.base.IEvent.MAPPER_BLACKBIRD; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class EventTagTest { @Test + // Verifies that getSupportedFields returns expected fields and values. void getSupportedFields() { String eventId = UUID.randomUUID().toString().concat(UUID.randomUUID().toString()).substring(0, 64); String recommendedRelayUrl = "ws://localhost:5555"; @@ -36,6 +43,42 @@ void getSupportedFields() { assertFalse(fields.stream().flatMap(field -> eventTag.getFieldValue(field).stream()).anyMatch(fieldValue -> fieldValue.equals(eventId + "x"))); } + @Test + // Ensures that a newly created EventTag has a null marker and serializes without it. + void serializeWithoutMarker() throws Exception { + String eventId = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; + EventTag eventTag = new EventTag(eventId); + + assertNull(eventTag.getMarker()); + + String json = new BaseTagEncoder(eventTag).encode(); + assertEquals("[\"e\",\"" + eventId + "\"]", json); + + BaseTag decoded = MAPPER_BLACKBIRD.readValue(json, BaseTag.class); + assertInstanceOf(EventTag.class, decoded); + assertNull(((EventTag) decoded).getMarker()); + } + + @Test + // Checks that an explicit marker is serialized and restored on deserialization. + void serializeWithMarker() throws Exception { + String eventId = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; + EventTag eventTag = EventTag.builder() + .idEvent(eventId) + .recommendedRelayUrl("wss://relay.example.com") + .marker(Marker.ROOT) + .build(); + + String json = new BaseTagEncoder(eventTag).encode(); + assertEquals("[\"e\",\"" + eventId + "\",\"wss://relay.example.com\",\"ROOT\"]", json); + + BaseTag decoded = MAPPER_BLACKBIRD.readValue(json, BaseTag.class); + assertInstanceOf(EventTag.class, decoded); + EventTag decodedEventTag = (EventTag) decoded; + assertEquals(Marker.ROOT, decodedEventTag.getMarker()); + assertEquals("wss://relay.example.com", decodedEventTag.getRecommendedRelayUrl()); + } + private static void anyFieldNameMatch(List fields, Predicate predicate) { assertTrue(fields.stream().anyMatch(predicate)); } diff --git a/nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java b/nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java index 494683482..9f75b87fc 100644 --- a/nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java +++ b/nostr-java-event/src/test/java/nostr/event/unit/TagDeserializerTest.java @@ -13,10 +13,12 @@ import static nostr.base.IEvent.MAPPER_BLACKBIRD; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; class TagDeserializerTest { @Test + // Parses an AddressTag from JSON and verifies its fields. void testAddressTagDeserialization() throws Exception { String pubKey = "bbbd79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984"; String json = "[\"a\",\"1:" + pubKey + ":test\",\"ws://localhost:8080\"]"; @@ -30,6 +32,7 @@ void testAddressTagDeserialization() throws Exception { } @Test + // Parses an EventTag with relay and marker and checks values. void testEventTagDeserialization() throws Exception { String id = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; String json = "[\"e\",\"" + id + "\",\"wss://relay.example.com\",\"root\"]"; @@ -42,6 +45,20 @@ void testEventTagDeserialization() throws Exception { } @Test + // Parses an EventTag without relay or marker and ensures marker is null. + void testEventTagDeserializationWithoutMarker() throws Exception { + String id = "494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"; + String json = "[\"e\",\"" + id + "\"]"; + BaseTag tag = MAPPER_BLACKBIRD.readValue(json, BaseTag.class); + assertInstanceOf(EventTag.class, tag); + EventTag eTag = (EventTag) tag; + assertEquals(id, eTag.getIdEvent()); + assertNull(eTag.getMarker()); + assertNull(eTag.getRecommendedRelayUrl()); + } + + @Test + // Parses a PriceTag from JSON and validates number and currency. void testPriceTagDeserialization() throws Exception { String json = "[\"price\",\"10.99\",\"USD\"]"; BaseTag tag = MAPPER_BLACKBIRD.readValue(json, BaseTag.class); @@ -52,6 +69,7 @@ void testPriceTagDeserialization() throws Exception { } @Test + // Parses a UrlTag from JSON and checks the URL value. void testUrlTagDeserialization() throws Exception { String json = "[\"u\",\"http://example.com\"]"; BaseTag tag = MAPPER_BLACKBIRD.readValue(json, BaseTag.class); @@ -61,6 +79,7 @@ void testUrlTagDeserialization() throws Exception { } @Test + // Falls back to GenericTag for unknown tag codes. void testGenericFallback() throws Exception { String json = "[\"unknown\",\"value\"]"; BaseTag tag = MAPPER_BLACKBIRD.readValue(json, BaseTag.class); From 4529fcb4d69b2bc562777afdabd8a4c935ae1c93 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 16:25:46 +0100 Subject: [PATCH 016/105] docs: enforce PR template usage --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 9846c17b8..a17bf4c0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,7 @@ The URL format for the NIPs is https://github.com/nostr-protocol/nips/blob/maste ## Pull Requests -- Always follow the PR submission guidelines and use the pull request template at `.github/pull_request_template.md`, filling out all sections. +- Always use the pull request template at `.github/pull_request_template.md` when crafting a PR, and fill out all sections. - Summarize the changes made and describe how they were tested. - Include any limitations or known issues in the description. - Add a "Network Access" section summarizing blocked domains if network requests were denied. From f195b0b1cc5bb39d9373f090f0603d269bf37609 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 16:29:55 +0100 Subject: [PATCH 017/105] chore: bump version to 0.2.2.1 --- nostr-java-api/pom.xml | 2 +- nostr-java-base/pom.xml | 2 +- nostr-java-client/pom.xml | 2 +- nostr-java-crypto/pom.xml | 2 +- nostr-java-encryption/pom.xml | 2 +- nostr-java-event/pom.xml | 2 +- nostr-java-examples/pom.xml | 2 +- nostr-java-id/pom.xml | 2 +- nostr-java-util/pom.xml | 2 +- pom.xml | 4 ++-- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nostr-java-api/pom.xml b/nostr-java-api/pom.xml index 0248134a7..c38a292f3 100644 --- a/nostr-java-api/pom.xml +++ b/nostr-java-api/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-base/pom.xml b/nostr-java-base/pom.xml index 835fc67b7..9f2161eb7 100644 --- a/nostr-java-base/pom.xml +++ b/nostr-java-base/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-client/pom.xml b/nostr-java-client/pom.xml index c8c41793c..22bd5438a 100644 --- a/nostr-java-client/pom.xml +++ b/nostr-java-client/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-crypto/pom.xml b/nostr-java-crypto/pom.xml index fc332a740..ef375ee87 100644 --- a/nostr-java-crypto/pom.xml +++ b/nostr-java-crypto/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-encryption/pom.xml b/nostr-java-encryption/pom.xml index 22e6e19e1..1541f2f62 100644 --- a/nostr-java-encryption/pom.xml +++ b/nostr-java-encryption/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-event/pom.xml b/nostr-java-event/pom.xml index 87461c835..8221b7130 100644 --- a/nostr-java-event/pom.xml +++ b/nostr-java-event/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-examples/pom.xml b/nostr-java-examples/pom.xml index 27256d127..3f96104d7 100644 --- a/nostr-java-examples/pom.xml +++ b/nostr-java-examples/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-id/pom.xml b/nostr-java-id/pom.xml index 603cd1eaf..e133eb9b5 100644 --- a/nostr-java-id/pom.xml +++ b/nostr-java-id/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/nostr-java-util/pom.xml b/nostr-java-util/pom.xml index 15eaf1b84..70c6068cd 100644 --- a/nostr-java-util/pom.xml +++ b/nostr-java-util/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 ../pom.xml diff --git a/pom.xml b/pom.xml index 29a5eabe7..28e9ee831 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ xyz.tcheeric nostr-java - 0.2.2 + 0.2.2.1 pom @@ -74,7 +74,7 @@ - 0.2.2 + 0.2.2.1 21 3.5.4 From 37b06d13474b0318979a8f46357643bea8eb025e Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 21 Aug 2025 18:47:59 +0300 Subject: [PATCH 018/105] Update nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/nostr/event/json/codec/EventEncodingException.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java b/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java index 493a344ec..b529b36bd 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java @@ -2,6 +2,10 @@ import lombok.experimental.StandardException; +/** + * Exception thrown to indicate a problem occurred while encoding a Nostr event to JSON. + * This exception is typically thrown when the event cannot be serialized due to invalid data or encoding errors. + */ @StandardException public class EventEncodingException extends RuntimeException { } From 4c5ef4cdceb311742cae41106415213c24293526 Mon Sep 17 00:00:00 2001 From: Eric T Date: Fri, 22 Aug 2025 09:29:48 +0300 Subject: [PATCH 019/105] test: add validation tests for more event types --- .../impl/AddressableEventValidateTest.java | 39 +++++++++++ .../impl/ChannelMessageEventValidateTest.java | 61 +++++++++++++++++ .../event/impl/DeletionEventValidateTest.java | 67 +++++++++++++++++++ .../impl/DirectMessageEventValidateTest.java | 56 ++++++++++++++++ .../impl/EphemeralEventValidateTest.java | 39 +++++++++++ .../impl/HideMessageEventValidateTest.java | 53 +++++++++++++++ .../event/impl/MuteUserEventValidateTest.java | 62 +++++++++++++++++ .../event/impl/ReactionEventValidateTest.java | 62 +++++++++++++++++ .../impl/ReplaceableEventValidateTest.java | 56 ++++++++++++++++ 9 files changed, 495 insertions(+) create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java create mode 100644 nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java diff --git a/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java new file mode 100644 index 000000000..e5feabe64 --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/AddressableEventValidateTest.java @@ -0,0 +1,39 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AddressableEventValidateTest { + private static final String HEX_64 = "a".repeat(64); + private static final String SIG_HEX = "b".repeat(128); + + private AddressableEvent createEvent(int kind) { + AddressableEvent event = new AddressableEvent(new PublicKey(HEX_64), kind, new ArrayList(), ""); + event.setId(HEX_64); + event.setCreatedAt(Instant.now().getEpochSecond()); + event.setSignature(Signature.fromString(SIG_HEX)); + return event; + } + + // Valid kind within the 30000-39999 range passes validation + @Test + public void testValidateKindSuccess() { + AddressableEvent event = createEvent(30000); + assertDoesNotThrow(event::validate); + } + + // Kind outside the allowed range triggers validation failure + @Test + public void testValidateKindFailure() { + AddressableEvent event = createEvent(1000); + assertThrows(AssertionError.class, event::validate); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java new file mode 100644 index 000000000..e9fcdfa1d --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/ChannelMessageEventValidateTest.java @@ -0,0 +1,61 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ChannelMessageEventValidateTest { + private static final String HEX_64_A = "a".repeat(64); + private static final String HEX_64_B = "b".repeat(64); + private static final String SIG_HEX = "c".repeat(128); + private static final String CHANNEL_JSON = "{\"name\":\"chan\",\"about\":\"desc\",\"picture\":\"http://example.com/img.png\"}"; + + private ChannelCreateEvent createRootEvent() { + ChannelCreateEvent root = new ChannelCreateEvent(new PublicKey(HEX_64_A), CHANNEL_JSON); + root.setId(HEX_64_B); + root.setCreatedAt(Instant.now().getEpochSecond()); + root.setSignature(Signature.fromString(SIG_HEX)); + return root; + } + + private ChannelMessageEvent createValidEvent() { + ChannelCreateEvent root = createRootEvent(); + ChannelMessageEvent event = new ChannelMessageEvent(new PublicKey(HEX_64_A), root, "hi", null); + event.setId(HEX_64_A); + event.setCreatedAt(Instant.now().getEpochSecond()); + event.setSignature(Signature.fromString(SIG_HEX)); + return event; + } + + // Channel message referencing its root event validates successfully + @Test + public void testValidateSuccess() { + ChannelMessageEvent event = createValidEvent(); + assertDoesNotThrow(event::validate); + } + + // Missing root event tag results in validation failure + @Test + public void testValidateMissingRootTag() { + ChannelMessageEvent event = new ChannelMessageEvent(new PublicKey(HEX_64_A), new ArrayList(), "hi"); + event.setId(HEX_64_A); + event.setCreatedAt(Instant.now().getEpochSecond()); + event.setSignature(Signature.fromString(SIG_HEX)); + assertThrows(AssertionError.class, event::validate); + } + + // Wrong kind value triggers validation error + @Test + public void testValidateWrongKind() { + ChannelMessageEvent event = createValidEvent(); + event.setKind(-1); + assertThrows(AssertionError.class, event::validate); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java new file mode 100644 index 000000000..a1c582203 --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/DeletionEventValidateTest.java @@ -0,0 +1,67 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import nostr.event.tag.EventTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DeletionEventValidateTest { + private static final String HEX_64_A = "a".repeat(64); + private static final String HEX_64_B = "b".repeat(64); + private static final String SIG_HEX = "c".repeat(128); + + private DeletionEvent createValidEvent() { + PublicKey pubKey = new PublicKey(HEX_64_A); + List tags = new ArrayList<>(); + tags.add(new EventTag(HEX_64_B)); + tags.add(BaseTag.create("k", "1")); + DeletionEvent event = new DeletionEvent(pubKey, tags); + event.setId(HEX_64_A); + event.setSignature(Signature.fromString(SIG_HEX)); + event.setCreatedAt(Instant.now().getEpochSecond()); + return event; + } + + // Valid deletion event with required tags passes validation + @Test + public void testValidateSuccess() { + DeletionEvent event = createValidEvent(); + assertDoesNotThrow(event::validate); + } + + // Validation fails when event or author tag is missing + @Test + public void testValidateMissingEventOrAuthorTag() { + DeletionEvent event = createValidEvent(); + List tags = new ArrayList<>(); + tags.add(BaseTag.create("k", "1")); + event.setTags(tags); + assertThrows(AssertionError.class, event::validate); + } + + // Validation fails when the kind tag is absent + @Test + public void testValidateMissingKindTag() { + DeletionEvent event = createValidEvent(); + List tags = new ArrayList<>(); + tags.add(new EventTag(HEX_64_B)); + event.setTags(tags); + assertThrows(AssertionError.class, event::validate); + } + + // Validation fails if the event kind is incorrect + @Test + public void testValidateWrongKind() { + DeletionEvent event = createValidEvent(); + event.setKind(-1); + assertThrows(AssertionError.class, event::validate); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java new file mode 100644 index 000000000..2317a0f76 --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/DirectMessageEventValidateTest.java @@ -0,0 +1,56 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DirectMessageEventValidateTest { + private static final String HEX_64_A = "a".repeat(64); + private static final String HEX_64_B = "b".repeat(64); + private static final String SIG_HEX = "c".repeat(128); + + private DirectMessageEvent createValidEvent() { + DirectMessageEvent event = new DirectMessageEvent(new PublicKey(HEX_64_A), new PublicKey(HEX_64_B), "hello"); + event.setId(HEX_64_A); + event.setCreatedAt(Instant.now().getEpochSecond()); + event.setSignature(Signature.fromString(SIG_HEX)); + return event; + } + + private DirectMessageEvent createEventWithoutRecipient() { + DirectMessageEvent event = new DirectMessageEvent(new PublicKey(HEX_64_A), new ArrayList(), "hello"); + event.setId(HEX_64_A); + event.setCreatedAt(Instant.now().getEpochSecond()); + event.setSignature(Signature.fromString(SIG_HEX)); + return event; + } + + // Direct message with recipient tag validates successfully + @Test + public void testValidateSuccess() { + DirectMessageEvent event = createValidEvent(); + assertDoesNotThrow(event::validate); + } + + // Missing recipient public key tag causes validation failure + @Test + public void testValidateMissingRecipient() { + DirectMessageEvent event = createEventWithoutRecipient(); + assertThrows(AssertionError.class, event::validate); + } + + // Incorrect kind value is rejected during validation + @Test + public void testValidateWrongKind() { + DirectMessageEvent event = createValidEvent(); + event.setKind(-1); + assertThrows(AssertionError.class, event::validate); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java new file mode 100644 index 000000000..9389d0432 --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/EphemeralEventValidateTest.java @@ -0,0 +1,39 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class EphemeralEventValidateTest { + private static final String HEX_64 = "a".repeat(64); + private static final String SIG_HEX = "b".repeat(128); + + private EphemeralEvent createEvent(int kind) { + EphemeralEvent event = new EphemeralEvent(new PublicKey(HEX_64), kind, new ArrayList(), ""); + event.setId(HEX_64); + event.setCreatedAt(Instant.now().getEpochSecond()); + event.setSignature(Signature.fromString(SIG_HEX)); + return event; + } + + // Kind within the 20000-29999 range validates successfully + @Test + public void testValidateKindSuccess() { + EphemeralEvent event = createEvent(20000); + assertDoesNotThrow(event::validate); + } + + // Kind outside the 20000-29999 range fails validation + @Test + public void testValidateKindFailure() { + EphemeralEvent event = createEvent(1000); + assertThrows(AssertionError.class, event::validate); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java new file mode 100644 index 000000000..a7d9b11de --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/HideMessageEventValidateTest.java @@ -0,0 +1,53 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import nostr.event.tag.EventTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HideMessageEventValidateTest { + private static final String HEX_64_A = "a".repeat(64); + private static final String HEX_64_B = "b".repeat(64); + private static final String SIG_HEX = "c".repeat(128); + + private HideMessageEvent createValidEvent() { + List tags = new ArrayList<>(); + tags.add(new EventTag(HEX_64_B)); + HideMessageEvent event = new HideMessageEvent(new PublicKey(HEX_64_A), tags, ""); + event.setId(HEX_64_A); + event.setCreatedAt(Instant.now().getEpochSecond()); + event.setSignature(Signature.fromString(SIG_HEX)); + return event; + } + + // Hide message event with at least one event tag validates successfully + @Test + public void testValidateSuccess() { + HideMessageEvent event = createValidEvent(); + assertDoesNotThrow(event::validate); + } + + // Missing event tag causes validation to fail + @Test + public void testValidateMissingEventTag() { + HideMessageEvent event = createValidEvent(); + event.setTags(new ArrayList<>()); + assertThrows(AssertionError.class, event::validate); + } + + // Wrong kind value triggers validation error + @Test + public void testValidateWrongKind() { + HideMessageEvent event = createValidEvent(); + event.setKind(-1); + assertThrows(AssertionError.class, event::validate); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java new file mode 100644 index 000000000..4695dd7ed --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/MuteUserEventValidateTest.java @@ -0,0 +1,62 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import nostr.event.tag.PubKeyTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MuteUserEventValidateTest { + private static final String HEX_64_A = "a".repeat(64); + private static final String HEX_64_B = "b".repeat(64); + private static final String SIG_HEX = "c".repeat(128); + + private MuteUserEvent createValidEvent() { + PublicKey pubKey = new PublicKey(HEX_64_A); + List tags = new ArrayList<>(); + tags.add(new PubKeyTag(new PublicKey(HEX_64_B))); + MuteUserEvent event = new MuteUserEvent(pubKey, tags, "mute"); + event.setId(HEX_64_A); + event.setSignature(Signature.fromString(SIG_HEX)); + event.setCreatedAt(Instant.now().getEpochSecond()); + return event; + } + + // Valid mute user event should pass validation + @Test + public void testValidateSuccess() { + MuteUserEvent event = createValidEvent(); + assertDoesNotThrow(event::validate); + } + + // Validation fails when the pubkey tag is missing + @Test + public void testValidateMissingPubKeyTag() { + MuteUserEvent event = createValidEvent(); + event.setTags(new ArrayList<>()); + assertThrows(AssertionError.class, event::validate); + } + + // Validation fails if the event kind is incorrect + @Test + public void testValidateWrongKind() { + MuteUserEvent event = createValidEvent(); + event.setKind(-1); + assertThrows(AssertionError.class, event::validate); + } + + // Retrieves the muted user's public key from tags + @Test + public void testGetMutedUser() { + MuteUserEvent event = createValidEvent(); + assertEquals(HEX_64_B, event.getMutedUser().toString()); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java new file mode 100644 index 000000000..54c10c781 --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/ReactionEventValidateTest.java @@ -0,0 +1,62 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import nostr.event.tag.EventTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ReactionEventValidateTest { + private static final String HEX_64_A = "a".repeat(64); + private static final String HEX_64_B = "b".repeat(64); + private static final String SIG_HEX = "c".repeat(128); + + private ReactionEvent createValidEvent() { + PublicKey pubKey = new PublicKey(HEX_64_A); + List tags = new ArrayList<>(); + tags.add(new EventTag(HEX_64_B)); + ReactionEvent event = new ReactionEvent(pubKey, tags, "+"); + event.setId(HEX_64_A); + event.setSignature(Signature.fromString(SIG_HEX)); + event.setCreatedAt(Instant.now().getEpochSecond()); + return event; + } + + // Valid reaction event should pass validation + @Test + public void testValidateSuccess() { + ReactionEvent event = createValidEvent(); + assertDoesNotThrow(event::validate); + } + + // Validation fails when the required event tag is missing + @Test + public void testValidateMissingEventTag() { + ReactionEvent event = createValidEvent(); + event.setTags(new ArrayList<>()); + assertThrows(AssertionError.class, event::validate); + } + + // Validation fails if the event kind is incorrect + @Test + public void testValidateWrongKind() { + ReactionEvent event = createValidEvent(); + event.setKind(-1); + assertThrows(AssertionError.class, event::validate); + } + + // Retrieves the ID of the reacted event from tags + @Test + public void testGetReactedEventId() { + ReactionEvent event = createValidEvent(); + assertEquals(HEX_64_B, event.getReactedEventId()); + } +} diff --git a/nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java b/nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java new file mode 100644 index 000000000..6503cc233 --- /dev/null +++ b/nostr-java-event/src/test/java/nostr/event/impl/ReplaceableEventValidateTest.java @@ -0,0 +1,56 @@ +package nostr.event.impl; + +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.event.BaseTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ReplaceableEventValidateTest { + private static final String HEX_64_A = "a".repeat(64); + private static final String SIG_HEX = "c".repeat(128); + + private ReplaceableEvent createEventWithKind(int kind) { + PublicKey pubKey = new PublicKey(HEX_64_A); + List tags = new ArrayList<>(); + ReplaceableEvent event = new ReplaceableEvent(pubKey, kind, tags, ""); + event.setId(HEX_64_A); + event.setSignature(Signature.fromString(SIG_HEX)); + event.setCreatedAt(Instant.now().getEpochSecond()); + return event; + } + + // Validation succeeds when kind falls within replaceable range + @Test + public void testValidateKindInRange() { + ReplaceableEvent event = createEventWithKind(10_000); + assertDoesNotThrow(event::validate); + } + + // Kind zero is allowed for replaceable events + @Test + public void testValidateKindZero() { + ReplaceableEvent event = createEventWithKind(0); + assertDoesNotThrow(event::validate); + } + + // Kind three is permitted under replaceable rules + @Test + public void testValidateKindThree() { + ReplaceableEvent event = createEventWithKind(3); + assertDoesNotThrow(event::validate); + } + + // Validation fails when kind is outside the replaceable range + @Test + public void testValidateKindInvalid() { + ReplaceableEvent event = createEventWithKind(9_999); + assertThrows(AssertionError.class, event::validate); + } +} From 7b1128e613819d8c8f5b64cea719fd816eb65949 Mon Sep 17 00:00:00 2001 From: Eric T Date: Fri, 22 Aug 2025 09:54:45 +0300 Subject: [PATCH 020/105] refactor: remove unused Nip05Obj inner class --- .../src/main/java/nostr/util/validator/Nip05Validator.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java b/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java index d121c2904..090da6bf6 100644 --- a/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java +++ b/nostr-java-util/src/main/java/nostr/util/validator/Nip05Validator.java @@ -153,9 +153,4 @@ private String getPublicKey(String content, String localPart) throws NostrExcept return names.get(localPart); } - @Data - public static final class Nip05Obj { - private final String name; - private final String nip05; - } } From 18f3f2fc976b020827d3a7320c7af3990b6dfb09 Mon Sep 17 00:00:00 2001 From: Eric T Date: Fri, 22 Aug 2025 10:01:02 +0300 Subject: [PATCH 021/105] refactor: use EventEncodingException for encoding --- .../nostr/api/integration/ApiEventIT.java | 17 +++++++-- .../src/main/java/nostr/base/IDecoder.java | 3 +- .../main/java/nostr/event/BaseMessage.java | 4 +- .../event/json/codec/BaseEventEncoder.java | 2 +- .../event/json/codec/BaseMessageDecoder.java | 33 ++++++++++------- .../event/json/codec/BaseTagDecoder.java | 7 ++-- .../event/json/codec/BaseTagEncoder.java | 7 ++-- .../java/nostr/event/json/codec/FDecoder.java | 4 +- .../event/json/codec/FiltersDecoder.java | 32 +++++++++------- .../event/json/codec/GenericEventDecoder.java | 11 ++++-- .../event/json/codec/GenericTagDecoder.java | 18 ++------- .../event/json/codec/Nip05ContentDecoder.java | 7 ++-- .../CanonicalAuthenticationMessage.java | 6 +-- .../nostr/event/message/CloseMessage.java | 15 +++++--- .../java/nostr/event/message/EoseMessage.java | 15 +++++--- .../nostr/event/message/EventMessage.java | 14 +++---- .../nostr/event/message/GenericMessage.java | 9 ++++- .../nostr/event/message/NoticeMessage.java | 15 +++++--- .../java/nostr/event/message/OkMessage.java | 23 +++++++----- .../message/RelayAuthenticationMessage.java | 15 +++++--- .../java/nostr/event/message/ReqMessage.java | 37 ++++++++++++------- 21 files changed, 175 insertions(+), 119 deletions(-) diff --git a/nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java b/nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java index 34628d154..a6ca83875 100644 --- a/nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java +++ b/nostr-java-api/src/test/java/nostr/api/integration/ApiEventIT.java @@ -1,6 +1,5 @@ package nostr.api.integration; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; import nostr.api.EventNostr; import nostr.api.NIP01; @@ -29,6 +28,7 @@ import nostr.event.filter.VoteTagFilter; import nostr.event.impl.GenericEvent; import nostr.event.json.codec.BaseMessageDecoder; +import nostr.event.json.codec.EventEncodingException; import nostr.event.message.EoseMessage; import nostr.event.message.EventMessage; import nostr.event.message.OkMessage; @@ -38,6 +38,7 @@ import nostr.event.tag.LabelNamespaceTag; import nostr.event.tag.LabelTag; import nostr.event.tag.PubKeyTag; +import com.fasterxml.jackson.core.JsonProcessingException; import nostr.event.tag.UrlTag; import nostr.event.tag.VoteTag; import nostr.id.Identity; @@ -273,7 +274,7 @@ public void testFilterUrlTag() throws IOException { .map(json -> { try { return new BaseMessageDecoder<>().decode(json); - } catch (JsonProcessingException e) { + } catch (EventEncodingException e) { throw new RuntimeException(e); } }) @@ -403,7 +404,7 @@ public void testNIP04EncryptDecrypt() { } @Test - public void testNIP15CreateStallEvent() throws JsonProcessingException { + public void testNIP15CreateStallEvent() throws EventEncodingException { System.out.println("testNIP15CreateStallEvent"); Stall stall = createStall(); @@ -416,11 +417,19 @@ public void testNIP15CreateStallEvent() throws JsonProcessingException { // Fetch the content and compare with the above original var content = instance.getEvent().getContent(); - var expected = MAPPER_BLACKBIRD.readValue(content, Stall.class); + var expected = readStall(content); assertEquals(expected, stall); } + private Stall readStall(String content) throws EventEncodingException { + try { + return MAPPER_BLACKBIRD.readValue(content, Stall.class); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to decode stall content", e); + } + } + @Test public void testNIP15UpdateStallEvent() throws IOException { System.out.println("testNIP15UpdateStallEvent"); diff --git a/nostr-java-base/src/main/java/nostr/base/IDecoder.java b/nostr-java-base/src/main/java/nostr/base/IDecoder.java index 1e4467583..5bf56e417 100644 --- a/nostr-java-base/src/main/java/nostr/base/IDecoder.java +++ b/nostr-java-base/src/main/java/nostr/base/IDecoder.java @@ -1,7 +1,6 @@ package nostr.base; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.module.blackbird.BlackbirdModule; @@ -14,6 +13,6 @@ public interface IDecoder { ObjectMapper I_DECODER_MAPPER_BLACKBIRD = JsonMapper.builder().addModule(new BlackbirdModule()).build().configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - T decode(String str) throws JsonProcessingException; + T decode(String str); } diff --git a/nostr-java-event/src/main/java/nostr/event/BaseMessage.java b/nostr-java-event/src/main/java/nostr/event/BaseMessage.java index f19c6c663..931f924b2 100644 --- a/nostr-java-event/src/main/java/nostr/event/BaseMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/BaseMessage.java @@ -1,8 +1,8 @@ package nostr.event; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.Getter; import nostr.base.IElement; +import nostr.event.json.codec.EventEncodingException; /** * @@ -16,5 +16,5 @@ protected BaseMessage(String command) { this.command = command; } - public abstract String encode() throws JsonProcessingException; + public abstract String encode() throws EventEncodingException; } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java index 76ae9258b..11790b550 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseEventEncoder.java @@ -16,7 +16,7 @@ public BaseEventEncoder(T event) { @Override // TODO: refactor all methods calling this to properly handle invalid json exception - public String encode() { + public String encode() throws EventEncodingException { try { return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(event); } catch (JsonProcessingException e) { diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java index d85605e4c..022cf0377 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java @@ -6,6 +6,7 @@ import lombok.NonNull; import nostr.base.IDecoder; import nostr.event.BaseMessage; +import nostr.event.json.codec.EventEncodingException; import nostr.event.message.CanonicalAuthenticationMessage; import nostr.event.message.CloseMessage; import nostr.event.message.EoseMessage; @@ -26,7 +27,7 @@ public class BaseMessageDecoder implements IDecoder { public static final int ARG_INDEX = 1; @Override - public T decode(@NonNull String jsonString) throws JsonProcessingException { + public T decode(@NonNull String jsonString) throws EventEncodingException { ValidNostrJsonStructure validNostrJsonStructure = validateProperlyFormedJson(jsonString); String command = validNostrJsonStructure.getCommand(); Object subscriptionId = validNostrJsonStructure.getSubscriptionId(); @@ -55,21 +56,25 @@ public T decode(@NonNull String jsonString) throws JsonProcessingException { }; } - private ValidNostrJsonStructure validateProperlyFormedJson(@NonNull String jsonString) throws JsonProcessingException { - JsonNode root = I_DECODER_MAPPER_BLACKBIRD.readTree(jsonString); - JsonNode commandNode = root.get(COMMAND_INDEX); - JsonNode argNode = root.get(ARG_INDEX); + private ValidNostrJsonStructure validateProperlyFormedJson(@NonNull String jsonString) throws EventEncodingException { + try { + JsonNode root = I_DECODER_MAPPER_BLACKBIRD.readTree(jsonString); + JsonNode commandNode = root.get(COMMAND_INDEX); + JsonNode argNode = root.get(ARG_INDEX); - if (commandNode == null || argNode == null) { - String missingFields = (commandNode == null ? "commandNode" : "") + - (commandNode == null && argNode == null ? " and " : "") + - (argNode == null ? "argNode" : ""); - throw new IllegalArgumentException(String.format("Invalid JSON structure: Missing %s in JSON string [%s]", missingFields, jsonString)); - } + if (commandNode == null || argNode == null) { + String missingFields = (commandNode == null ? "commandNode" : "") + + (commandNode == null && argNode == null ? " and " : "") + + (argNode == null ? "argNode" : ""); + throw new IllegalArgumentException(String.format("Invalid JSON structure: Missing %s in JSON string [%s]", missingFields, jsonString)); + } - return new ValidNostrJsonStructure( - commandNode.asText(), - argNode.asText()); + return new ValidNostrJsonStructure( + commandNode.asText(), + argNode.asText()); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to decode message", e); + } } private record ValidNostrJsonStructure( diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java index 3a5e1bf4e..eab704aec 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java @@ -1,9 +1,10 @@ package nostr.event.json.codec; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.Data; import nostr.base.IDecoder; import nostr.event.BaseTag; +import com.fasterxml.jackson.core.JsonProcessingException; +import nostr.event.json.codec.EventEncodingException; import static nostr.base.IEvent.MAPPER_BLACKBIRD; @@ -21,11 +22,11 @@ public BaseTagDecoder() { } @Override - public T decode(String jsonString) { + public T decode(String jsonString) throws EventEncodingException { try { return MAPPER_BLACKBIRD.readValue(jsonString, clazz); } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); + throw new EventEncodingException("Failed to decode tag", ex); } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java index c176515e1..ed693ffb4 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagEncoder.java @@ -1,11 +1,12 @@ package nostr.event.json.codec; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import nostr.base.Encoder; import nostr.event.BaseTag; import nostr.event.json.serializer.BaseTagSerializer; +import com.fasterxml.jackson.core.JsonProcessingException; +import nostr.event.json.codec.EventEncodingException; public record BaseTagEncoder(BaseTag tag) implements Encoder { public static final ObjectMapper BASETAG_ENCODER_MAPPER_BLACKBIRD = @@ -15,11 +16,11 @@ public record BaseTagEncoder(BaseTag tag) implements Encoder { new BaseTagSerializer<>())); @Override - public String encode() { + public String encode() throws EventEncodingException { try { return BASETAG_ENCODER_MAPPER_BLACKBIRD.writeValueAsString(tag); } catch (JsonProcessingException e) { - throw new RuntimeException(e); + throw new EventEncodingException("Failed to encode tag", e); } } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java index 24a894d78..276b3a4e3 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java @@ -1,6 +1,6 @@ package nostr.event.json.codec; public interface FDecoder { - - T decode(String str); + + T decode(String str) throws EventEncodingException; } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java index 2fedfedb9..bd231b452 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java @@ -1,12 +1,13 @@ package nostr.event.json.codec; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.NonNull; -import lombok.SneakyThrows; import nostr.event.filter.Filterable; import nostr.event.filter.Filters; +import nostr.event.json.codec.EventEncodingException; import java.util.ArrayList; import java.util.List; @@ -20,21 +21,24 @@ @Data public class FiltersDecoder implements FDecoder { - @SneakyThrows - public Filters decode(@NonNull String jsonFiltersList) { - final List filterables = new ArrayList<>(); + public Filters decode(@NonNull String jsonFiltersList) throws EventEncodingException { + try { + final List filterables = new ArrayList<>(); - Map filtersMap = MAPPER_BLACKBIRD.readValue( - jsonFiltersList, - new TypeReference>() {}); + Map filtersMap = MAPPER_BLACKBIRD.readValue( + jsonFiltersList, + new TypeReference>() {}); - for (Map.Entry entry : filtersMap.entrySet()) { - filterables.addAll( - FilterableProvider.getFilterFunction( - entry.getValue(), - entry.getKey())); - } + for (Map.Entry entry : filtersMap.entrySet()) { + filterables.addAll( + FilterableProvider.getFilterFunction( + entry.getValue(), + entry.getKey())); + } - return new Filters(filterables); + return new Filters(filterables); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to decode filters", e); + } } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java index c6d91a0a5..551c9ded2 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java @@ -5,6 +5,7 @@ import lombok.Data; import nostr.base.IDecoder; import nostr.event.impl.GenericEvent; +import nostr.event.json.codec.EventEncodingException; /** * @@ -24,8 +25,12 @@ public GenericEventDecoder(Class clazz) { } @Override - public T decode(String jsonEvent) throws JsonProcessingException { - I_DECODER_MAPPER_BLACKBIRD.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - return I_DECODER_MAPPER_BLACKBIRD.readValue(jsonEvent, clazz); + public T decode(String jsonEvent) throws EventEncodingException { + try { + I_DECODER_MAPPER_BLACKBIRD.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + return I_DECODER_MAPPER_BLACKBIRD.readValue(jsonEvent, clazz); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to decode generic event", e); + } } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java index e2c208c1a..ae7883c58 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java @@ -1,12 +1,13 @@ package nostr.event.json.codec; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.Data; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import nostr.base.ElementAttribute; import nostr.base.IDecoder; import nostr.event.tag.GenericTag; +import com.fasterxml.jackson.core.JsonProcessingException; +import nostr.event.json.codec.EventEncodingException; import java.util.ArrayList; @@ -25,7 +26,7 @@ public GenericTagDecoder(@NonNull Class clazz) { } @Override - public T decode(@NonNull String json) { + public T decode(@NonNull String json) throws EventEncodingException { try { String[] jsonElements = I_DECODER_MAPPER_BLACKBIRD.readValue(json, String[].class); GenericTag genericTag = new GenericTag( @@ -42,23 +43,12 @@ public T decode(@NonNull String json) { } } }); - /* - GenericTag genericTag = new GenericTag( - jsonElements[0], - IntStream.of(1, jsonElements.length - 1) - .mapToObj(i -> - new ElementAttribute( - "param".concat(String.valueOf(i - 1)), - jsonElements[i])) - .distinct() - .toList()); -*/ log.info("Decoded GenericTag: {}", genericTag); return (T) genericTag; } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); + throw new EventEncodingException("Failed to decode generic tag", ex); } } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java index 0e102c07f..4b2f1cfe5 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java @@ -1,9 +1,10 @@ package nostr.event.json.codec; -import com.fasterxml.jackson.core.JsonProcessingException; import lombok.Data; import nostr.base.IDecoder; import nostr.event.Nip05Content; +import com.fasterxml.jackson.core.JsonProcessingException; +import nostr.event.json.codec.EventEncodingException; import static nostr.base.IEvent.MAPPER_BLACKBIRD; @@ -21,11 +22,11 @@ public Nip05ContentDecoder() { } @Override - public T decode(String jsonContent) { + public T decode(String jsonContent) throws EventEncodingException { try { return MAPPER_BLACKBIRD.readValue(jsonContent, clazz); } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); + throw new EventEncodingException("Failed to decode nip05 content", ex); } } } diff --git a/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java b/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java index fc4cace08..8b351c127 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/CanonicalAuthenticationMessage.java @@ -39,15 +39,15 @@ public CanonicalAuthenticationMessage(CanonicalAuthenticationEvent event) { } @Override - public String encode() throws JsonProcessingException { + public String encode() throws EventEncodingException { try { return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( JsonNodeFactory.instance.arrayNode() .add(getCommand()) .add(ENCODER_MAPPER_BLACKBIRD.readTree( new BaseEventEncoder<>(getEvent()).encode()))); - } catch (EventEncodingException e) { - throw new IllegalStateException("Failed to encode canonical authentication event", e); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode canonical authentication message", e); } } diff --git a/nostr-java-event/src/main/java/nostr/event/message/CloseMessage.java b/nostr-java-event/src/main/java/nostr/event/message/CloseMessage.java index b182b0f13..a2388d3f7 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/CloseMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/CloseMessage.java @@ -8,6 +8,7 @@ import lombok.Setter; import nostr.base.Command; import nostr.event.BaseMessage; +import nostr.event.json.codec.EventEncodingException; import static nostr.base.Encoder.ENCODER_MAPPER_BLACKBIRD; @@ -32,11 +33,15 @@ public CloseMessage(String subscriptionId) { } @Override - public String encode() throws JsonProcessingException { - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( - JsonNodeFactory.instance.arrayNode() - .add(getCommand()) - .add(getSubscriptionId())); + public String encode() throws EventEncodingException { + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( + JsonNodeFactory.instance.arrayNode() + .add(getCommand()) + .add(getSubscriptionId())); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode close message", e); + } } public static T decode(@NonNull Object arg) { diff --git a/nostr-java-event/src/main/java/nostr/event/message/EoseMessage.java b/nostr-java-event/src/main/java/nostr/event/message/EoseMessage.java index 6727e1c3a..1be1961bf 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/EoseMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/EoseMessage.java @@ -8,6 +8,7 @@ import lombok.Setter; import nostr.base.Command; import nostr.event.BaseMessage; +import nostr.event.json.codec.EventEncodingException; import static nostr.base.Encoder.ENCODER_MAPPER_BLACKBIRD; @@ -31,11 +32,15 @@ public EoseMessage(String subId) { } @Override - public String encode() throws JsonProcessingException { - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( - JsonNodeFactory.instance.arrayNode() - .add(getCommand()) - .add(getSubscriptionId())); + public String encode() throws EventEncodingException { + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( + JsonNodeFactory.instance.arrayNode() + .add(getCommand()) + .add(getSubscriptionId())); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode eose message", e); + } } public static T decode(@NonNull Object arg) { diff --git a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java index 75af5ac4f..daef640b4 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java @@ -49,25 +49,25 @@ public EventMessage(@NonNull IEvent event, @NonNull String subscriptionId) { } @Override - public String encode() throws JsonProcessingException { + public String encode() throws EventEncodingException { var arrayNode = JsonNodeFactory.instance.arrayNode().add(getCommand()); Optional.ofNullable(getSubscriptionId()) .ifPresent(arrayNode::add); try { arrayNode.add(ENCODER_MAPPER_BLACKBIRD.readTree( new BaseEventEncoder<>((BaseEvent) getEvent()).encode())); - } catch (EventEncodingException e) { - throw new IllegalStateException("Failed to encode event", e); + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(arrayNode); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode event message", e); } - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(arrayNode); } - public static T decode(@NonNull String jsonString) { + public static T decode(@NonNull String jsonString) throws EventEncodingException { try { Object[] msgArr = I_DECODER_MAPPER_BLACKBIRD.readValue(jsonString, Object[].class); return isEventWoSig.apply(msgArr) ? processEvent(msgArr[1]) : processEvent(msgArr); - } catch (Exception e) { - throw new AssertionError("Invalid argument: " + jsonString); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Invalid argument: " + jsonString, e); } } diff --git a/nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java b/nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java index 73e70fd35..9039299f7 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/GenericMessage.java @@ -10,6 +10,7 @@ import nostr.base.IElement; import nostr.base.IGenericElement; import nostr.event.BaseMessage; +import nostr.event.json.codec.EventEncodingException; import java.util.ArrayList; import java.util.List; @@ -47,11 +48,15 @@ public void addAttributes(List attributes) { } @Override - public String encode() throws JsonProcessingException { + public String encode() throws EventEncodingException { var encoderArrayNode = JsonNodeFactory.instance.arrayNode(); encoderArrayNode.add(getCommand()); getAttributes().stream().map(ElementAttribute::value).forEach(v -> encoderArrayNode.add(v.toString())); - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(encoderArrayNode); + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(encoderArrayNode); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode generic message", e); + } } public static T decode(@NonNull Object[] msgArr) { diff --git a/nostr-java-event/src/main/java/nostr/event/message/NoticeMessage.java b/nostr-java-event/src/main/java/nostr/event/message/NoticeMessage.java index 683f05b6d..ec634807e 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/NoticeMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/NoticeMessage.java @@ -8,6 +8,7 @@ import lombok.Setter; import nostr.base.Command; import nostr.event.BaseMessage; +import nostr.event.json.codec.EventEncodingException; import static nostr.base.Encoder.ENCODER_MAPPER_BLACKBIRD; @@ -28,11 +29,15 @@ public NoticeMessage(@NonNull String message) { } @Override - public String encode() throws JsonProcessingException { - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( - JsonNodeFactory.instance.arrayNode() - .add(getCommand()) - .add(getMessage())); + public String encode() throws EventEncodingException { + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( + JsonNodeFactory.instance.arrayNode() + .add(getCommand()) + .add(getMessage())); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode notice message", e); + } } public static T decode(@NonNull Object arg) { diff --git a/nostr-java-event/src/main/java/nostr/event/message/OkMessage.java b/nostr-java-event/src/main/java/nostr/event/message/OkMessage.java index a6a636967..9765b2112 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/OkMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/OkMessage.java @@ -8,6 +8,7 @@ import lombok.Setter; import nostr.base.Command; import nostr.event.BaseMessage; +import nostr.event.json.codec.EventEncodingException; import static nostr.base.Encoder.ENCODER_MAPPER_BLACKBIRD; import static nostr.base.IDecoder.I_DECODER_MAPPER_BLACKBIRD; @@ -33,21 +34,25 @@ public OkMessage(String eventId, Boolean flag, String message) { } @Override - public String encode() throws JsonProcessingException { - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( - JsonNodeFactory.instance.arrayNode() - .add(getCommand()) - .add(getEventId()) - .add(getFlag()) - .add(getMessage())); + public String encode() throws EventEncodingException { + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( + JsonNodeFactory.instance.arrayNode() + .add(getCommand()) + .add(getEventId()) + .add(getFlag()) + .add(getMessage())); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode ok message", e); + } } - public static T decode(@NonNull String jsonString) { + public static T decode(@NonNull String jsonString) throws EventEncodingException { try { Object[] msgArr = I_DECODER_MAPPER_BLACKBIRD.readValue(jsonString, Object[].class); return (T) new OkMessage(msgArr[1].toString(), (Boolean) msgArr[2], msgArr[3].toString()); } catch (JsonProcessingException e) { - throw new AssertionError(e); + throw new EventEncodingException("Failed to decode ok message", e); } } } diff --git a/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java b/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java index a9646c09a..00c7ea266 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/RelayAuthenticationMessage.java @@ -8,6 +8,7 @@ import lombok.Setter; import nostr.base.Command; import nostr.event.BaseMessage; +import nostr.event.json.codec.EventEncodingException; import static nostr.base.Encoder.ENCODER_MAPPER_BLACKBIRD; @@ -28,11 +29,15 @@ public RelayAuthenticationMessage(String challenge) { } @Override - public String encode() throws JsonProcessingException { - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( - JsonNodeFactory.instance.arrayNode() - .add(getCommand()) - .add(getChallenge())); + public String encode() throws EventEncodingException { + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString( + JsonNodeFactory.instance.arrayNode() + .add(getCommand()) + .add(getChallenge())); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode relay authentication message", e); + } } public static T decode(@NonNull Object arg) { diff --git a/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java b/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java index b5820d46c..9ffbaa3a9 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java @@ -7,13 +7,13 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; -import lombok.SneakyThrows; import lombok.ToString; import nostr.base.Command; import nostr.event.BaseMessage; import nostr.event.filter.Filters; import nostr.event.json.codec.FiltersDecoder; import nostr.event.json.codec.FiltersEncoder; +import nostr.event.json.codec.EventEncodingException; import java.time.temporal.ValueRange; import java.util.List; @@ -49,7 +49,7 @@ public ReqMessage(@NonNull String subscriptionId, @NonNull List filters } @Override - public String encode() throws JsonProcessingException { + public String encode() throws EventEncodingException { var encoderArrayNode = JsonNodeFactory.instance.arrayNode(); encoderArrayNode .add(getCommand()) @@ -61,22 +61,26 @@ public String encode() throws JsonProcessingException { .map(ReqMessage::createJsonNode) .forEach(encoderArrayNode::add); - return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(encoderArrayNode); + try { + return ENCODER_MAPPER_BLACKBIRD.writeValueAsString(encoderArrayNode); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to encode req message", e); + } } - public static T decode(@NonNull Object subscriptionId, @NonNull String jsonString) throws JsonProcessingException { + public static T decode(@NonNull Object subscriptionId, @NonNull String jsonString) throws EventEncodingException { validateSubscriptionId(subscriptionId.toString()); return (T) new ReqMessage( subscriptionId.toString(), - getJsonFiltersList(jsonString).stream().map(filtersList -> + getJsonFiltersList(jsonString).stream().map(filtersList -> new FiltersDecoder().decode(filtersList)).toList()); } - private static JsonNode createJsonNode(String jsonNode) { + private static JsonNode createJsonNode(String jsonNode) throws EventEncodingException { try { return ENCODER_MAPPER_BLACKBIRD.readTree(jsonNode); } catch (JsonProcessingException e) { - throw new IllegalArgumentException(String.format("Malformed encoding ReqMessage json: [%s]", jsonNode), e); + throw new EventEncodingException(String.format("Malformed encoding ReqMessage json: [%s]", jsonNode), e); } } @@ -86,13 +90,20 @@ private static void validateSubscriptionId(String subscriptionId) { } } - private static List getJsonFiltersList(String jsonString) throws JsonProcessingException { - return IntStream.range(FILTERS_START_INDEX, I_DECODER_MAPPER_BLACKBIRD.readTree(jsonString).size()) - .mapToObj(idx -> readTree(jsonString, idx)).toList(); + private static List getJsonFiltersList(String jsonString) throws EventEncodingException { + try { + return IntStream.range(FILTERS_START_INDEX, I_DECODER_MAPPER_BLACKBIRD.readTree(jsonString).size()) + .mapToObj(idx -> readTree(jsonString, idx)).toList(); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Invalid ReqMessage filters json", e); + } } - @SneakyThrows - private static String readTree(String jsonString, int idx) { - return I_DECODER_MAPPER_BLACKBIRD.readTree(jsonString).get(idx).toString(); + private static String readTree(String jsonString, int idx) throws EventEncodingException { + try { + return I_DECODER_MAPPER_BLACKBIRD.readTree(jsonString).get(idx).toString(); + } catch (JsonProcessingException e) { + throw new EventEncodingException("Failed to read json tree", e); + } } } From be4d51b1e6a41852856550708c41506825fd132d Mon Sep 17 00:00:00 2001 From: Eric T Date: Fri, 22 Aug 2025 10:56:07 +0300 Subject: [PATCH 022/105] refactor: unify decoder interface --- .../src/main/java/nostr/base/IDecoder.java | 10 +++++++++- .../event/json/codec/EventEncodingException.java | 0 .../src/main/java/nostr/event/filter/Filters.java | 3 ++- .../nostr/event/json/codec/BaseMessageDecoder.java | 7 +++++++ .../java/nostr/event/json/codec/BaseTagDecoder.java | 7 +++++++ .../main/java/nostr/event/json/codec/FDecoder.java | 6 ------ .../java/nostr/event/json/codec/FiltersDecoder.java | 13 +++++++++++-- .../nostr/event/json/codec/GenericEventDecoder.java | 7 +++++++ .../nostr/event/json/codec/GenericTagDecoder.java | 7 +++++++ .../nostr/event/json/codec/Nip05ContentDecoder.java | 7 +++++++ 10 files changed, 57 insertions(+), 10 deletions(-) rename {nostr-java-event => nostr-java-base}/src/main/java/nostr/event/json/codec/EventEncodingException.java (100%) delete mode 100644 nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java diff --git a/nostr-java-base/src/main/java/nostr/base/IDecoder.java b/nostr-java-base/src/main/java/nostr/base/IDecoder.java index 5bf56e417..140413d2c 100644 --- a/nostr-java-base/src/main/java/nostr/base/IDecoder.java +++ b/nostr-java-base/src/main/java/nostr/base/IDecoder.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.module.blackbird.BlackbirdModule; +import nostr.event.json.codec.EventEncodingException; /** * @@ -13,6 +14,13 @@ public interface IDecoder { ObjectMapper I_DECODER_MAPPER_BLACKBIRD = JsonMapper.builder().addModule(new BlackbirdModule()).build().configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - T decode(String str); + /** + * Decodes a JSON string into an element. + * + * @param str JSON string to decode + * @return decoded element + * @throws EventEncodingException if decoding fails + */ + T decode(String str) throws EventEncodingException; } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java b/nostr-java-base/src/main/java/nostr/event/json/codec/EventEncodingException.java similarity index 100% rename from nostr-java-event/src/main/java/nostr/event/json/codec/EventEncodingException.java rename to nostr-java-base/src/main/java/nostr/event/json/codec/EventEncodingException.java diff --git a/nostr-java-event/src/main/java/nostr/event/filter/Filters.java b/nostr-java-event/src/main/java/nostr/event/filter/Filters.java index dbbfc525d..74f9f8fbc 100644 --- a/nostr-java-event/src/main/java/nostr/event/filter/Filters.java +++ b/nostr-java-event/src/main/java/nostr/event/filter/Filters.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.NonNull; import lombok.ToString; +import nostr.base.IElement; import java.util.List; import java.util.Map; @@ -14,7 +15,7 @@ @Getter @EqualsAndHashCode @ToString -public class Filters { +public class Filters implements IElement { public static final int DEFAULT_FILTERS_LIMIT = 10; private static final String FILTERS_EMPTY_ERROR = "Filters cannot be empty."; private static final String FILTER_KEY_ERROR = "Filter key for filterable [%s] is not defined"; diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java index 022cf0377..07792ee89 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java @@ -27,6 +27,13 @@ public class BaseMessageDecoder implements IDecoder { public static final int ARG_INDEX = 1; @Override + /** + * Decodes a Nostr protocol message from its JSON representation. + * + * @param jsonString JSON representation of the message + * @return decoded message + * @throws EventEncodingException if decoding fails + */ public T decode(@NonNull String jsonString) throws EventEncodingException { ValidNostrJsonStructure validNostrJsonStructure = validateProperlyFormedJson(jsonString); String command = validNostrJsonStructure.getCommand(); diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java index eab704aec..4afa27246 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseTagDecoder.java @@ -22,6 +22,13 @@ public BaseTagDecoder() { } @Override + /** + * Decodes the provided JSON string into a tag instance. + * + * @param jsonString JSON representation of the tag + * @return decoded tag + * @throws EventEncodingException if decoding fails + */ public T decode(String jsonString) throws EventEncodingException { try { return MAPPER_BLACKBIRD.readValue(jsonString, clazz); diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java deleted file mode 100644 index 276b3a4e3..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FDecoder.java +++ /dev/null @@ -1,6 +0,0 @@ -package nostr.event.json.codec; - -public interface FDecoder { - - T decode(String str) throws EventEncodingException; -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java index bd231b452..90ae0817c 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.NonNull; +import nostr.base.IDecoder; import nostr.event.filter.Filterable; import nostr.event.filter.Filters; import nostr.event.json.codec.EventEncodingException; @@ -19,8 +20,16 @@ * @author eric */ @Data -public class FiltersDecoder implements FDecoder { - +public class FiltersDecoder implements IDecoder { + + /** + * Decodes a JSON string of filters into a {@link Filters} object. + * + * @param jsonFiltersList JSON representation of filters + * @return decoded filters + * @throws EventEncodingException if decoding fails + */ + @Override public Filters decode(@NonNull String jsonFiltersList) throws EventEncodingException { try { final List filterables = new ArrayList<>(); diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java index 551c9ded2..18445dc2d 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java @@ -25,6 +25,13 @@ public GenericEventDecoder(Class clazz) { } @Override + /** + * Decodes a JSON string into a {@link GenericEvent} instance. + * + * @param jsonEvent JSON representation of the event + * @return decoded event + * @throws EventEncodingException if decoding fails + */ public T decode(String jsonEvent) throws EventEncodingException { try { I_DECODER_MAPPER_BLACKBIRD.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java index ae7883c58..fc82823dd 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericTagDecoder.java @@ -26,6 +26,13 @@ public GenericTagDecoder(@NonNull Class clazz) { } @Override + /** + * Decodes a JSON array into a {@link GenericTag} instance. + * + * @param json JSON array string representing the tag + * @return decoded tag + * @throws EventEncodingException if decoding fails + */ public T decode(@NonNull String json) throws EventEncodingException { try { String[] jsonElements = I_DECODER_MAPPER_BLACKBIRD.readValue(json, String[].class); diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java index 4b2f1cfe5..188f68752 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/Nip05ContentDecoder.java @@ -22,6 +22,13 @@ public Nip05ContentDecoder() { } @Override + /** + * Decodes a JSON representation of NIP-05 content. + * + * @param jsonContent JSON content string + * @return decoded content + * @throws EventEncodingException if decoding fails + */ public T decode(String jsonContent) throws EventEncodingException { try { return MAPPER_BLACKBIRD.readValue(jsonContent, clazz); From 3e0994a0e2735bf305ef384f91657927fe24a531 Mon Sep 17 00:00:00 2001 From: erict875 Date: Fri, 22 Aug 2025 21:40:53 +0100 Subject: [PATCH 023/105] docs: update AGENTS.md to include guideline for adding test descriptions --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index a17bf4c0d..9d2fe49fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,6 +112,7 @@ The URL format for the NIPs is https://github.com/nostr-protocol/nips/blob/maste - Always make sure that the events are compliant with the Nostr protocol specifications, and that the events are valid according to the NIP specifications. - Always remove unused imports - When creating a branch, bump up the version in the pom files to the next minor version. +- Always add a description of the test as a comment at the top of the test method. ## Pull Requests From b8accba82eee6891405546c464d89716aff56153 Mon Sep 17 00:00:00 2001 From: erict875 Date: Fri, 22 Aug 2025 22:17:03 +0100 Subject: [PATCH 024/105] add GitHub action for resolving Codex-labeled issues --- .github/workflows/codex.yml | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/codex.yml diff --git a/.github/workflows/codex.yml b/.github/workflows/codex.yml new file mode 100644 index 000000000..eb7020faa --- /dev/null +++ b/.github/workflows/codex.yml @@ -0,0 +1,62 @@ +name: Codex Issue Resolution + +on: + issues: + types: [labeled] + +jobs: + codex-job: + if: contains(github.event.label.name, 'codex') + runs-on: ubuntu-latest + + steps: + - name: Extract issue info + id: issue + run: | + echo "title=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT + echo "body=${{ github.event.issue.body }}" >> $GITHUB_OUTPUT + + - name: Determine model + id: model + run: | + # Default model if not provided as a secret + MODEL="${{ secrets.OPENAI_MODEL }}" + if [ -z "$MODEL" ]; then + MODEL="gpt-4.1-mini" + echo "No OPENAI_MODEL secret found. Falling back to default: $MODEL" + fi + echo "model=$MODEL" >> $GITHUB_OUTPUT + + - name: Call OpenAI + id: codex + run: | + response=$(curl -s https://api.openai.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${{ secrets.OPENAI_API_KEY }}" \ + -d "{ + \"model\": \"${{ steps.model.outputs.model }}\", + \"messages\": [ + {\"role\": \"system\", \"content\": \"You are an AI assistant helping resolve GitHub issues.\"}, + {\"role\": \"user\", \"content\": \"Issue Title: ${{ steps.issue.outputs.title }}\nIssue Body: ${{ steps.issue.outputs.body }}\n\nPlease propose a resolution.\"} + ], + \"max_tokens\": 300 + }" | jq -r '.choices[0].message.content') + + echo "response<> $GITHUB_OUTPUT + echo "$response" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Post comment with Codex resolution + run: | + gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments \ + -f body="💡 **Codex Suggestion (Model: ${{ steps.model.outputs.model }})**\n\n${{ steps.codex.outputs.response }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update labels + run: | + gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/labels/codex -X DELETE || true + gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/labels \ + -f labels='["codex-resolved"]' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5d1cb686fc98cdbff71fbe748a9b5e3ea3b25390 Mon Sep 17 00:00:00 2001 From: Eric T Date: Sat, 23 Aug 2025 00:32:02 +0300 Subject: [PATCH 025/105] ci: restrict codex workflow to CI-created issues --- .github/workflows/codex.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codex.yml b/.github/workflows/codex.yml index eb7020faa..1a8a65e65 100644 --- a/.github/workflows/codex.yml +++ b/.github/workflows/codex.yml @@ -6,7 +6,7 @@ on: jobs: codex-job: - if: contains(github.event.label.name, 'codex') + if: contains(github.event.label.name, 'codex') && github.event.issue.user.login == 'github-actions[bot]' runs-on: ubuntu-latest steps: From 7f6132b7e38f79c02e71dbe59b9a0dd4966b4383 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 03:48:47 +0000 Subject: [PATCH 026/105] chore(deps): bump spring-boot.version from 3.5.4 to 3.5.5 Bumps `spring-boot.version` from 3.5.4 to 3.5.5. Updates `org.springframework.boot:spring-boot-starter-websocket` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-test` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-websocket dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28e9ee831..9b2ff8a58 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ 0.2.2.1 21 - 3.5.4 + 3.5.5 ${java.version} ${java.version} UTF-8 From 827e3d16250bba0dab9e710c148e6482d06efe4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 04:13:55 +0000 Subject: [PATCH 027/105] chore(deps): bump org.apache.maven.plugins:maven-gpg-plugin Bumps [org.apache.maven.plugins:maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) from 1.5 to 3.2.8. - [Release notes](https://github.com/apache/maven-gpg-plugin/releases) - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-1.5...maven-gpg-plugin-3.2.8) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-version: 3.2.8 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28e9ee831..d3025eefe 100644 --- a/pom.xml +++ b/pom.xml @@ -101,7 +101,7 @@ 1.7.2 3.14.0 3.5.3 - 1.5 + 3.2.8 3.11.3 3.3.1 3.5.3 From 10f063f3c363b715c8fd8c0fb1720f0981550d85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 04:18:38 +0000 Subject: [PATCH 028/105] chore(deps): bump actions/setup-java from 4 to 5 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 659451481..94e2a9fb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: issues: write steps: - uses: actions/checkout@v5 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 75c4ce406..3a75425f1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,7 +18,7 @@ jobs: security-events: write steps: - uses: actions/checkout@v5 - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' From 8f92263a1be2d219fd92059937ab8c4c3da86b07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 04:37:07 +0000 Subject: [PATCH 029/105] chore(deps): bump actions/checkout from 3 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pr-quality-gate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-quality-gate.yml b/.github/workflows/pr-quality-gate.yml index 4d0ddd1ec..9d5413191 100644 --- a/.github/workflows/pr-quality-gate.yml +++ b/.github/workflows/pr-quality-gate.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.actor != 'dependabot[bot]' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Verify Copilot instructions run: test -s .github/copilot-instructions.md - name: Run expertise standard checks From a7842f7040f2b646298420a1271937a3934a5175 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 04:38:38 +0000 Subject: [PATCH 030/105] chore(deps): bump org.springframework.boot:spring-boot-starter-parent Bumps [org.springframework.boot:spring-boot-starter-parent](https://github.com/spring-projects/spring-boot) from 3.5.4 to 3.5.5. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-parent dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 28e9ee831..b474051aa 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.4 + 3.5.5 ${project.artifactId} From 9f3bae5dfea30ffcec43e978f13f3a0fa1fff49b Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 28 Aug 2025 00:20:03 +0300 Subject: [PATCH 031/105] ci: configure release-please --- .github/workflows/release-please.yml | 22 ++++++++++ .release-please-manifest.json | 12 ++++++ release-please-config.json | 63 ++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 000000000..e4673800d --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,22 @@ +name: release-please + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + command: manifest + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + token: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..b2157d211 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,12 @@ +{ + ".": "0.2.2", + "nostr-java-base": "0.2.2", + "nostr-java-crypto": "0.2.2", + "nostr-java-event": "0.2.2", + "nostr-java-examples": "0.2.2", + "nostr-java-id": "0.2.2", + "nostr-java-util": "0.2.2", + "nostr-java-client": "0.2.2", + "nostr-java-api": "0.2.2", + "nostr-java-encryption": "0.2.2" +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 000000000..6d0dcb167 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,63 @@ +{ + "packages": { + ".": { + "release-type": "maven", + "package-name": "nostr-java", + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-base": { + "release-type": "maven", + "package-name": "nostr-java-base", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-crypto": { + "release-type": "maven", + "package-name": "nostr-java-crypto", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-event": { + "release-type": "maven", + "package-name": "nostr-java-event", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-examples": { + "release-type": "maven", + "package-name": "nostr-java-examples", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-id": { + "release-type": "maven", + "package-name": "nostr-java-id", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-util": { + "release-type": "maven", + "package-name": "nostr-java-util", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-client": { + "release-type": "maven", + "package-name": "nostr-java-client", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-api": { + "release-type": "maven", + "package-name": "nostr-java-api", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + }, + "nostr-java-encryption": { + "release-type": "maven", + "package-name": "nostr-java-encryption", + "include-component-in-tag": true, + "changelog-path": "CHANGELOG.md" + } + } +} From 2d1dd2e29465d69db381d20e1ad8e5788e508263 Mon Sep 17 00:00:00 2001 From: Eric T Date: Thu, 28 Aug 2025 00:25:26 +0300 Subject: [PATCH 032/105] ci: add workflow to publish artifacts --- .github/workflows/publish.yml | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..a0e011de2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Publish + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + - name: Configure Maven + run: | + mkdir -p ~/.m2 + cat < ~/.m2/settings.xml + + + + reposilite-releases + ${MAVEN_USERNAME} + ${MAVEN_PASSWORD} + + + reposilite-snapshots + ${MAVEN_USERNAME} + ${MAVEN_PASSWORD} + + + + EOF + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + - name: Publish artifacts + run: ./mvnw -q deploy + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} From f3851221dbc049a61420c47da328c084e13a2a44 Mon Sep 17 00:00:00 2001 From: erict875 Date: Thu, 28 Aug 2025 18:22:45 +0100 Subject: [PATCH 033/105] chore: add maven configuration file - Introduced a new maven.config file to set the thread count for builds. --- .mvn/maven.config | 1 + 1 file changed, 1 insertion(+) create mode 100644 .mvn/maven.config diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 000000000..9159a424c --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1 @@ +-T 2 \ No newline at end of file From 3bf07b52418b864ea0dd33890b1c1512fef1d8c5 Mon Sep 17 00:00:00 2001 From: erict875 Date: Thu, 28 Aug 2025 18:24:37 +0100 Subject: [PATCH 034/105] chore: add commitlint configuration for commit message validation - Introduced a commitlint configuration file to enforce commit message standards. - This includes rules for header length, type casing, and subject formatting. --- .commitlintrc.yml | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .commitlintrc.yml diff --git a/.commitlintrc.yml b/.commitlintrc.yml new file mode 100644 index 000000000..a1e61d98c --- /dev/null +++ b/.commitlintrc.yml @@ -0,0 +1,54 @@ +--- +extends: '@commitlint/config-conventional' + +rules: + # See: https://commitlint.js.org/reference/rules.html + # + # Rules are made up by a name and a configuration array. The configuration + # array contains: + # + # * Severity [0..2]: 0 disable rule, 1 warning if violated, or 2 error if + # violated + # * Applicability [always|never]: never inverts the rule + # * Value: value to use for this rule (if applicable) + # + # Run `npx commitlint --print-config` to see the current setting for all rules. + + header-max-length: [2, always, 100] # Header can not exceed 100 chars + + type-case: [2, always, lower-case] # Type must be lower case + type-empty: [2, never] # Type must not be empty + + # Supported conventional commit types + type-enum: [2, always, [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test]] + + scope-case: [2, always, lower-case] # Scope must be lower case + + # Error if subject is one of these cases (encourages lower-case) + subject-case: [2, never, [sentence-case, start-case, pascal-case, upper-case]] + subject-empty: [2, never] # Subject must not be empty + subject-full-stop: [2, never, "."] # Subject must not end with a period + + body-leading-blank: [2, always] # Body must have a blank line before it + body-max-line-length: [2, always, 100] # Body lines can not exceed 100 chars + + footer-leading-blank: [2, always] # Footer must have a blank line before it + footer-max-line-length: [2, always, 100] # Footer lines can not exceed 100 chars + + # ------------------------------------------------------------ + # BREAKING CHANGES — guidance (informational; not enforced): + # + # How to mark a breaking change (either or both): + # 1) Put "!" in the header after the type or scope, e.g.: + # feat!: drop support for node 14 + # refactor(auth)!: remove legacy token flow + # + # 2) Add a footer that starts with: + # BREAKING CHANGE: + # Follow with impact/migration details, each line ≤ 100 chars. + # + # This config already allows both patterns via @commitlint/config-conventional. + # Note: commitlint cannot (in YAML) *require* a BREAKING CHANGE footer only + # when "!" is used. If you need that kind of conditional enforcement, use a + # JS config (*.cjs) with a custom rule. + # ------------------------------------------------------------ From 675c0a7682b40aeeef844cc4e02c955b6ece3cce Mon Sep 17 00:00:00 2001 From: erict875 Date: Thu, 28 Aug 2025 18:24:46 +0100 Subject: [PATCH 035/105] docs: add commit message guidelines for consistency --- commit_instructions.md | 164 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 commit_instructions.md diff --git a/commit_instructions.md b/commit_instructions.md new file mode 100644 index 000000000..ed53e4c88 --- /dev/null +++ b/commit_instructions.md @@ -0,0 +1,164 @@ +# Git Commit Instructions (commitlint-compatible) + +You are writing Git commit messages. Follow these rules strictly: + +## FORMAT + + (): + + + +