block / block/buzz

relay: multi-value #h filters collapse to lexicographically-first channel (Workflows overview shows empty)

Open
#2,385 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## Summary

The relay's `/query` (HTTP bridge) and REQ paths mishandle a filter with **multiple distinct `#h` values**: instead of Nostr's OR semantics ("match any listed channel"), the query silently collapses to the **lexicographically-first** channel UUID. Every other listed channel's events are dropped.

User-visible impact: the Desktop **Workflows overview screen is empty** for essentially everyone. `get_channels_workflows` batches all member-channel ids into one `#h` filter (`desktop/src-tauri/src/commands/workflows.rs`, whose comment asserts "A nostr `#h` filter matches ANY of its listed values") — but the relay only returns workflows for whichever channel UUID sorts first, typically a DM channel with none.

## Root cause

Two channel extractors disagree, and the bridge catchall composes them into a contradictory query:

- `extract_channel_id_from_filter` (`crates/buzz-relay/src/handlers/req.rs` — singular-filter variant) returns the **first** parseable `#h` value even when several are present (`for val in tag_values { ... return Some(id) }`). Filter tag values live in a `BTreeSet`, so "first" = lexicographic minimum.
- `extract_channel_from_filter` (`crates/buzz-relay/src/api/bridge.rs`) correctly requires `vs.len() == 1` and returns `None` for multi-value filters.

In the bridge catchall (`query_events_authed`), `build_event_query_from_filter` uses the first extractor → `query.channel_id = Some()`, then `apply_access_scope_to_query` is passed the second extractor's `None` → also sets `query.channel_ids = accessible_channels`. Resulting SQL (captured via `log_statement=all`):

```sql
SELECT ... FROM events
WHERE community_id = $1 AND deleted_at IS NULL
AND channel_id = $2 -- first-sorted #h value
AND (channel_id IS NULL OR channel_id IN ($3,$4,$5)) -- access scope
AND kind IN ($6) ...
```

The `channel_id = $2` clause pins the whole query to one channel.

Note the plural WS-subscription variant `extract_channel_id_from_filters` already handles this correctly ("Multiple distinct channel IDs — fall back to global"); the singular variant predates that rule.

## Repro (any relay, any community)

1. Create workflows in channel A (id `57e6...`). Confirm single-channel query returns them:
`POST /query` `[{"kinds":[30620],"#h":["57e6..."]}]` → N events ✅
2. Add a second channel id that sorts **before** A:
`[{"kinds":[30620],"#h":["4e18...","57e6..."]}]` → `[]` ❌
3. Add a second channel id that sorts **after** A:
`[{"kinds":[30620],"#h":["57e6...","6254..."]}]` → N events (only A's) — confirms first-sorted collapse.

`buzz_core::filter::filters_match` implements `#h` OR correctly, so the in-memory post-filter is not the problem — rows never come back from SQL.

## Suggested fix

Make `extract_channel_id_from_filter` return `None` when the filter carries multiple distinct parseable `#h` values (mirroring `extract_channel_id_from_filters`), so the access-scope path takes over and `filters_match` enforces the per-event `#h` OR check. Alternatively push all `#h` values into SQL as an `IN` list. Either way, a regression test: multi-`#h` query must return events from every listed accessible channel.

Happy to send a PR for either shape.

Contributor guide

Open the contributing guide

Research direction

Start with extract_channel_id_from_filter in crates/buzz-relay/src/handlers/req.rs, compare it with extract_channel_from_filter in crates/buzz-relay/src/api/bridge.rs and the plural extractor, then trace query_events_authed and the workflow query in desktop/src-tauri/src/commands/workflows.rs. Reproduce /query with two #h values and add a regression test. Done means multi-value filters return events from every listed accessible channel without breaking single-channel queries.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, sql
Domain
api, backend, databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.