README.md

# nostr_access

An Elixir library for querying Nostr relays with caching and deduplication.

## Features

- **Multi-relay queries**: Query multiple Nostr relays simultaneously
- **Automatic deduplication**: Follows NIP-01 rules for event deduplication
- **In-memory caching**: Configurable TTL for events (2h) and misses (10min)
- **Connection pooling**: Up to 3 connections per relay with ≤10 subscriptions each
- **Dual API**: Both synchronous (`fetch/3`) and asynchronous (`stream/3`) interfaces
- **Automatic timeouts**: Idle timeout (500ms) and overall timeout (30s) handling

## Installation

Add `nostr_access` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:nostr_access, "~> 0.1.0"}
  ]
end
```

## Quick Start

### Command Line Interface

The `nostr_access` package includes a command-line tool that follows the `nak` tool syntax:

```bash
# Build the CLI tool
mix escript.build

# Generate a filter for kind 1 events
./nostr_access --bare -k 1 -l 10

# Query multiple relays
./nostr_access -k 1 -l 15 wss://relay.example.com wss://nostr-pub.wellorder.net

# Use multiple authors and kinds
./nostr_access --bare -a pubkey1 -a pubkey2 -k 1 -k 6

# With tags and time filters
./nostr_access --bare -k 1 -t e=event123 -s 1700000000 -u 1700003600

# Stream events in real-time
./nostr_access --stream -k 1 wss://relay.example.com

# Paginate through events
./nostr_access --paginate --paginate-global-limit 100 -k 1 wss://relay.example.com
```

### Programmatic API

#### Fetching Events (Synchronous)

```elixir
# Fetch text notes from a single relay
{:ok, events} = Nostr.Client.fetch(
  ["wss://relay.example.com"],
  %{kinds: [1]}
)

# Fetch events from multiple relays
{:ok, events} = Nostr.Client.fetch(
  ["wss://relay1.com", "wss://relay2.com"],
  %{authors: ["pubkey1", "pubkey2"], kinds: [1, 6]}
)

# With custom options
{:ok, events} = Nostr.Client.fetch(
  ["wss://relay.example.com"],
  %{kinds: [1]},
  idle_ms: 1000,
  overall_timeout: 60_000,
  cache?: false
)
```

#### Streaming Events (Asynchronous)

```elixir
# Start a streaming query
{:ok, query_ref} = Nostr.Client.stream(
  ["wss://relay1.com", "wss://relay2.com"],
  %{kinds: [1]}
)

# Receive events
receive do
  {:nostr_event, ^query_ref, event} ->
    IO.puts("Received event: #{event["id"]}")
    
  {:nostr_eose, ^query_ref, true} ->
    IO.puts("All relays finished")
    
  {:nostr_eose, ^query_ref, false} ->
    IO.puts("One relay finished")
end

# Cancel a streaming query
Nostr.Client.cancel(query_ref)
```

## Configuration

Configure `nostr_access` in your `config/config.exs`:

```elixir
config :nostr_access,
  idle_ms: 500,                    # Inactivity window (ms)
  overall_timeout: 30_000,         # Hard stop timeout (ms)
  cache?: true,                    # Enable/disable caching
  dedup_strategy: Nostr.Dedup.Default  # Deduplication strategy
```

## CLI Reference

The `nostr_access` command-line tool follows the `nak` tool syntax for compatibility:

### Usage

```bash
nostr_access [options] [relay...]
```

### Options

#### Filter Attributes
- `--author, -a` - Only accept events from these authors (pubkey as hex)
- `--id, -i` - Only accept events with these ids (hex)
- `--kind, -k` - Only accept events with these kind numbers
- `--limit, -l` - Only accept up to this number of events
- `--search` - NIP-50 search query (relay support required)
- `--since, -s` - Only accept events newer than this (unix timestamp)
- `--tag, -t` - Takes a tag like `-t e=<id>`, only accept events with these tags
- `--until, -u` - Only accept events older than this (unix timestamp)
- `-d` - Shortcut for `--tag d=<value>`
- `-e` - Shortcut for `--tag e=<value>`
- `-p` - Shortcut for `--tag p=<value>`

#### Output Options
- `--bare` - Print just the filter, not enveloped in a `["REQ", ...]` array
- `--ids-only` - Fetch just a list of event IDs
- `--stream` - Keep subscription open, print events as they arrive
- `--paginate` - Make multiple REQs decreasing 'until' until conditions met
- `--paginate-global-limit` - Global limit for pagination
- `--paginate-interval` - Time between pagination queries (e.g., "5s", "1m")

#### Global Options
- `--help, -h` - Show help
- `--quiet, -q` - Suppress logs and info messages
- `--verbose, -v` - Print more detailed information
- `--version` - Print version

### Examples

```bash
# Generate a filter for kind 1 events
./nostr_access --bare -k 1 -l 10

