Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion crates/buzz-sdk/src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,15 @@ fn build_git_issue_assignee_operation(
tags.push(tag(&["prior", &prior])?);
}

Ok(EventBuilder::new(Kind::Custom(1), content).tags(tags))
// `allow_self_tagging` because an assignee may be the signer, and that is
// the normal case: an agent claiming work, or a person clicking "Assign to
// me". `nostr` removes a `p` tag matching the author by default, which
// silently produced assignment events naming nobody — the operation
// published, the comment read "Assigned this issue to …", and the assignee
// list stayed empty. Here the `p` tags *are* the payload, not a mention.
Ok(EventBuilder::new(Kind::Custom(1), content)
.tags(tags)
.allow_self_tagging())
}

/// Status to apply to a patch or issue root (kind:1630/1631/1632/1633, NIP-34).
Expand Down Expand Up @@ -3711,6 +3719,55 @@ mod tests {
assert!(matches!(err, SdkError::InvalidInput(_)));
}

/// The failure this guards: `nostr` removes a `p` tag naming the author
/// unless `allow_self_tagging` is set, so a self-assignment published an
/// event with no assignee in it. The comment log still read "Assigned this
/// issue to …" while the assignee list stayed empty — for agents claiming
/// their own work and for anyone clicking "Assign to me".
#[test]
fn self_assignment_keeps_the_assignee() {
let keys = Keys::generate();
let me = keys.public_key().to_hex();
let repo = GitRepoCoord {
owner: "a".repeat(64),
id: "repo".to_string(),
};
let builder = build_git_issue_assignment(
&repo,
&"d".repeat(64),
std::slice::from_ref(&me),
"Assigned this issue to me",
)
.unwrap();
let ev = builder.sign_with_keys(&keys).unwrap();
assert!(
has_tag(&ev, "p", &me),
"the assignee is the payload, not a mention: {:?}",
ev.tags
);
}

/// Unassigning yourself is the same shape and failed the same way.
#[test]
fn self_unassignment_keeps_the_assignee() {
let keys = Keys::generate();
let me = keys.public_key().to_hex();
let repo = GitRepoCoord {
owner: "a".repeat(64),
id: "repo".to_string(),
};
let ev = build_git_issue_unassignment(
&repo,
&"d".repeat(64),
std::slice::from_ref(&me),
"Unassigned me from this issue",
)
.unwrap()
.sign_with_keys(&keys)
.unwrap();
assert!(has_tag(&ev, "p", &me));
}

#[test]
fn git_issue_assignment_happy_path() {
let owner = "a".repeat(64);
Expand Down
9 changes: 8 additions & 1 deletion desktop/src-tauri/src/commands/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,14 @@ pub async fn sign_event(
.map(|tag| Tag::parse(tag).map_err(|error| format!("invalid tag: {error}")))
.collect::<Result<Vec<_>, _>>()?;

let mut builder = EventBuilder::new(Kind::Custom(kind), content).tags(nostr_tags);
// Sign exactly the tags the caller composed. `nostr` otherwise removes
// any `p` tag naming the author, which made this signer silently drop
// part of what it was handed — the self-assignment path in
// `issueAssignments.ts` published assignments with no assignee. A
// caller that does not want a self `p` tag simply does not add one.
let mut builder = EventBuilder::new(Kind::Custom(kind), content)
.tags(nostr_tags)
.allow_self_tagging();
if let Some(created_at) = created_at {
builder = builder.custom_created_at(Timestamp::from(created_at));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ fn build_labeled_recipient_note_event(
.map(Tag::parse)
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("build {label} tags: {error}"))?;
let mut builder = EventBuilder::new(Kind::TextNote, content).tags(tags);
// The recipients are the payload here, and one of them may be the signer —
// assigning yourself, or requesting your own review. `nostr` strips a `p`
// tag matching the author unless told not to, which turned those into
// events naming nobody. See the same call in `buzz-sdk`.
let mut builder = EventBuilder::new(Kind::TextNote, content)
.tags(tags)
.allow_self_tagging();
if let Some(created_at) = created_at {
builder = builder.custom_created_at(Timestamp::from_secs(created_at));
}
Expand Down