Add NIP-34 git stuff implementation with event creation helpers for repository announcements, state, patches, PRs, issues, status, grasp lists, and nostr:// URL parsing
This commit is contained in:
@@ -51,6 +51,7 @@ else()
|
||||
nostr_core/nip017.c
|
||||
nostr_core/nip019.c
|
||||
nostr_core/nip021.c
|
||||
nostr_core/nip034.c
|
||||
nostr_core/nip042.c
|
||||
nostr_core/nip044.c
|
||||
nostr_core/nip046.c
|
||||
|
||||
@@ -43,7 +43,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-31](nips/31.md) - Dealing with Unknown Events
|
||||
- [ ] [NIP-32](nips/32.md) - Labeling
|
||||
- [ ] [NIP-33](nips/33.md) - Parameterized Replaceable Events
|
||||
- [ ] [NIP-34](nips/34.md) - `git` stuff
|
||||
- [x] [NIP-34](nips/34.md) - `git` stuff
|
||||
- [ ] [NIP-35](nips/35.md) - Torrents
|
||||
- [ ] [NIP-36](nips/36.md) - Sensitive Content / Content Warning
|
||||
- [ ] [NIP-37](nips/37.md) - Draft Events
|
||||
@@ -96,7 +96,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
|
||||
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented | ➖ Not Applicable
|
||||
|
||||
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
|
||||
**Implementation Summary:** 14 of 96+ NIPs fully implemented (14.6%)
|
||||
|
||||
|
||||
## 📦 Quick Start
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
/*
|
||||
* NIP-34: `git` stuff Implementation
|
||||
* https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "nip034.h"
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
#include "nostr_common.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
// Forward declarations for crypto functions
|
||||
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
|
||||
|
||||
/**
|
||||
* Helper: add a string array tag to a cJSON tags array.
|
||||
* Creates ["<name>", "<value>"] and appends it.
|
||||
*/
|
||||
static cJSON* add_simple_tag(cJSON* tags, const char* name, const char* value) {
|
||||
if (!tags || !name || !value) return NULL;
|
||||
cJSON* tag = cJSON_CreateArray();
|
||||
if (!tag) return NULL;
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(name));
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
|
||||
cJSON_AddItemToArray(tags, tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: add a multi-value tag (same name, multiple values).
|
||||
* Creates ["<name>", "<value>"] for each value and appends it.
|
||||
*/
|
||||
static int add_multi_tag(cJSON* tags, const char* name, const char** values, int count) {
|
||||
if (!tags || !name || !values || count <= 0) return -1;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (values[i]) {
|
||||
cJSON* tag = cJSON_CreateArray();
|
||||
if (!tag) return -1;
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(name));
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(values[i]));
|
||||
cJSON_AddItemToArray(tags, tag);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: add a three-value tag.
|
||||
* Creates ["<name>", "<value1>", "<value2>"] and appends it.
|
||||
*/
|
||||
static cJSON* add_tag_with_extra(cJSON* tags, const char* name, const char* value1, const char* value2) {
|
||||
if (!tags || !name || !value1) return NULL;
|
||||
cJSON* tag = cJSON_CreateArray();
|
||||
if (!tag) return NULL;
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(name));
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(value1));
|
||||
if (value2) {
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(value2));
|
||||
}
|
||||
cJSON_AddItemToArray(tags, tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create a repository announcement event (kind 30617)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_repo_announcement(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_id,
|
||||
const char* name,
|
||||
const char* description,
|
||||
const char* web_url,
|
||||
const char** clone_urls,
|
||||
int clone_count,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
const char* euc_commit,
|
||||
const char** maintainers,
|
||||
int maintainer_count
|
||||
) {
|
||||
if (!signer || !repo_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// d tag (required)
|
||||
add_simple_tag(tags, "d", repo_id);
|
||||
|
||||
// name tag (optional)
|
||||
if (name) {
|
||||
add_simple_tag(tags, "name", name);
|
||||
}
|
||||
|
||||
// description tag (optional)
|
||||
if (description) {
|
||||
add_simple_tag(tags, "description", description);
|
||||
}
|
||||
|
||||
// web tag (optional, can be multiple)
|
||||
if (web_url) {
|
||||
add_simple_tag(tags, "web", web_url);
|
||||
}
|
||||
|
||||
// clone tags (optional, can be multiple)
|
||||
if (clone_urls && clone_count > 0) {
|
||||
add_multi_tag(tags, "clone", clone_urls, clone_count);
|
||||
}
|
||||
|
||||
// relays tags (optional, can be multiple)
|
||||
if (relay_urls && relay_count > 0) {
|
||||
add_multi_tag(tags, "relays", relay_urls, relay_count);
|
||||
}
|
||||
|
||||
// r tag with euc marker (optional)
|
||||
if (euc_commit) {
|
||||
cJSON* r_tag = cJSON_CreateArray();
|
||||
if (r_tag) {
|
||||
cJSON_AddItemToArray(r_tag, cJSON_CreateString("r"));
|
||||
cJSON_AddItemToArray(r_tag, cJSON_CreateString(euc_commit));
|
||||
cJSON_AddItemToArray(r_tag, cJSON_CreateString("euc"));
|
||||
cJSON_AddItemToArray(tags, r_tag);
|
||||
}
|
||||
}
|
||||
|
||||
// maintainers tags (optional, can be multiple)
|
||||
if (maintainers && maintainer_count > 0) {
|
||||
add_multi_tag(tags, "maintainers", maintainers, maintainer_count);
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP34_KIND_REPO_ANNOUNCEMENT, "", tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create a repository state announcement event (kind 30618)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_repo_state(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_id,
|
||||
const char** ref_names,
|
||||
const char** ref_values,
|
||||
int ref_count
|
||||
) {
|
||||
if (!signer || !repo_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// d tag (required)
|
||||
add_simple_tag(tags, "d", repo_id);
|
||||
|
||||
// refs tags (optional, can be multiple)
|
||||
if (ref_names && ref_values && ref_count > 0) {
|
||||
for (int i = 0; i < ref_count; i++) {
|
||||
if (ref_names[i] && ref_values[i]) {
|
||||
add_simple_tag(tags, ref_names[i], ref_values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP34_KIND_REPO_STATE, "", tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create a patch event (kind 1617)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_patch(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* patch_content,
|
||||
const char* commit_id,
|
||||
const char* parent_commit_id,
|
||||
const char* commit_pgp_sig,
|
||||
const char* committer_name,
|
||||
const char* committer_email,
|
||||
const char* committer_timestamp,
|
||||
int committer_tz_offset,
|
||||
int is_root,
|
||||
int is_root_revision
|
||||
) {
|
||||
if (!signer || !repo_address || !patch_content) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// a tag pointing to the repository announcement
|
||||
add_simple_tag(tags, "a", repo_address);
|
||||
|
||||
// r tag with euc (optional)
|
||||
if (euc_commit) {
|
||||
add_tag_with_extra(tags, "r", euc_commit, NULL);
|
||||
}
|
||||
|
||||
// t tags for root/root-revision markers
|
||||
if (is_root) {
|
||||
add_simple_tag(tags, "t", "root");
|
||||
}
|
||||
if (is_root_revision) {
|
||||
add_simple_tag(tags, "t", "root-revision");
|
||||
}
|
||||
|
||||
// commit tag (optional, for stable commit IDs)
|
||||
if (commit_id) {
|
||||
add_simple_tag(tags, "commit", commit_id);
|
||||
}
|
||||
|
||||
// parent-commit tag (optional)
|
||||
if (parent_commit_id) {
|
||||
add_simple_tag(tags, "parent-commit", parent_commit_id);
|
||||
}
|
||||
|
||||
// commit-pgp-sig tag (optional)
|
||||
if (commit_pgp_sig) {
|
||||
add_simple_tag(tags, "commit-pgp-sig", commit_pgp_sig);
|
||||
}
|
||||
|
||||
// committer tag (optional)
|
||||
if (committer_name && committer_email && committer_timestamp) {
|
||||
char tz_str[16];
|
||||
snprintf(tz_str, sizeof(tz_str), "%d", committer_tz_offset);
|
||||
cJSON* committer_tag = cJSON_CreateArray();
|
||||
if (committer_tag) {
|
||||
cJSON_AddItemToArray(committer_tag, cJSON_CreateString("committer"));
|
||||
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(committer_name));
|
||||
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(committer_email));
|
||||
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(committer_timestamp));
|
||||
cJSON_AddItemToArray(committer_tag, cJSON_CreateString(tz_str));
|
||||
cJSON_AddItemToArray(tags, committer_tag);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP34_KIND_PATCH, patch_content, tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create a pull request event (kind 1618)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_pull_request(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* subject,
|
||||
const char* content,
|
||||
const char* tip_commit,
|
||||
const char** clone_urls,
|
||||
int clone_count,
|
||||
const char* branch_name,
|
||||
const char* root_patch_event_id,
|
||||
const char* merge_base
|
||||
) {
|
||||
if (!signer || !repo_address) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* body = content ? content : "";
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// a tag pointing to the repository announcement
|
||||
add_simple_tag(tags, "a", repo_address);
|
||||
|
||||
// r tag with euc (optional)
|
||||
if (euc_commit) {
|
||||
add_tag_with_extra(tags, "r", euc_commit, NULL);
|
||||
}
|
||||
|
||||
// subject tag (optional)
|
||||
if (subject) {
|
||||
add_simple_tag(tags, "subject", subject);
|
||||
}
|
||||
|
||||
// c tag for tip commit (optional)
|
||||
if (tip_commit) {
|
||||
add_simple_tag(tags, "c", tip_commit);
|
||||
}
|
||||
|
||||
// clone tags (optional, can be multiple)
|
||||
if (clone_urls && clone_count > 0) {
|
||||
add_multi_tag(tags, "clone", clone_urls, clone_count);
|
||||
}
|
||||
|
||||
// branch-name tag (optional)
|
||||
if (branch_name) {
|
||||
add_simple_tag(tags, "branch-name", branch_name);
|
||||
}
|
||||
|
||||
// e tag for revision of existing patch (optional)
|
||||
if (root_patch_event_id) {
|
||||
add_simple_tag(tags, "e", root_patch_event_id);
|
||||
}
|
||||
|
||||
// merge-base tag (optional)
|
||||
if (merge_base) {
|
||||
add_simple_tag(tags, "merge-base", merge_base);
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP34_KIND_PULL_REQUEST, body, tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create a pull request update event (kind 1619)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_pr_update(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* pr_event_id,
|
||||
const char* pr_author,
|
||||
const char* tip_commit,
|
||||
const char** clone_urls,
|
||||
int clone_count,
|
||||
const char* merge_base
|
||||
) {
|
||||
if (!signer || !repo_address || !pr_event_id || !pr_author) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// a tag pointing to the repository announcement
|
||||
add_simple_tag(tags, "a", repo_address);
|
||||
|
||||
// r tag with euc (optional)
|
||||
if (euc_commit) {
|
||||
add_tag_with_extra(tags, "r", euc_commit, NULL);
|
||||
}
|
||||
|
||||
// NIP-22 tags for the parent PR
|
||||
add_simple_tag(tags, "E", pr_event_id);
|
||||
add_simple_tag(tags, "P", pr_author);
|
||||
|
||||
// c tag for updated tip commit (optional)
|
||||
if (tip_commit) {
|
||||
add_simple_tag(tags, "c", tip_commit);
|
||||
}
|
||||
|
||||
// clone tags (optional, can be multiple)
|
||||
if (clone_urls && clone_count > 0) {
|
||||
add_multi_tag(tags, "clone", clone_urls, clone_count);
|
||||
}
|
||||
|
||||
// merge-base tag (optional)
|
||||
if (merge_base) {
|
||||
add_simple_tag(tags, "merge-base", merge_base);
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP34_KIND_PR_UPDATE, "", tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create an issue event (kind 1621)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_issue(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* subject,
|
||||
const char* content,
|
||||
const char** labels,
|
||||
int label_count
|
||||
) {
|
||||
if (!signer || !repo_address) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* body = content ? content : "";
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// a tag pointing to the repository announcement
|
||||
add_simple_tag(tags, "a", repo_address);
|
||||
|
||||
// subject tag (optional)
|
||||
if (subject) {
|
||||
add_simple_tag(tags, "subject", subject);
|
||||
}
|
||||
|
||||
// t tags for labels (optional, can be multiple)
|
||||
if (labels && label_count > 0) {
|
||||
add_multi_tag(tags, "t", labels, label_count);
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP34_KIND_ISSUE, body, tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create a status event (kind 1630-1633)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_status(
|
||||
nostr_signer_t* signer,
|
||||
int kind,
|
||||
const char* root_id,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* content
|
||||
) {
|
||||
if (!signer || !root_id) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate kind is a valid status kind
|
||||
if (kind != NOSTR_NIP34_KIND_STATUS_OPEN &&
|
||||
kind != NOSTR_NIP34_KIND_STATUS_MERGED &&
|
||||
kind != NOSTR_NIP34_KIND_STATUS_CLOSED &&
|
||||
kind != NOSTR_NIP34_KIND_STATUS_DRAFT) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* body = content ? content : "";
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// e tag pointing to the root issue, PR, or patch
|
||||
add_tag_with_extra(tags, "e", root_id, "root");
|
||||
|
||||
// a tag for the repository (optional)
|
||||
if (repo_address) {
|
||||
add_simple_tag(tags, "a", repo_address);
|
||||
}
|
||||
|
||||
// r tag with euc (optional)
|
||||
if (euc_commit) {
|
||||
add_tag_with_extra(tags, "r", euc_commit, NULL);
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
kind, body, tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Create a user grasp list event (kind 10317)
|
||||
*/
|
||||
cJSON* nostr_nip34_create_grasp_list(
|
||||
nostr_signer_t* signer,
|
||||
const char** grasp_urls,
|
||||
int grasp_count
|
||||
) {
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
// g tags for grasp server URLs (optional, can be multiple)
|
||||
if (grasp_urls && grasp_count > 0) {
|
||||
add_multi_tag(tags, "g", grasp_urls, grasp_count);
|
||||
}
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP34_KIND_GRASP_LIST, "", tags, signer, 0
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Percent-decode a string in-place.
|
||||
* Returns the number of characters after decoding.
|
||||
*/
|
||||
static int percent_decode_inplace(char* s) {
|
||||
if (!s) return 0;
|
||||
char* src = s;
|
||||
char* dst = s;
|
||||
while (*src) {
|
||||
if (*src == '%' && *(src+1) && *(src+2)) {
|
||||
char hex[3] = { src[1], src[2], '\0' };
|
||||
*dst++ = (char)strtol(hex, NULL, 16);
|
||||
src += 3;
|
||||
} else {
|
||||
*dst++ = *src++;
|
||||
}
|
||||
}
|
||||
*dst = '\0';
|
||||
return (int)(dst - s);
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-34: Parse a nostr:// git URL into its components
|
||||
*
|
||||
* Supports formats:
|
||||
* nostr://<naddr>
|
||||
* nostr://<npub|nip05>/<identifier>
|
||||
* nostr://<npub|nip05>/<relay-hint>/<identifier>
|
||||
*
|
||||
* Uses manual character scanning (no strtok) to avoid reentrancy issues.
|
||||
*/
|
||||
int nostr_nip34_parse_nostr_url(
|
||||
const char* url,
|
||||
char* npub_out,
|
||||
char* identifier_out,
|
||||
char* relay_hint_out
|
||||
) {
|
||||
if (!url || !npub_out || !identifier_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check for nostr:// prefix
|
||||
const char* prefix = "nostr://";
|
||||
size_t prefix_len = strlen(prefix);
|
||||
|
||||
if (strncmp(url, prefix, prefix_len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* p = url + prefix_len;
|
||||
if (*p == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Make a mutable copy of the path
|
||||
char* path_copy = strdup(p);
|
||||
if (!path_copy) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Find the three possible segments separated by '/'
|
||||
// Segment 0: npub or nip05 address (up to first '/')
|
||||
// Segment 1: relay hint (between first and second '/')
|
||||
// Segment 2: identifier (after second '/')
|
||||
char* segments[3] = {NULL, NULL, NULL};
|
||||
int seg_count = 0;
|
||||
|
||||
segments[0] = path_copy;
|
||||
seg_count = 1;
|
||||
|
||||
for (char* q = path_copy; *q; q++) {
|
||||
if (*q == '/') {
|
||||
*q = '\0'; // null-terminate current segment
|
||||
if (seg_count < 3) {
|
||||
segments[seg_count] = q + 1;
|
||||
seg_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Percent-decode each segment in-place
|
||||
for (int i = 0; i < seg_count; i++) {
|
||||
if (segments[i]) {
|
||||
percent_decode_inplace(segments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy segments to output buffers
|
||||
if (seg_count >= 1 && segments[0]) {
|
||||
strncpy(npub_out, segments[0], 255);
|
||||
npub_out[255] = '\0';
|
||||
} else {
|
||||
npub_out[0] = '\0';
|
||||
}
|
||||
|
||||
if (seg_count >= 2 && segments[1]) {
|
||||
strncpy(identifier_out, segments[1], 255);
|
||||
identifier_out[255] = '\0';
|
||||
} else {
|
||||
identifier_out[0] = '\0';
|
||||
}
|
||||
|
||||
if (relay_hint_out) {
|
||||
if (seg_count >= 3 && segments[2]) {
|
||||
// With 3 segments, segment[1] is the relay hint, segment[2] is the identifier
|
||||
strncpy(relay_hint_out, segments[1], 511);
|
||||
relay_hint_out[511] = '\0';
|
||||
// Move identifier from segment[2] to identifier_out
|
||||
strncpy(identifier_out, segments[2], 255);
|
||||
identifier_out[255] = '\0';
|
||||
} else {
|
||||
relay_hint_out[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
// No relay_hint_out provided, but we still need to handle 3-segment case
|
||||
if (seg_count >= 3 && segments[2]) {
|
||||
strncpy(identifier_out, segments[2], 255);
|
||||
identifier_out[255] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
free(path_copy);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* NIP-34: `git` stuff
|
||||
* https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||
*
|
||||
* Defines event kinds and helpers for code collaboration using git on Nostr.
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_NIP034_H
|
||||
#define NOSTR_NIP034_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// NIP-34 event kinds
|
||||
#define NOSTR_NIP34_KIND_REPO_ANNOUNCEMENT 30617
|
||||
#define NOSTR_NIP34_KIND_REPO_STATE 30618
|
||||
#define NOSTR_NIP34_KIND_PATCH 1617
|
||||
#define NOSTR_NIP34_KIND_PULL_REQUEST 1618
|
||||
#define NOSTR_NIP34_KIND_PR_UPDATE 1619
|
||||
#define NOSTR_NIP34_KIND_ISSUE 1621
|
||||
#define NOSTR_NIP34_KIND_STATUS_OPEN 1630
|
||||
#define NOSTR_NIP34_KIND_STATUS_MERGED 1631
|
||||
#define NOSTR_NIP34_KIND_STATUS_CLOSED 1632
|
||||
#define NOSTR_NIP34_KIND_STATUS_DRAFT 1633
|
||||
#define NOSTR_NIP34_KIND_GRASP_LIST 10317
|
||||
|
||||
/**
|
||||
* NIP-34: Create a repository announcement event (kind 30617)
|
||||
*
|
||||
* Announces a git repository's existence on Nostr.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param repo_id Repository identifier (d tag, usually kebab-case short name)
|
||||
* @param name Human-readable project name (can be NULL)
|
||||
* @param description Brief human-readable project description (can be NULL)
|
||||
* @param web_url URL for browsing the repository (can be NULL)
|
||||
* @param clone_urls Array of git clone URLs (can be NULL)
|
||||
* @param clone_count Number of clone URLs
|
||||
* @param relay_urls Array of relay URLs for patches and issues (can be NULL)
|
||||
* @param relay_count Number of relay URLs
|
||||
* @param euc_commit Earliest unique commit ID for fork grouping (can be NULL)
|
||||
* @param maintainers Array of maintainer pubkeys (can be NULL)
|
||||
* @param maintainer_count Number of maintainers
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_repo_announcement(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_id,
|
||||
const char* name,
|
||||
const char* description,
|
||||
const char* web_url,
|
||||
const char** clone_urls,
|
||||
int clone_count,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
const char* euc_commit,
|
||||
const char** maintainers,
|
||||
int maintainer_count
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Create a repository state announcement event (kind 30618)
|
||||
*
|
||||
* Announces the current state of branches and tags in a repository.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param repo_id Repository identifier (matches the d tag in the announcement)
|
||||
* @param ref_names Array of reference names (e.g., "refs/heads/master")
|
||||
* @param ref_values Array of reference values (commit hashes or "ref: refs/heads/<branch>")
|
||||
* @param ref_count Number of references
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_repo_state(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_id,
|
||||
const char** ref_names,
|
||||
const char** ref_values,
|
||||
int ref_count
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Create a patch event (kind 1617)
|
||||
*
|
||||
* Contains the output of git format-patch for proposing changes.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
|
||||
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
|
||||
* @param patch_content The git format-patch output
|
||||
* @param commit_id Current commit ID for stable commit IDs (can be NULL)
|
||||
* @param parent_commit_id Parent commit ID (can be NULL)
|
||||
* @param commit_pgp_sig PGP signature for the commit (can be NULL, empty string for unsigned)
|
||||
* @param committer_name Committer name (can be NULL)
|
||||
* @param committer_email Committer email (can be NULL)
|
||||
* @param committer_timestamp Committer timestamp (can be NULL)
|
||||
* @param committer_tz_offset Committer timezone offset in minutes (0 if unused)
|
||||
* @param is_root 1 if this is the root patch in a series, 0 otherwise
|
||||
* @param is_root_revision 1 if this is the first patch in a revision, 0 otherwise
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_patch(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* patch_content,
|
||||
const char* commit_id,
|
||||
const char* parent_commit_id,
|
||||
const char* commit_pgp_sig,
|
||||
const char* committer_name,
|
||||
const char* committer_email,
|
||||
const char* committer_timestamp,
|
||||
int committer_tz_offset,
|
||||
int is_root,
|
||||
int is_root_revision
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Create a pull request event (kind 1618)
|
||||
*
|
||||
* Points to proposed changes in a git repository via clone URLs.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
|
||||
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
|
||||
* @param subject PR subject/title (can be NULL)
|
||||
* @param content Markdown body text
|
||||
* @param tip_commit Current commit ID at the tip of the PR branch (can be NULL)
|
||||
* @param clone_urls Array of git clone URLs where the PR branch can be fetched
|
||||
* @param clone_count Number of clone URLs
|
||||
* @param branch_name Recommended branch name (can be NULL)
|
||||
* @param root_patch_event_id Event ID of the root patch this revises (can be NULL)
|
||||
* @param merge_base Most recent common ancestor with the target branch (can be NULL)
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_pull_request(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* subject,
|
||||
const char* content,
|
||||
const char* tip_commit,
|
||||
const char** clone_urls,
|
||||
int clone_count,
|
||||
const char* branch_name,
|
||||
const char* root_patch_event_id,
|
||||
const char* merge_base
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Create a pull request update event (kind 1619)
|
||||
*
|
||||
* Updates the tip commit of an existing pull request.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
|
||||
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
|
||||
* @param pr_event_id The pull request event ID being updated
|
||||
* @param pr_author The pubkey of the pull request author
|
||||
* @param tip_commit Updated commit ID at the tip of the PR branch (can be NULL)
|
||||
* @param clone_urls Array of git clone URLs where the PR branch can be fetched
|
||||
* @param clone_count Number of clone URLs
|
||||
* @param merge_base Most recent common ancestor with the target branch (can be NULL)
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_pr_update(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* pr_event_id,
|
||||
const char* pr_author,
|
||||
const char* tip_commit,
|
||||
const char** clone_urls,
|
||||
int clone_count,
|
||||
const char* merge_base
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Create an issue event (kind 1621)
|
||||
*
|
||||
* Bug reports, feature requests, questions or comments related to a repository.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>"
|
||||
* @param subject Issue subject/title (can be NULL)
|
||||
* @param content Markdown body text
|
||||
* @param labels Array of label strings (can be NULL)
|
||||
* @param label_count Number of labels
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_issue(
|
||||
nostr_signer_t* signer,
|
||||
const char* repo_address,
|
||||
const char* subject,
|
||||
const char* content,
|
||||
const char** labels,
|
||||
int label_count
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Create a status event (kind 1630-1633)
|
||||
*
|
||||
* Sets the status of a root patch, PR, or issue.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param kind Status kind: 1630 (Open), 1631 (Applied/Merged/Resolved),
|
||||
* 1632 (Closed), 1633 (Draft)
|
||||
* @param root_id Event ID of the root issue, PR, or original root patch
|
||||
* @param repo_address Address tag value "30617:<pubkey>:<repo-id>" (can be NULL)
|
||||
* @param euc_commit Earliest unique commit ID of the repo (can be NULL)
|
||||
* @param content Markdown text describing the status change (can be NULL)
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_status(
|
||||
nostr_signer_t* signer,
|
||||
int kind,
|
||||
const char* root_id,
|
||||
const char* repo_address,
|
||||
const char* euc_commit,
|
||||
const char* content
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Create a user grasp list event (kind 10317)
|
||||
*
|
||||
* Lists grasp servers the user wishes to use for NIP-34 activity.
|
||||
*
|
||||
* @param signer Signer to use for event creation
|
||||
* @param grasp_urls Array of grasp service websocket URLs
|
||||
* @param grasp_count Number of grasp URLs
|
||||
* @return Signed cJSON event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip34_create_grasp_list(
|
||||
nostr_signer_t* signer,
|
||||
const char** grasp_urls,
|
||||
int grasp_count
|
||||
);
|
||||
|
||||
/**
|
||||
* NIP-34: Parse a nostr:// git URL into its components
|
||||
*
|
||||
* Supports formats:
|
||||
* nostr://<naddr>
|
||||
* nostr://<npub|nip05>/<identifier>
|
||||
* nostr://<npub|nip05>/<relay-hint>/<identifier>
|
||||
*
|
||||
* @param url The nostr:// URL to parse
|
||||
* @param npub_out Buffer for npub or NIP-05 address (at least 256 bytes)
|
||||
* @param identifier_out Buffer for the repository identifier (at least 256 bytes)
|
||||
* @param relay_hint_out Buffer for the relay hint URL (at least 512 bytes, optional, can be NULL)
|
||||
* @return 0 on success, -1 on error
|
||||
*/
|
||||
int nostr_nip34_parse_nostr_url(
|
||||
const char* url,
|
||||
char* npub_out,
|
||||
char* identifier_out,
|
||||
char* relay_hint_out
|
||||
);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_NIP034_H */
|
||||
+14
-2
@@ -2,10 +2,10 @@
|
||||
#define NOSTR_CORE_H
|
||||
|
||||
// Version information (auto-updated by increment_and_push.sh)
|
||||
#define VERSION "v0.6.11"
|
||||
#define VERSION "v0.6.12"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 6
|
||||
#define VERSION_PATCH 11
|
||||
#define VERSION_PATCH 12
|
||||
|
||||
/*
|
||||
* NOSTR Core Library - Complete API Reference
|
||||
@@ -71,6 +71,17 @@
|
||||
* - nostr_nip17_receive_dm() -> Receive and decrypt DM
|
||||
* - nostr_nip17_extract_dm_relays() -> Extract relay URLs from kind 10050
|
||||
*
|
||||
* NIP-34 GIT STUFF:
|
||||
* - nostr_nip34_create_repo_announcement() -> Create repo announcement (kind 30617)
|
||||
* - nostr_nip34_create_repo_state() -> Create repo state (kind 30618)
|
||||
* - nostr_nip34_create_patch() -> Create patch (kind 1617)
|
||||
* - nostr_nip34_create_pull_request() -> Create pull request (kind 1618)
|
||||
* - nostr_nip34_create_pr_update() -> Create PR update (kind 1619)
|
||||
* - nostr_nip34_create_issue() -> Create issue (kind 1621)
|
||||
* - nostr_nip34_create_status() -> Create status (kind 1630-1633)
|
||||
* - nostr_nip34_create_grasp_list() -> Create grasp list (kind 10317)
|
||||
* - nostr_nip34_parse_nostr_url() -> Parse nostr:// git URL
|
||||
*
|
||||
* NIP-42 AUTHENTICATION:
|
||||
* - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242)
|
||||
* - nostr_nip42_verify_auth_event() -> Verify authentication event (relay-side)
|
||||
@@ -187,6 +198,7 @@ extern "C" {
|
||||
#include "nip017.h" // Private Direct Messages
|
||||
#include "nip019.h" // Bech32 encoding (nsec/npub)
|
||||
#include "nip021.h" // nostr: URI scheme
|
||||
#include "nip034.h" // git stuff
|
||||
#include "nip042.h" // Authentication of clients to relays
|
||||
#include "nip044.h" // Encryption (modern)
|
||||
#include "nip046.h" // Remote signing
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
/*
|
||||
* NIP-34 Git Stuff Test Suite
|
||||
* Tests repository announcement, state, patch, PR, issue, and status event creation
|
||||
* Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include "../nostr_core/nip034.h"
|
||||
#include "../nostr_core/nip001.h"
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
#include "../nostr_core/utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Test private key (well-known test key, DO NOT use in production)
|
||||
static const unsigned char test_private_key[32] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
|
||||
};
|
||||
|
||||
static int test_count = 0;
|
||||
static int passed_tests = 0;
|
||||
|
||||
void print_test_header(const char* test_name) {
|
||||
test_count++;
|
||||
printf("\n=== TEST %d: %s ===\n", test_count, test_name);
|
||||
}
|
||||
|
||||
void print_test_result(int passed, const char* test_name) {
|
||||
if (passed) {
|
||||
passed_tests++;
|
||||
printf("✅ PASS: %s\n", test_name);
|
||||
} else {
|
||||
printf("❌ FAIL: %s\n", test_name);
|
||||
}
|
||||
}
|
||||
|
||||
void print_json_event(cJSON* event, const char* label) {
|
||||
if (!event) {
|
||||
printf("%s: NULL\n", label);
|
||||
return;
|
||||
}
|
||||
char* json = cJSON_Print(event);
|
||||
printf("%s:\n%s\n", label, json ? json : "NULL");
|
||||
if (json) free(json);
|
||||
}
|
||||
|
||||
// Test 1: Create a repository announcement event
|
||||
int test_repo_announcement(void) {
|
||||
print_test_header("Repository Announcement (kind 30617)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* clone_urls[] = {
|
||||
"https://laantungir.net/grasp/npub1test/c-relay.git",
|
||||
"https://gitgrasp.com/npub1test/c-relay.git",
|
||||
NULL
|
||||
};
|
||||
const char* relay_urls[] = {
|
||||
"wss://relay.laantungir.net",
|
||||
"wss://relay.damus.io",
|
||||
NULL
|
||||
};
|
||||
const char* maintainers[] = {
|
||||
"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4",
|
||||
NULL
|
||||
};
|
||||
|
||||
cJSON* event = nostr_nip34_create_repo_announcement(
|
||||
signer,
|
||||
"c-relay",
|
||||
"c-relay",
|
||||
"A blazingly fast, production-ready Nostr relay",
|
||||
"https://git.laantungir.net/laantungir/c-relay",
|
||||
clone_urls, 2,
|
||||
relay_urls, 2,
|
||||
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
|
||||
maintainers, 1
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create repo announcement event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "Repository Announcement Event");
|
||||
|
||||
// Validate the event
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0 = NOSTR_SUCCESS)\n", rc);
|
||||
|
||||
// Check kind
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 30617)\n", kind_val);
|
||||
|
||||
// Check tags
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
int tag_count = tags ? cJSON_GetArraySize(tags) : 0;
|
||||
printf("Tag count: %d (expected >= 6)\n", tag_count);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 30617 && tag_count >= 6);
|
||||
print_test_result(passed, "Repository Announcement");
|
||||
return passed;
|
||||
}
|
||||
|
||||
// Test 2: Create a repository state event
|
||||
int test_repo_state(void) {
|
||||
print_test_header("Repository State (kind 30618)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* ref_names[] = {
|
||||
"refs/heads/master",
|
||||
"refs/heads/develop",
|
||||
"refs/tags/v1.0.0",
|
||||
"HEAD",
|
||||
NULL
|
||||
};
|
||||
const char* ref_values[] = {
|
||||
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6",
|
||||
"f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1",
|
||||
"0011223344550011223344550011223344550011223344550011223344550011",
|
||||
"ref: refs/heads/master",
|
||||
NULL
|
||||
};
|
||||
|
||||
cJSON* event = nostr_nip34_create_repo_state(
|
||||
signer,
|
||||
"c-relay",
|
||||
ref_names, ref_values, 4
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create repo state event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "Repository State Event");
|
||||
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0)\n", rc);
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 30618)\n", kind_val);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 30618);
|
||||
print_test_result(passed, "Repository State");
|
||||
return passed;
|
||||
}
|
||||
|
||||
// Test 3: Create a patch event
|
||||
int test_patch(void) {
|
||||
print_test_header("Patch (kind 1617)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* patch_content =
|
||||
"From 1234567890abcdef1234567890abcdef12345678 Mon Sep 17 00:00:00 2024\n"
|
||||
"From: Test User <test@example.com>\n"
|
||||
"Date: Mon, 1 Jan 2024 12:00:00 +0000\n"
|
||||
"Subject: [PATCH] Add new feature\n"
|
||||
"\n"
|
||||
"This is a test patch.\n"
|
||||
"---\n"
|
||||
" src/main.c | 2 ++\n"
|
||||
" 1 file changed, 2 insertions(+)\n"
|
||||
"\n"
|
||||
"diff --git a/src/main.c b/src/main.c\n"
|
||||
"index abc123..def456 100644\n"
|
||||
"--- a/src/main.c\n"
|
||||
"+++ b/src/main.c\n"
|
||||
"@@ -1,3 +1,5 @@\n"
|
||||
" #include <stdio.h>\n"
|
||||
"+\n"
|
||||
"+// New feature\n"
|
||||
" int main(void) {\n"
|
||||
" return 0;\n"
|
||||
" }\n";
|
||||
|
||||
cJSON* event = nostr_nip34_create_patch(
|
||||
signer,
|
||||
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
|
||||
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
|
||||
patch_content,
|
||||
"1234567890abcdef1234567890abcdef12345678",
|
||||
"abcdef1234567890abcdef1234567890abcdef12",
|
||||
"-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCAAdFiEE...\n-----END PGP SIGNATURE-----",
|
||||
"Test User",
|
||||
"test@example.com",
|
||||
"1704110400",
|
||||
0,
|
||||
1, // is_root
|
||||
1 // is_root_revision
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create patch event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "Patch Event");
|
||||
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0)\n", rc);
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 1617)\n", kind_val);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 1617);
|
||||
print_test_result(passed, "Patch");
|
||||
return passed;
|
||||
}
|
||||
|
||||
// Test 4: Create a pull request event
|
||||
int test_pull_request(void) {
|
||||
print_test_header("Pull Request (kind 1618)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* clone_urls[] = {
|
||||
"https://github.com/user/c-relay.git",
|
||||
NULL
|
||||
};
|
||||
|
||||
cJSON* event = nostr_nip34_create_pull_request(
|
||||
signer,
|
||||
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
|
||||
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
|
||||
"Fix critical bug in relay connection handling",
|
||||
"This PR fixes a race condition in the relay connection code.\n\nChanges:\n- Added mutex locking\n- Fixed timeout handling",
|
||||
"fedcba9876543210fedcba9876543210fedcba98",
|
||||
clone_urls, 1,
|
||||
"fix-race-condition",
|
||||
NULL, // no root patch event
|
||||
"abcdef1234567890abcdef1234567890abcdef12" // merge base
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create pull request event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "Pull Request Event");
|
||||
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0)\n", rc);
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 1618)\n", kind_val);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 1618);
|
||||
print_test_result(passed, "Pull Request");
|
||||
return passed;
|
||||
}
|
||||
|
||||
// Test 5: Create an issue event
|
||||
int test_issue(void) {
|
||||
print_test_header("Issue (kind 1621)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* labels[] = {
|
||||
"bug",
|
||||
"high-priority",
|
||||
NULL
|
||||
};
|
||||
|
||||
cJSON* event = nostr_nip34_create_issue(
|
||||
signer,
|
||||
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
|
||||
"Connection timeout on startup",
|
||||
"When starting the relay with a large database, the connection times out after 30 seconds.\n\nSteps to reproduce:\n1. Start relay with 1M events\n2. Try to connect\n3. Observe timeout",
|
||||
labels, 2
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create issue event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "Issue Event");
|
||||
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0)\n", rc);
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 1621)\n", kind_val);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 1621);
|
||||
print_test_result(passed, "Issue");
|
||||
return passed;
|
||||
}
|
||||
|
||||
// Test 6: Create a status event
|
||||
int test_status(void) {
|
||||
print_test_header("Status (kind 1631 - Merged)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* event = nostr_nip34_create_status(
|
||||
signer,
|
||||
NOSTR_NIP34_KIND_STATUS_MERGED,
|
||||
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
|
||||
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
|
||||
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
|
||||
"Patch has been merged to master"
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create status event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "Status Event (Merged)");
|
||||
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0)\n", rc);
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 1631)\n", kind_val);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 1631);
|
||||
print_test_result(passed, "Status");
|
||||
return passed;
|
||||
}
|
||||
|
||||
// Test 7: Create a grasp list event
|
||||
int test_grasp_list(void) {
|
||||
print_test_header("Grasp List (kind 10317)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* grasp_urls[] = {
|
||||
"wss://grasp.laantungir.net",
|
||||
"wss://gitgrasp.com",
|
||||
NULL
|
||||
};
|
||||
|
||||
cJSON* event = nostr_nip34_create_grasp_list(
|
||||
signer,
|
||||
grasp_urls, 2
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create grasp list event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "Grasp List Event");
|
||||
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0)\n", rc);
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 10317)\n", kind_val);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 10317);
|
||||
print_test_result(passed, "Grasp List");
|
||||
return passed;
|
||||
}
|
||||
|
||||
// Test 8: Parse nostr:// URLs
|
||||
int test_parse_nostr_url(void) {
|
||||
print_test_header("Parse nostr:// URLs");
|
||||
|
||||
int all_passed = 1;
|
||||
|
||||
// Test 1: nostr://<npub>/<identifier>
|
||||
{
|
||||
char npub[256], ident[256], relay[512];
|
||||
int rc = nostr_nip34_parse_nostr_url(
|
||||
"nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/c-relay",
|
||||
npub, ident, relay
|
||||
);
|
||||
printf("Test 1 - npub/identifier:\n");
|
||||
printf(" Result: %d (expected 0)\n", rc);
|
||||
printf(" npub: %s\n", npub);
|
||||
printf(" ident: %s\n", ident);
|
||||
printf(" relay: %s\n", relay);
|
||||
if (rc != 0 || strlen(npub) == 0 || strlen(ident) == 0) {
|
||||
printf(" ❌ FAIL\n");
|
||||
all_passed = 0;
|
||||
} else {
|
||||
printf(" ✅ PASS\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: nostr://<npub>/<relay>/<identifier>
|
||||
{
|
||||
char npub[256], ident[256], relay[512];
|
||||
int rc = nostr_nip34_parse_nostr_url(
|
||||
"nostr://npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/relay.ngit.dev/ngit",
|
||||
npub, ident, relay
|
||||
);
|
||||
printf("Test 2 - npub/relay/identifier:\n");
|
||||
printf(" Result: %d (expected 0)\n", rc);
|
||||
printf(" npub: %s\n", npub);
|
||||
printf(" relay: %s\n", relay);
|
||||
printf(" ident: %s\n", ident);
|
||||
if (rc != 0 || strlen(npub) == 0 || strlen(relay) == 0 || strlen(ident) == 0) {
|
||||
printf(" ❌ FAIL\n");
|
||||
all_passed = 0;
|
||||
} else {
|
||||
printf(" ✅ PASS\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: nostr://<nip05>/<identifier>
|
||||
{
|
||||
char npub[256], ident[256], relay[512];
|
||||
int rc = nostr_nip34_parse_nostr_url(
|
||||
"nostr://danconwaydev.com/ws%3A%2F%2Flocalhost%3A7334/my-local-only-repo",
|
||||
npub, ident, relay
|
||||
);
|
||||
printf("Test 3 - nip05/relay/identifier (percent-encoded):\n");
|
||||
printf(" Result: %d (expected 0)\n", rc);
|
||||
printf(" npub: %s\n", npub);
|
||||
printf(" relay: %s\n", relay);
|
||||
printf(" ident: %s\n", ident);
|
||||
if (rc != 0 || strlen(npub) == 0 || strlen(relay) == 0 || strcmp(ident, "my-local-only-repo") != 0) {
|
||||
printf(" ❌ FAIL\n");
|
||||
all_passed = 0;
|
||||
} else {
|
||||
printf(" ✅ PASS\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: Invalid URL (no nostr:// prefix)
|
||||
{
|
||||
char npub[256], ident[256], relay[512];
|
||||
int rc = nostr_nip34_parse_nostr_url(
|
||||
"https://example.com/repo.git",
|
||||
npub, ident, relay
|
||||
);
|
||||
printf("Test 4 - invalid URL (https):\n");
|
||||
printf(" Result: %d (expected -1)\n", rc);
|
||||
if (rc != -1) {
|
||||
printf(" ❌ FAIL\n");
|
||||
all_passed = 0;
|
||||
} else {
|
||||
printf(" ✅ PASS\n");
|
||||
}
|
||||
}
|
||||
|
||||
print_test_result(all_passed, "Parse nostr:// URLs");
|
||||
return all_passed;
|
||||
}
|
||||
|
||||
// Test 9: Create a PR update event
|
||||
int test_pr_update(void) {
|
||||
print_test_header("PR Update (kind 1619)");
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(test_private_key);
|
||||
if (!signer) {
|
||||
printf("❌ Failed to create signer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* clone_urls[] = {
|
||||
"https://github.com/user/c-relay.git",
|
||||
NULL
|
||||
};
|
||||
|
||||
cJSON* event = nostr_nip34_create_pr_update(
|
||||
signer,
|
||||
"30617:aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4:c-relay",
|
||||
"abc123def456abc123def456abc123def456abc123def456abc123def456abc123",
|
||||
"pr_event_id_1234567890abcdef",
|
||||
"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4",
|
||||
"new_tip_commit_hash_1234567890abcdef",
|
||||
clone_urls, 1,
|
||||
"updated_merge_base_hash_1234567890"
|
||||
);
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Failed to create PR update event\n");
|
||||
nostr_signer_free(signer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
print_json_event(event, "PR Update Event");
|
||||
|
||||
int rc = nostr_validate_event(event);
|
||||
printf("Validation result: %d (expected 0)\n", rc);
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
int kind_val = kind ? (int)cJSON_GetNumberValue(kind) : -1;
|
||||
printf("Kind: %d (expected 1619)\n", kind_val);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
|
||||
int passed = (rc == NOSTR_SUCCESS && kind_val == 1619);
|
||||
print_test_result(passed, "PR Update");
|
||||
return passed;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("========================================\n");
|
||||
printf(" NIP-34 Git Stuff Test Suite\n");
|
||||
printf("========================================\n");
|
||||
|
||||
// Initialize crypto
|
||||
if (nostr_crypto_init() != 0) {
|
||||
printf("❌ Failed to initialize crypto\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
int results[9];
|
||||
results[0] = test_repo_announcement();
|
||||
results[1] = test_repo_state();
|
||||
results[2] = test_patch();
|
||||
results[3] = test_pull_request();
|
||||
results[4] = test_issue();
|
||||
results[5] = test_status();
|
||||
results[6] = test_grasp_list();
|
||||
results[7] = test_parse_nostr_url();
|
||||
results[8] = test_pr_update();
|
||||
|
||||
// Summary
|
||||
printf("\n========================================\n");
|
||||
printf(" TEST SUMMARY\n");
|
||||
printf("========================================\n");
|
||||
printf("Total tests: %d\n", test_count);
|
||||
printf("Passed: %d\n", passed_tests);
|
||||
printf("Failed: %d\n", test_count - passed_tests);
|
||||
|
||||
nostr_crypto_cleanup();
|
||||
|
||||
if (passed_tests == test_count) {
|
||||
printf("🎉 ALL TESTS PASSED! NIP-34 implementation is working correctly.\n");
|
||||
return 0;
|
||||
} else {
|
||||
printf("❌ SOME TESTS FAILED!\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user