use std::collections::HashMap; use serde::{Deserialize, Deserializer, Serialize}; #[derive(Serialize)] pub struct IdentityInfo { pub pubkey: String, pub display_name: String, } #[derive(Serialize, Deserialize)] pub struct ProfileInfo { pub pubkey: String, pub display_name: Option, pub avatar_url: Option, pub about: Option, pub nip05_handle: Option, pub owner_pubkey: Option, /// `true` when a real kind:0 event was found on the relay; `false` for the /// synthesized fallback returned when no metadata event exists. The /// onboarding gate uses this to distinguish "new user with no profile" from /// "returning user whose display_name happens to be empty". pub has_profile_event: bool, } #[derive(Serialize, Deserialize)] pub struct UserProfileSummaryInfo { pub display_name: Option, pub avatar_url: Option, pub nip05_handle: Option, pub owner_pubkey: Option, #[serde(default)] pub is_agent: bool, } #[derive(Serialize, Deserialize)] pub struct UsersBatchResponse { pub profiles: HashMap, pub missing: Vec, } #[derive(Serialize, Deserialize)] pub struct UserSearchResultInfo { pub pubkey: String, pub display_name: Option, pub avatar_url: Option, pub nip05_handle: Option, pub owner_pubkey: Option, #[serde(default)] pub is_agent: bool, } #[derive(Serialize, Deserialize)] pub struct SearchUsersResponse { pub users: Vec, pub next_cursor: Option, } #[derive(Serialize, Deserialize)] pub struct UserNoteInfo { pub id: String, pub pubkey: String, pub created_at: i64, pub content: String, pub tags: Vec>, } #[derive(Serialize, Deserialize)] pub struct NoteReactionSummary { pub note_id: String, pub emoji: String, pub count: usize, pub pubkeys: Vec, } #[derive(Serialize, Deserialize)] pub struct UserNotesCursor { pub before: i64, pub before_id: String, } #[derive(Serialize, Deserialize)] pub struct UserNotesResponse { pub notes: Vec, pub next_cursor: Option, } #[derive(Serialize, Deserialize)] pub struct ChannelInfo { pub id: String, pub name: String, pub channel_type: String, pub visibility: String, #[serde(deserialize_with = "deserialize_null_string_as_empty")] pub description: String, pub topic: Option, pub purpose: Option, pub member_count: i64, #[serde(default)] pub member_pubkeys: Vec, pub last_message_at: Option, pub archived_at: Option, #[serde(default)] pub participants: Vec, #[serde(default)] pub participant_pubkeys: Vec, #[serde(default = "default_true")] pub is_member: bool, pub ttl_seconds: Option, pub ttl_deadline: Option, } #[derive(Serialize, Deserialize)] pub struct ChannelDetailInfo { pub id: String, pub name: String, pub channel_type: String, pub visibility: String, #[serde(deserialize_with = "deserialize_null_string_as_empty")] pub description: String, pub topic: Option, pub topic_set_by: Option, pub topic_set_at: Option, pub purpose: Option, pub purpose_set_by: Option, pub purpose_set_at: Option, pub created_by: String, pub created_at: String, pub updated_at: String, pub archived_at: Option, pub member_count: i64, pub topic_required: bool, pub max_members: Option, pub nip29_group_id: Option, pub ttl_seconds: Option, pub ttl_deadline: Option, } #[derive(Serialize, Deserialize)] pub struct ChannelMemberInfo { pub pubkey: String, pub role: String, #[serde(default)] pub is_agent: bool, /// Optional — kind:39002 events do not carry per-member join timestamps, /// so this is `None` when populated from a NIP-29 members event. #[serde(default)] pub joined_at: Option, pub display_name: Option, } #[derive(Serialize, Deserialize)] pub struct ChannelMembersResponse { pub members: Vec, pub next_cursor: Option, } #[derive(Serialize, Deserialize)] pub struct FeedItemInfo { pub id: String, pub kind: u32, pub pubkey: String, pub content: String, pub created_at: u64, pub channel_id: Option, pub channel_name: String, #[serde(default)] pub channel_type: Option, pub tags: Vec>, pub category: String, } #[derive(Serialize, Deserialize)] pub struct FeedSections { pub mentions: Vec, pub needs_action: Vec, pub activity: Vec, pub agent_activity: Vec, } #[derive(Serialize, Deserialize)] pub struct FeedMeta { pub since: i64, pub total: u64, pub generated_at: i64, } #[derive(Serialize, Deserialize)] pub struct FeedResponse { pub feed: FeedSections, pub meta: FeedMeta, } #[derive(Serialize, Deserialize)] pub struct SearchHitInfo { pub event_id: String, pub content: String, pub kind: u32, pub pubkey: String, pub channel_id: Option, pub channel_name: Option, pub created_at: u64, pub score: f64, } #[derive(Serialize, Deserialize)] pub struct SearchResponse { pub hits: Vec, pub found: u64, } #[derive(Serialize, Deserialize)] pub struct SendChannelMessageResponse { pub event_id: String, pub parent_event_id: Option, pub root_event_id: Option, pub depth: u32, pub created_at: i64, } #[derive(Serialize, Deserialize)] pub struct ThreadSummary { pub reply_count: u32, pub descendant_count: u32, pub last_reply_at: Option, pub participants: Vec, } #[derive(Serialize, Deserialize)] pub struct ForumMessageInfo { pub event_id: String, pub pubkey: String, pub content: String, pub kind: u32, pub created_at: i64, pub channel_id: String, pub tags: Vec>, #[serde(default)] pub thread_summary: Option, #[serde(default)] pub reactions: serde_json::Value, } #[derive(Serialize, Deserialize)] pub struct ForumPostsResponse { pub messages: Vec, pub next_cursor: Option, } #[derive(Serialize, Deserialize)] pub struct ForumThreadReplyInfo { pub event_id: String, pub pubkey: String, pub content: String, pub kind: u32, pub created_at: i64, pub channel_id: String, pub tags: Vec>, pub parent_event_id: Option, pub root_event_id: Option, pub depth: u32, pub broadcast: bool, #[serde(default)] pub reactions: serde_json::Value, } #[derive(Serialize, Deserialize)] pub struct ForumThreadResponse { pub root: ForumMessageInfo, pub replies: Vec, pub total_replies: u32, pub next_cursor: Option, } /// Forward keyset pagination cursor for `get_thread_replies`. /// /// Thread replies routinely share a `created_at` second (bursty threads), so /// the cursor must carry the last reply's `event_id` as a tiebreak alongside /// its `created_at`. A timestamp-only cursor advances past the entire tied /// second after one page and silently drops every tied reply beyond the page /// limit — the "missed messages" bug this read-path work fixes. The relay /// keysets on `(event_created_at, event_id)` to match. #[derive(Serialize, Deserialize, Clone)] pub struct ThreadCursor { /// `created_at` of the last reply already loaded (Unix seconds). pub created_at: i64, /// Hex event id of that last reply — the tiebreak within a shared second. pub event_id: String, } /// Response for `get_thread_replies` — the full reply subtree under a root /// event, fetched server-side from `thread_metadata` (NOT assembled from the /// channel cache). `events` are raw Nostr events in chronological order; /// `next_cursor` is the composite `(created_at, event_id)` of the last event /// when a full page was returned, for forward keyset paging, else `None`. #[derive(Serialize, Deserialize)] pub struct ThreadRepliesResponse { pub events: Vec, pub next_cursor: Option, } /// Composite backward keyset cursor for channel-timeline paging via the bridge /// (`get_channel_messages_before`). The relay orders `created_at DESC, id ASC` /// and advances past a tied second with `id > before_id`, so the event id is the /// tiebreak that lets paging escape a second denser than one WS page — /// the case a bare `until` cursor cannot advance through. #[derive(Serialize, Deserialize, Clone)] pub struct ChannelPageCursor { /// `created_at` of the last (oldest) message already loaded (Unix seconds). pub created_at: i64, /// Hex event id of that message — the `before_id` tiebreak within a second. pub event_id: String, } /// Response for `get_channel_messages_before` — one keyset page of top-level /// channel history, oldest-last (relay order: `created_at DESC, id ASC`). /// `next_cursor` is the composite key of the last (oldest) event when a full /// page was returned, else `None`. #[derive(Serialize, Deserialize)] pub struct ChannelMessagesPageResponse { pub events: Vec, pub next_cursor: Option, } fn deserialize_null_string_as_empty<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { Ok(Option::::deserialize(deserializer)?.unwrap_or_default()) } fn default_true() -> bool { true } // ── Social / Contact list ─────────────────────────────────────────────────── #[derive(Serialize, Deserialize)] pub struct ContactListResponse { pub id: String, pub pubkey: String, pub created_at: i64, pub tags: Vec>, pub content: String, } #[derive(Serialize, Deserialize)] pub struct ContactEntry { pub pubkey: String, #[serde(default)] pub relay_url: Option, #[serde(default)] pub petname: Option, }