Skip to content

feat: add nip-25 support#589

Open
CKodidela wants to merge 1 commit intocameri:mainfrom
CKodidela:feat/nip-25
Open

feat: add nip-25 support#589
CKodidela wants to merge 1 commit intocameri:mainfrom
CKodidela:feat/nip-25

Conversation

@CKodidela
Copy link
Copy Markdown
Collaborator

Description

Implements NIP-25 (Reactions) support for kind 7 (nostr event reactions) and kind 17 (external content reactions).

  • Added EXTERNAL_CONTENT_REACTION = 17 to EventKinds and new tags (Address, Index, Emoji) to EventTags
  • Added ReactionEntry type to @types/event
  • Created src/utils/nip25.ts with isReactionEvent, isExternalContentReactionEvent, isLikeReaction,
    isDislikeReaction, and parseReaction helpers
  • Schema validation: kind 7 must include at least one e tag; kind 17 must include k and i tags
  • Wired reaction event types into event-strategy-factory

Related 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?

  • Unit tests for all utility functions (isReactionEvent, isLikeReaction, isDislikeReaction, parseReaction,
    etc.)
  • Unit tests for schema validation (valid/invalid kind 7 and kind 17 events)
  • Unit tests for event strategy factory routing
  • Integration tests covering like, dislike, and emoji reaction scenarios
  • All existing tests continue to pass

Screenshots (if appropriate):

N/A

Types of changes

  • Non-functional change (docs, style, minor refactor)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my code changes.
  • I added a changeset, or this is docs-only and I added an empty changeset.
  • All new and existing tests passed.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

⚠️ No Changeset found

Latest commit: 45bc4dd

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coveralls
Copy link
Copy Markdown
Collaborator

Coverage Status

coverage: 64.219% (+0.2%) from 64.015% — CKodidela:feat/nip-25 into cameri:main

@CKodidela
Copy link
Copy Markdown
Collaborator Author

@copilot can you add changeset and upadte readme with nip 25 and also add 25 to package.json

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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-related EventTags, ReactionEntry).
  • Implemented src/utils/nip25.ts helpers for identifying/parsing reaction events.
  • Extended eventSchema with 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.

Comment on lines +35 to +38
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',
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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',

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +51
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'],
})
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/nip25.ts
Comment on lines +20 to +25

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,
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +45 to 48
}
if (isReactionEvent(event) || isExternalContentReactionEvent(event)) {
return new DefaultEventStrategy(adapter, eventRepository)
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add NIP-25 (Reactions) support

3 participants