# Query multiple relays
./nostr_access -k 1 -l 15 wss://relay.example.com wss://nostr-pub.wellorder.net

# Use multiple authors and kinds
./nostr_access --bare -a pubkey1 -a pubkey2 -k 1 -k 6

# With tags and time filters
./nostr_access --bare -k 1 -t e=event123 -s 1700000000 -u 1700003600

# Stream events in real-time
./nostr_access --stream -k 1 wss://relay.example.com

# Paginate through events
./nostr_access --paginate --paginate-global-limit 100 -k 1 wss://relay.example.com
```

## API Reference

### `Nostr.Client.fetch/3`

Fetches events from relays synchronously.

```elixir
@spec fetch([relay_uri], filter, Keyword.t()) :: {:ok, [event]} | {:error, term()}
```

**Options:**
- `:idle_ms` - Inactivity window in milliseconds (default: 500)
- `:overall_timeout` - Hard stop timeout in milliseconds (default: 30_000)
- `:cache?` - Enable/disable caching (default: true)
- `:dedup_strategy` - Deduplication strategy module (default: Nostr.Dedup.Default)

### `Nostr.Client.stream/3`

Starts a streaming query to relays.

```elixir
@spec stream([relay_uri], filter, Keyword.t()) :: {:ok, query_ref} | {:error, term()}
```

**Messages received:**
- `{:nostr_event, query_ref, event}` - When a new event arrives
- `{:nostr_eose, query_ref, done?}` - When a relay sends EOSE

### `Nostr.Client.cancel/1`

Cancels a streaming query.

```elixir
@spec cancel(query_ref) :: :ok | {:error, :not_found}
```

## Filter Examples

```elixir
# Text notes from specific authors
%{kinds: [1], authors: ["pubkey1", "pubkey2"]}

# Recent events
%{kinds: [1], since: System.system_time(:second) - 3600}

# Events with specific tags
%{kinds: [1], "#t" => ["nostr", "elixir"]}

# Addressable events
%{kinds: [30000], authors: ["pubkey"], "#d" => ["identifier"]}

# Limited results
%{kinds: [1], limit: 100}
```

## Architecture

The library uses a supervision tree with the following components:

```
nostr_access.Application (Supervisor, :rest_for_one)
├─ Registry.NostrQueries              # {query_ref, pid}
├─ Nostr.Cache.Events   (Cachex, TTL 2 h)
├─ Nostr.Cache.Miss     (Cachex, TTL 10 min)
├─ Nostr.Telemetry.Supervisor
├─ DynamicSupervisor.QuerySup         # one Nostr.Query per request
└─ DynamicSupervisor.RelayPoolSup     # one per relay URI
    └─ Nostr.RelayPool (GenServer)
        └─ 0–3 Nostr.Connection (WebSockex)   # ≤10 subs each
```

## Deduplication Strategy

The library follows NIP-01 rules for event deduplication:

| Event Class | Uniqueness Key |
|-------------|----------------|
| Addressable (kind 30000-39999) | `{kind, pubkey, d_tag}` |
| Lists & Metadata (kind 0, 10000-19999) | `{kind, pubkey}` |
| All other kinds | `event.id` |

You can implement custom deduplication strategies by implementing the `Nostr.Dedup` behaviour.

## Testing

Run the test suite:

```bash
mix test
```

Run with coverage:

```bash
MIX_ENV=test mix test --cover
```

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgments

- Built for the Nostr protocol (NIP-01)
- Uses WebSockex for WebSocket connections
- Uses Cachex for in-memory caching