feat: add nip-25 support#589
Conversation
|
|
@copilot can you add changeset and upadte readme with nip 25 and also add 25 to package.json |
There was a problem hiding this comment.
Pull request overview
Adds relay-side support for NIP-25 reactions by introducing reaction kinds/tags, validation rules, and helper utilities, along with unit + integration coverage.
Changes:
- Added NIP-25 constants/types (
EXTERNAL_CONTENT_REACTION, reaction-relatedEventTags,ReactionEntry). - Implemented
src/utils/nip25.tshelpers for identifying/parsing reaction events. - Extended
eventSchemawith NIP-25-specific validation and added unit/integration tests for reaction flows.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/constants/base.ts |
Adds kind 17 and reaction-related tag constants. |
src/@types/event.ts |
Introduces ReactionEntry for parsed reaction targets. |
src/utils/nip25.ts |
Implements NIP-25 reaction detection/parsing helpers. |
src/schemas/event-schema.ts |
Adds schema refinement enforcing reaction tag requirements. |
src/factories/event-strategy-factory.ts |
Wires reaction kinds into strategy routing (currently default). |
test/unit/utils/nip25.spec.ts |
Unit tests for NIP-25 utility helpers. |
test/unit/schemas/event-schema.spec.ts |
Unit tests for new NIP-25 schema constraints. |
test/unit/factories/event-strategy-factory.spec.ts |
Tests factory routing for reaction kinds. |
test/integration/features/nip-25/nip-25.feature |
Cucumber feature scenarios for like/dislike/emoji reactions. |
test/integration/features/nip-25/nip-25.feature.ts |
Step definitions implementing the NIP-25 feature scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!event.tags.some((tag) => tag[0] === EventTags.Event)) { | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: 'Reaction event (kind 7) must have at least one e tag', |
There was a problem hiding this comment.
NIP-25 reactions (kind 7) can target parameterized replaceable events via an a tag. Current validation rejects any reaction that doesn't include an e tag, even if it has an a tag. Consider accepting reactions that have at least one e OR at least one a tag (and ideally ensure the tag has a value).
| if (!event.tags.some((tag) => tag[0] === EventTags.Event)) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'Reaction event (kind 7) must have at least one e tag', | |
| const hasEventTag = event.tags.some((tag) => tag[0] === EventTags.Event && typeof tag[1] === 'string' && tag[1].length > 0) | |
| const hasAddressTag = event.tags.some((tag) => tag[0] === 'a' && typeof tag[1] === 'string' && tag[1].length > 0) | |
| if (!hasEventTag && !hasAddressTag) { | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message: 'Reaction event (kind 7) must have at least one e or a tag', |
| const hasKTag = event.tags.some((tag) => tag[0] === EventTags.Kind) | ||
| const hasITag = event.tags.some((tag) => tag[0] === EventTags.Index) | ||
| if (!hasKTag || !hasITag) { | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: 'External content reaction event (kind 17) must have k and i tags', | ||
| path: ['tags'], | ||
| }) | ||
| } |
There was a problem hiding this comment.
For external content reactions (kind 17), the k / i presence checks only look at tag[0]. Because tagSchema allows 1-element tags (e.g. ['k']), this will accept malformed events that don't actually provide required values. Consider requiring tag.length >= 2 (and non-empty values) for both required tags.
|
|
||
| return { | ||
| targetEventId: eTags.length > 0 ? eTags[eTags.length - 1][1] : undefined, | ||
| targetPubkey: pTags.length > 0 ? pTags[pTags.length - 1][1] : undefined, | ||
| targetAddress: aTags.length > 0 ? aTags[aTags.length - 1][1] : undefined, | ||
| targetKind: kTag ? Number(kTag[1]) : undefined, |
There was a problem hiding this comment.
parseReaction sets targetKind: Number(kTag[1]), which will produce NaN when the k tag is non-numeric (e.g. kind 17 tests use k: 'web') or when the tag is missing a value (['k']). Consider guarding with a numeric check and returning undefined (or representing the kind as a string/union type if kind 17 expects non-numeric values).
| return { | |
| targetEventId: eTags.length > 0 ? eTags[eTags.length - 1][1] : undefined, | |
| targetPubkey: pTags.length > 0 ? pTags[pTags.length - 1][1] : undefined, | |
| targetAddress: aTags.length > 0 ? aTags[aTags.length - 1][1] : undefined, | |
| targetKind: kTag ? Number(kTag[1]) : undefined, | |
| const kTagValue = kTag && kTag.length > 1 ? kTag[1] : undefined | |
| const parsedTargetKind = kTagValue !== undefined ? Number(kTagValue) : undefined | |
| const targetKind = | |
| parsedTargetKind !== undefined && Number.isFinite(parsedTargetKind) | |
| ? parsedTargetKind | |
| : undefined | |
| return { | |
| targetEventId: eTags.length > 0 ? eTags[eTags.length - 1][1] : undefined, | |
| targetPubkey: pTags.length > 0 ? pTags[pTags.length - 1][1] : undefined, | |
| targetAddress: aTags.length > 0 ? aTags[aTags.length - 1][1] : undefined, | |
| targetKind, |
| } | ||
| if (isReactionEvent(event) || isExternalContentReactionEvent(event)) { | ||
| return new DefaultEventStrategy(adapter, eventRepository) | ||
| } |
There was a problem hiding this comment.
The reaction/external-reaction branch is redundant because the factory falls back to DefaultEventStrategy anyway. Consider removing this if (and the nip25 imports) or converting it into an else if only if a distinct strategy is planned; this keeps the decision tree easier to read.
Description
Implements NIP-25 (Reactions) support for kind 7 (nostr event reactions) and kind 17 (external content reactions).
EXTERNAL_CONTENT_REACTION = 17toEventKindsand new tags (Address,Index,Emoji) toEventTagsReactionEntrytype to@types/eventsrc/utils/nip25.tswithisReactionEvent,isExternalContentReactionEvent,isLikeReaction,isDislikeReaction, andparseReactionhelpersetag; kind 17 must includekanditagsevent-strategy-factoryRelated Issue
Closes #578
Motivation and Context
NIP-25 defines how clients express reactions (likes, dislikes, emoji) to notes and external content. This adds
relay-side support for storing, validating, and querying reaction events.
How Has This Been Tested?
isReactionEvent,isLikeReaction,isDislikeReaction,parseReaction,etc.)
Screenshots (if appropriate):
N/A
Types of changes
Checklist