Skip to content
Merged
Prev Previous commit
Next Next commit
refactor: use typed tag helpers across API and events; add code-aware…
… helper variants
  • Loading branch information
erict875 committed Oct 6, 2025
commit cc1a5d34d6634ccf274959e0fea868a313dbfd41
543 changes: 0 additions & 543 deletions PR_COMPLETE_CHANGES_0.2.2_TO_0.5.1.md

This file was deleted.

2 changes: 1 addition & 1 deletion nostr-java-api/src/main/java/nostr/api/NIP01.java
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ public static BaseTag createIdentifierTag(@NonNull String id) {
*/
public static BaseTag createAddressTag(
@NonNull Integer kind, @NonNull PublicKey publicKey, BaseTag idTag, Relay relay) {
if (idTag != null && !idTag.getCode().equals(Constants.Tag.IDENTITY_CODE)) {
if (idTag != null && !(idTag instanceof nostr.event.tag.IdentifierTag)) {
throw new IllegalArgumentException("idTag must be an identifier tag");
}

Expand Down
2 changes: 1 addition & 1 deletion nostr-java-api/src/main/java/nostr/api/NIP02.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public NIP02 createContactListEvent(List<BaseTag> pubKeyTags) {
* @param tag the pubkey tag
*/
public NIP02 addContactTag(@NonNull BaseTag tag) {
if (!tag.getCode().equals(Constants.Tag.PUBKEY_CODE)) {
if (!(tag instanceof nostr.event.tag.PubKeyTag)) {
throw new IllegalArgumentException("Tag must be a pubkey tag");
}
getEvent().addTag(tag);
Expand Down
10 changes: 4 additions & 6 deletions nostr-java-api/src/main/java/nostr/api/NIP04.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,11 @@ public static String decrypt(@NonNull Identity rcptId, @NonNull GenericEvent eve
}

private static boolean amITheRecipient(@NonNull Identity recipient, @NonNull GenericEvent event) {
var pTag =
event.getTags().stream()
.filter(t -> t.getCode().equalsIgnoreCase("p"))
.findFirst()
.orElseThrow(() -> new NoSuchElementException("No matching p-tag found."));
// Use helper to fetch the p-tag without manual casts
PubKeyTag pTag =
Filterable.requireTagOfType(PubKeyTag.class, event, "No matching p-tag found.");

if (Objects.equals(recipient.getPublicKey(), ((PubKeyTag) pTag).getPublicKey())) {
if (Objects.equals(recipient.getPublicKey(), pTag.getPublicKey())) {
return true;
}

Expand Down
10 changes: 4 additions & 6 deletions nostr-java-api/src/main/java/nostr/api/NIP44.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,11 @@ public static String decrypt(@NonNull Identity recipient, @NonNull GenericEvent
}

private static boolean amITheRecipient(@NonNull Identity recipient, @NonNull GenericEvent event) {
var pTag =
event.getTags().stream()
.filter(t -> t.getCode().equalsIgnoreCase("p"))
.findFirst()
.orElseThrow(() -> new NoSuchElementException("No matching p-tag found."));
// Use helper to fetch the p-tag without manual casts
PubKeyTag pTag =
Filterable.requireTagOfType(PubKeyTag.class, event, "No matching p-tag found.");

if (Objects.equals(recipient.getPublicKey(), ((PubKeyTag) pTag).getPublicKey())) {
if (Objects.equals(recipient.getPublicKey(), pTag.getPublicKey())) {
return true;
}

Expand Down
8 changes: 2 additions & 6 deletions nostr-java-api/src/main/java/nostr/api/NIP52.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import nostr.event.entities.CalendarContent;
import nostr.event.entities.CalendarRsvpContent;
import nostr.event.impl.GenericEvent;
import nostr.event.tag.GenericTag;
import nostr.event.tag.EventTag;
import nostr.event.tag.GeohashTag;
import nostr.id.Identity;
import org.apache.commons.lang3.stream.Streams;
Expand Down Expand Up @@ -174,11 +174,7 @@ public NIP52 addEndTag(@NonNull Long end) {
return this;
}

public NIP52 addEventTag(@NonNull GenericTag eventTag) {
if (!Constants.Tag.EVENT_CODE.equals(eventTag.getCode())) { // Sanity check
throw new IllegalArgumentException("tag must be of type EventTag");
}

public NIP52 addEventTag(@NonNull EventTag eventTag) {
addTag(eventTag);
return this;
}
Expand Down
17 changes: 5 additions & 12 deletions nostr-java-api/src/main/java/nostr/api/NIP57.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public NIP57 createZapRequestEvent(
GenericEvent zappedEvent,
BaseTag addressTag) {

if (!relaysTags.getCode().equals(Constants.Tag.RELAYS_CODE)) {
if (!(relaysTags instanceof RelaysTag)) {
throw new IllegalArgumentException("tag must be of type RelaysTag");
}
Comment on lines 93 to 98

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rejecting non-RelaysTag inputs in createZapRequestEvent

The new guard if (!(relaysTags instanceof RelaysTag)) now throws whenever callers pass a generic BaseTag whose code is "relays". Before this change the method only checked the tag code, so callers could (and in NIP57ImplTest still do) pass BaseTag.create("relays", url) without constructing a dedicated RelaysTag. Because the method signature still accepts BaseTag, this silently turns previously valid usage into an IllegalArgumentException at runtime and drops support for generic relays tags. If the intent is to require RelaysTag, the parameter type should be narrowed and the break documented; otherwise the old code check should be preserved to avoid breaking existing clients.

Useful? React with 👍 / 👎.


Expand All @@ -113,7 +113,7 @@ public NIP57 createZapRequestEvent(
}

if (addressTag != null) {
if (!addressTag.getCode().equals(Constants.Tag.ADDRESS_CODE)) { // Sanity check
if (!(addressTag instanceof nostr.event.tag.AddressTag)) { // Sanity check
throw new IllegalArgumentException("Address tag must be of type AddressTag");
}
genericEvent.addTag(addressTag);
Expand Down Expand Up @@ -205,16 +205,9 @@ public NIP57 createZapReceiptEvent(
genericEvent.addTag(createZapSenderPubKeyTag(zapRequestEvent.getPubKey()));
genericEvent.addTag(NIP01.createEventTag(zapRequestEvent.getId()));

GenericTag addressTag =
(GenericTag)
zapRequestEvent.getTags().stream()
.filter(tag -> tag.getCode().equals(Constants.Tag.ADDRESS_CODE))
.findFirst()
.orElse(null);

if (addressTag != null) {
genericEvent.addTag(addressTag);
}
nostr.event.filter.Filterable
.firstTagOfTypeWithCode(nostr.event.tag.AddressTag.class, Constants.Tag.ADDRESS_CODE, zapRequestEvent)
.ifPresent(genericEvent::addTag);

genericEvent.setCreatedAt(zapRequestEvent.getCreatedAt());

Expand Down
50 changes: 50 additions & 0 deletions nostr-java-event/src/main/java/nostr/event/filter/Filterable.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,56 @@ static <T extends BaseTag> List<T> getTypeSpecificTags(
return event.getTags().stream().filter(tagClass::isInstance).map(tagClass::cast).toList();
}

/**
* Convenience: return the first tag of the specified type, if present.
*/
static <T extends BaseTag> java.util.Optional<T> firstTagOfType(
@NonNull Class<T> tagClass, @NonNull GenericEvent event) {
return getTypeSpecificTags(tagClass, event).stream().findFirst();
}

/**
* Convenience: return the first tag of the specified type and code, if present.
*/
static <T extends BaseTag> java.util.Optional<T> firstTagOfTypeWithCode(
@NonNull Class<T> tagClass, @NonNull String code, @NonNull GenericEvent event) {
return getTypeSpecificTags(tagClass, event).stream()
.filter(t -> code.equals(t.getCode()))
.findFirst();
}

/**
* Convenience: return the first tag of the specified type or throw with a clear message.
*
* Rationale: callers often need a single tag instance; this avoids repeated casts and stream code.
*/
static <T extends BaseTag> T requireTagOfType(
@NonNull Class<T> tagClass, @NonNull GenericEvent event, @NonNull String errorMessage) {
return firstTagOfType(tagClass, event)
.orElseThrow(() -> new java.util.NoSuchElementException(errorMessage));
}

/**
* Convenience: return the first tag of the specified type and code or throw with a clear message.
*/
static <T extends BaseTag> T requireTagOfTypeWithCode(
@NonNull Class<T> tagClass,
@NonNull String code,
@NonNull GenericEvent event,
@NonNull String errorMessage) {
return firstTagOfTypeWithCode(tagClass, code, event)
.orElseThrow(() -> new java.util.NoSuchElementException(errorMessage));
}

/**
* Convenience overload: generic error if not found.
*/
static <T extends BaseTag> T requireTagOfTypeWithCode(
@NonNull Class<T> tagClass, @NonNull String code, @NonNull GenericEvent event) {
return requireTagOfTypeWithCode(
tagClass, code, event, "Missing required tag of type %s with code '%s'".formatted(tagClass.getSimpleName(), code));
}

default ObjectNode toObjectNode(ObjectNode objectNode) {
ArrayNode arrayNode = MAPPER_BLACKBIRD.createArrayNode();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ public AbstractBaseNostrConnectEvent(
}

public PublicKey getActor() {
return ((PubKeyTag) getTag("p")).getPublicKey();
var pTag =
nostr.event.filter.Filterable.requireTagOfType(
PubKeyTag.class, this, "Invalid `tags`: missing PubKeyTag (p)");
return pTag.getPublicKey();
}

public void validate() {
super.validate();

// 1. p - tag validation
getTags().stream()
.filter(tag -> tag instanceof PubKeyTag)
.findFirst()
nostr.event.filter.Filterable
.firstTagOfType(PubKeyTag.class, this)
.orElseThrow(
() -> new AssertionError("Invalid `tags`: Must include at least one valid PubKeyTag."));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,44 +71,49 @@ public List<ReferenceTag> getReferences() {
protected CalendarContent<T> getCalendarContent() {
CalendarContent<T> calendarContent =
new CalendarContent<>(
(IdentifierTag) getTag("d"),
((GenericTag) getTag("title")).getAttributes().get(0).value().toString(),
nostr.event.filter.Filterable.requireTagOfTypeWithCode(
IdentifierTag.class, "d", this),
nostr.event.filter.Filterable
.requireTagOfTypeWithCode(GenericTag.class, "title", this)
.getAttributes()
.get(0)
.value()
.toString(),
Long.parseLong(
((GenericTag) getTag("start")).getAttributes().get(0).value().toString()));
nostr.event.filter.Filterable
.requireTagOfTypeWithCode(GenericTag.class, "start", this)
.getAttributes()
.get(0)
.value()
.toString()));

// Update the calendarContent object with the values from the tags
Optional.ofNullable(getTag("end"))
nostr.event.filter.Filterable
.firstTagOfTypeWithCode(GenericTag.class, "end", this)
.ifPresent(
baseTag ->
tag ->
calendarContent.setEnd(
Long.parseLong(
((GenericTag) baseTag).getAttributes().get(0).value().toString())));
Long.parseLong(tag.getAttributes().get(0).value().toString())));

Optional.ofNullable(getTag("location"))
.ifPresent(
baseTag ->
calendarContent.setLocation(
((GenericTag) baseTag).getAttributes().get(0).value().toString()));
nostr.event.filter.Filterable
.firstTagOfTypeWithCode(GenericTag.class, "location", this)
.ifPresent(tag -> calendarContent.setLocation(tag.getAttributes().get(0).value().toString()));

Optional.ofNullable(getTag("g"))
.ifPresent(baseTag -> calendarContent.setGeohashTag((GeohashTag) baseTag));
nostr.event.filter.Filterable
.firstTagOfTypeWithCode(GeohashTag.class, "g", this)
.ifPresent(calendarContent::setGeohashTag);

Optional.ofNullable(getTags("p"))
.ifPresent(
baseTags ->
baseTags.forEach(
baseTag -> calendarContent.addParticipantPubKeyTag((PubKeyTag) baseTag)));
nostr.event.filter.Filterable
.getTypeSpecificTags(PubKeyTag.class, this)
.forEach(calendarContent::addParticipantPubKeyTag);

Optional.ofNullable(getTags("t"))
.ifPresent(
baseTags ->
baseTags.forEach(baseTag -> calendarContent.addHashtagTag((HashtagTag) baseTag)));
nostr.event.filter.Filterable
.getTypeSpecificTags(HashtagTag.class, this)
.forEach(calendarContent::addHashtagTag);

Optional.ofNullable(getTags("r"))
.ifPresent(
baseTags ->
baseTags.forEach(
baseTag -> calendarContent.addReferenceTag((ReferenceTag) baseTag)));
nostr.event.filter.Filterable
.getTypeSpecificTags(ReferenceTag.class, this)
.forEach(calendarContent::addReferenceTag);

return calendarContent;
}
Expand Down
36 changes: 19 additions & 17 deletions nostr-java-event/src/main/java/nostr/event/impl/CalendarEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ public List<PublicKey> getCalendarEventAuthors() {
@Override
protected CalendarContent<BaseTag> getCalendarContent() {

BaseTag identifierTag = getTag("d");
BaseTag titleTag = getTag("title");

CalendarContent<BaseTag> calendarContent =
new CalendarContent<>(
(IdentifierTag) identifierTag,
((GenericTag) titleTag).getAttributes().get(0).value().toString(),
-1L);

List<BaseTag> aTags = getTags("a");

Optional.ofNullable(aTags)
.ifPresent(tags -> tags.forEach(aTag -> calendarContent.addAddressTag((AddressTag) aTag)));
IdentifierTag idTag =
nostr.event.filter.Filterable.requireTagOfTypeWithCode(IdentifierTag.class, "d", this);
String title =
nostr.event.filter.Filterable
.requireTagOfTypeWithCode(GenericTag.class, "title", this)
.getAttributes()
.get(0)
.value()
.toString();

CalendarContent<BaseTag> calendarContent = new CalendarContent<>(idTag, title, -1L);

nostr.event.filter.Filterable
.getTypeSpecificTags(AddressTag.class, this)
.forEach(calendarContent::addAddressTag);

return calendarContent;
}
Expand All @@ -69,13 +71,13 @@ protected void validateTags() {
super.validateTags();

// Validate required tags ("d", "title")
BaseTag dTag = getTag("d");
if (dTag == null) {
if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(IdentifierTag.class, "d", this)
.isEmpty()) {
throw new AssertionError("Missing `d` tag for the event identifier.");
}

BaseTag titleTag = getTag("title");
if (titleTag == null) {
if (nostr.event.filter.Filterable.firstTagOfTypeWithCode(GenericTag.class, "title", this)
.isEmpty()) {
throw new AssertionError("Missing `title` tag for the event title.");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,27 @@ public Optional<PublicKey> getAuthor() {
protected CalendarRsvpContent getCalendarContent() {
CalendarRsvpContent calendarRsvpContent =
CalendarRsvpContent.builder(
(IdentifierTag) getTag("d"),
(AddressTag) getTag("a"),
((GenericTag) getTag("status")).getAttributes().get(0).value().toString())
nostr.event.filter.Filterable.requireTagOfTypeWithCode(
IdentifierTag.class, "d", this),
nostr.event.filter.Filterable.requireTagOfTypeWithCode(
AddressTag.class, "a", this),
nostr.event.filter.Filterable
.requireTagOfTypeWithCode(GenericTag.class, "status", this)
.getAttributes()
.get(0)
.value()
.toString())
.build();

Optional.ofNullable(getTag("e"))
.ifPresent(baseTag -> calendarRsvpContent.setEventTag((EventTag) baseTag));
nostr.event.filter.Filterable
.firstTagOfType(EventTag.class, this)
.ifPresent(calendarRsvpContent::setEventTag);
// FB tag is encoded as a generic tag with code 'fb'
Optional.ofNullable(getTag("fb"))
.ifPresent(baseTag -> calendarRsvpContent.setFbTag((GenericTag) baseTag));
Optional.ofNullable(getTag("p"))
.ifPresent(baseTag -> calendarRsvpContent.setAuthorPubKeyTag((PubKeyTag) baseTag));
nostr.event.filter.Filterable
.firstTagOfType(PubKeyTag.class, this)
.ifPresent(calendarRsvpContent::setAuthorPubKeyTag);

return calendarRsvpContent;
}
Expand Down
Loading