mirror of
https://github.com/mikedilger/chorus.git
synced 2026-08-31 07:31:01 +00:00
Implement Negentropy NIP-77 (nostr-protocol/nips PR #1494)
This commit is contained in:
parent
ac87fc9d29
commit
2e1ca26d3e
@ -4,6 +4,7 @@ pub mod error;
|
|||||||
pub mod filestore;
|
pub mod filestore;
|
||||||
pub mod globals;
|
pub mod globals;
|
||||||
pub mod ip;
|
pub mod ip;
|
||||||
|
mod neg_storage;
|
||||||
pub mod nostr;
|
pub mod nostr;
|
||||||
pub mod reply;
|
pub mod reply;
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
@ -25,6 +26,7 @@ use hyper::{Request, Response};
|
|||||||
use hyper_tungstenite::tungstenite;
|
use hyper_tungstenite::tungstenite;
|
||||||
use hyper_tungstenite::{HyperWebsocket, WebSocketStream};
|
use hyper_tungstenite::{HyperWebsocket, WebSocketStream};
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
|
use neg_storage::NegentropyStorageVector;
|
||||||
use pocket_db::Store;
|
use pocket_db::Store;
|
||||||
use pocket_types::{Id, OwnedFilter, Pubkey};
|
use pocket_types::{Id, OwnedFilter, Pubkey};
|
||||||
use speedy::{Readable, Writable};
|
use speedy::{Readable, Writable};
|
||||||
@ -215,6 +217,7 @@ async fn websocket_thread(peer: HashedPeer, websocket: HyperWebsocket, origin: S
|
|||||||
let mut ws_service = WebSocketService {
|
let mut ws_service = WebSocketService {
|
||||||
peer,
|
peer,
|
||||||
subscriptions: HashMap::new(),
|
subscriptions: HashMap::new(),
|
||||||
|
neg_subscriptions: HashMap::new(),
|
||||||
// We start with a 1-page buffer, and grow it if needed.
|
// We start with a 1-page buffer, and grow it if needed.
|
||||||
buffer: vec![0; 4096],
|
buffer: vec![0; 4096],
|
||||||
websocket,
|
websocket,
|
||||||
@ -350,6 +353,7 @@ async fn websocket_thread(peer: HashedPeer, websocket: HyperWebsocket, origin: S
|
|||||||
struct WebSocketService {
|
struct WebSocketService {
|
||||||
pub peer: HashedPeer,
|
pub peer: HashedPeer,
|
||||||
pub subscriptions: HashMap<String, Vec<OwnedFilter>>,
|
pub subscriptions: HashMap<String, Vec<OwnedFilter>>,
|
||||||
|
pub neg_subscriptions: HashMap<String, NegentropyStorageVector>,
|
||||||
pub buffer: Vec<u8>,
|
pub buffer: Vec<u8>,
|
||||||
pub websocket: WebSocketStream<TokioIo<Upgraded>>,
|
pub websocket: WebSocketStream<TokioIo<Upgraded>>,
|
||||||
pub last_message: Instant,
|
pub last_message: Instant,
|
||||||
@ -425,7 +429,7 @@ impl WebSocketService {
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
instant = interval.tick() => {
|
instant = interval.tick() => {
|
||||||
// Drop them if they have no subscriptions
|
// Drop them if they have no subscriptions
|
||||||
if self.subscriptions.is_empty() {
|
if self.subscriptions.is_empty() && self.neg_subscriptions.is_empty() {
|
||||||
// And they are idle for timeout_seconds with no subscriptions
|
// And they are idle for timeout_seconds with no subscriptions
|
||||||
if last_message_at + Duration::from_secs(timeout_seconds) < instant {
|
if last_message_at + Duration::from_secs(timeout_seconds) < instant {
|
||||||
self.wsclose(ChorusError::TimedOut.into()).await?;
|
self.wsclose(ChorusError::TimedOut.into()).await?;
|
||||||
|
|||||||
123
src/neg_storage.rs
Normal file
123
src/neg_storage.rs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
use negentropy::{Bound, Error, Id, Item, NegentropyStorageBase};
|
||||||
|
|
||||||
|
// We construct our own NegentropyStorageVector since NegentropyStorageBase
|
||||||
|
// is not implemented for a &NegentropyStorageVector upstream
|
||||||
|
|
||||||
|
/// Negentropy Storage Vector
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct NegentropyStorageVector {
|
||||||
|
items: Vec<Item>,
|
||||||
|
sealed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NegentropyStorageBase for &NegentropyStorageVector {
|
||||||
|
fn size(&self) -> Result<usize, Error> {
|
||||||
|
self.check_sealed()?;
|
||||||
|
Ok(self.items.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_item(&self, i: usize) -> Result<Option<Item>, Error> {
|
||||||
|
self.check_sealed()?;
|
||||||
|
Ok(self.items.get(i).copied())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iterate(
|
||||||
|
&self,
|
||||||
|
begin: usize,
|
||||||
|
end: usize,
|
||||||
|
cb: &mut dyn FnMut(Item, usize) -> Result<bool, Error>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.check_sealed()?;
|
||||||
|
self.check_bounds(begin, end)?;
|
||||||
|
|
||||||
|
for i in begin..end {
|
||||||
|
if !cb(self.items[i], i)? {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_lower_bound(&self, mut first: usize, last: usize, value: &Bound) -> usize {
|
||||||
|
let mut count: usize = last - first;
|
||||||
|
|
||||||
|
while count > 0 {
|
||||||
|
let mut it: usize = first;
|
||||||
|
let step: usize = count / 2;
|
||||||
|
it += step;
|
||||||
|
|
||||||
|
if self.items[it] < value.item {
|
||||||
|
it += 1;
|
||||||
|
first = it;
|
||||||
|
count -= step + 1;
|
||||||
|
} else {
|
||||||
|
count = step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NegentropyStorageVector {
|
||||||
|
/// Create new storage
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new storage with capacity
|
||||||
|
#[inline]
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
items: Vec::with_capacity(capacity),
|
||||||
|
sealed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert item
|
||||||
|
pub fn insert(&mut self, created_at: u64, id: Id) -> Result<(), Error> {
|
||||||
|
if self.sealed {
|
||||||
|
return Err(Error::AlreadySealed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem: Item = Item::with_timestamp_and_id(created_at, id);
|
||||||
|
self.items.push(elem);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seal
|
||||||
|
pub fn seal(&mut self) -> Result<(), Error> {
|
||||||
|
if self.sealed {
|
||||||
|
return Err(Error::AlreadySealed);
|
||||||
|
}
|
||||||
|
self.sealed = true;
|
||||||
|
|
||||||
|
self.items.sort();
|
||||||
|
self.items.dedup();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unseal
|
||||||
|
pub fn unseal(&mut self) -> Result<(), Error> {
|
||||||
|
self.sealed = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_sealed(&self) -> Result<(), Error> {
|
||||||
|
if !self.sealed {
|
||||||
|
return Err(Error::NotSealed);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_bounds(&self, begin: usize, end: usize) -> Result<(), Error> {
|
||||||
|
if begin > end || end > self.items.len() {
|
||||||
|
return Err(Error::BadRange);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/nostr.rs
237
src/nostr.rs
@ -1,10 +1,12 @@
|
|||||||
use crate::error::{ChorusError, Error};
|
use crate::error::{ChorusError, Error};
|
||||||
use crate::globals::GLOBALS;
|
use crate::globals::GLOBALS;
|
||||||
|
use crate::neg_storage::NegentropyStorageVector;
|
||||||
use crate::reply::{NostrReply, NostrReplyPrefix};
|
use crate::reply::{NostrReply, NostrReplyPrefix};
|
||||||
use crate::WebSocketService;
|
use crate::WebSocketService;
|
||||||
use hyper_tungstenite::tungstenite::Message;
|
use hyper_tungstenite::tungstenite::Message;
|
||||||
|
use negentropy::{Bytes, Negentropy};
|
||||||
use pocket_types::json::{eat_whitespace, json_unescape, verify_char};
|
use pocket_types::json::{eat_whitespace, json_unescape, verify_char};
|
||||||
use pocket_types::{Event, Filter, Hll8, Kind, OwnedFilter, Pubkey, Time};
|
use pocket_types::{read_hex, Event, Filter, Hll8, Kind, OwnedFilter, Pubkey, Time};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
impl WebSocketService {
|
impl WebSocketService {
|
||||||
@ -32,6 +34,12 @@ impl WebSocketService {
|
|||||||
self.close(msg, inpos + 6).await?;
|
self.close(msg, inpos + 6).await?;
|
||||||
} else if &input[inpos..inpos + 5] == b"AUTH\"" {
|
} else if &input[inpos..inpos + 5] == b"AUTH\"" {
|
||||||
self.auth(msg, inpos + 5).await?;
|
self.auth(msg, inpos + 5).await?;
|
||||||
|
} else if &input[inpos..inpos + 9] == b"NEG-OPEN\"" {
|
||||||
|
self.neg_open(msg, inpos + 9).await?;
|
||||||
|
} else if &input[inpos..inpos + 8] == b"NEG-MSG\"" {
|
||||||
|
self.neg_msg(msg, inpos + 8).await?;
|
||||||
|
} else if &input[inpos..inpos + 10] == b"NEG-CLOSE\"" {
|
||||||
|
self.neg_close(msg, inpos + 10).await?;
|
||||||
} else {
|
} else {
|
||||||
log::warn!(target: "Client", "{}: Received unhandled text message: {}", self.peer, msg);
|
log::warn!(target: "Client", "{}: Received unhandled text message: {}", self.peer, msg);
|
||||||
let reply = NostrReply::Notice("Command unrecognized".to_owned());
|
let reply = NostrReply::Notice("Command unrecognized".to_owned());
|
||||||
@ -435,6 +443,233 @@ impl WebSocketService {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn neg_open(&mut self, msg: &str, mut inpos: usize) -> Result<(), Error> {
|
||||||
|
let input = msg.as_bytes();
|
||||||
|
|
||||||
|
// ["NEG-OPEN", "<subid>", "<filter>", "<hex-message>"]
|
||||||
|
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b',', &mut inpos)?;
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
|
||||||
|
let mut outpos = 0;
|
||||||
|
|
||||||
|
// Read the subid into the session buffer
|
||||||
|
let subid = {
|
||||||
|
verify_char(input, b'"', &mut inpos)?;
|
||||||
|
let (inlen, outlen) = json_unescape(&input[inpos..], &mut self.buffer[outpos..])?;
|
||||||
|
inpos += inlen;
|
||||||
|
let subid = unsafe {
|
||||||
|
String::from_utf8_unchecked(self.buffer[outpos..outpos + outlen].to_owned())
|
||||||
|
};
|
||||||
|
outpos += outlen;
|
||||||
|
verify_char(input, b'"', &mut inpos)?; // FIXME: json_unescape should eat the closing quote
|
||||||
|
subid
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the filter into the session buffer
|
||||||
|
let filter = {
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b',', &mut inpos)?;
|
||||||
|
// whitespace after the comma is handled within Filter::from_json
|
||||||
|
let (incount, outcount, filter) =
|
||||||
|
Filter::from_json(&input[inpos..], &mut self.buffer[outpos..])?;
|
||||||
|
inpos += incount;
|
||||||
|
outpos += outcount;
|
||||||
|
filter.to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the negentropy message
|
||||||
|
let incoming_msg = {
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b',', &mut inpos)?;
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b'"', &mut inpos)?;
|
||||||
|
let (inlen, outlen) = json_unescape(&input[inpos..], &mut self.buffer[outpos..])?;
|
||||||
|
inpos += inlen;
|
||||||
|
verify_char(input, b'"', &mut inpos)?;
|
||||||
|
let mut msg = vec![0; outlen / 2];
|
||||||
|
read_hex!(&self.buffer[outpos..outpos + outlen], &mut msg, outlen / 2)?;
|
||||||
|
//outpos += outlen;
|
||||||
|
msg
|
||||||
|
};
|
||||||
|
|
||||||
|
// NEG-ERR if the message was empty
|
||||||
|
if incoming_msg.is_empty() {
|
||||||
|
let reply = NostrReply::NegErr(&subid, "Empty negentropy message".to_owned());
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the version is too high, respond with our version number
|
||||||
|
if incoming_msg[0] != 0x61 {
|
||||||
|
let reply = NostrReply::NegMsg(&subid, vec![0x61]);
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = self.user;
|
||||||
|
let authorized_user = authorized_user(&user);
|
||||||
|
|
||||||
|
// Find all matching events
|
||||||
|
let mut events: Vec<&Event> = Vec::new();
|
||||||
|
let screen = |event: &Event| {
|
||||||
|
let event_flags = event_flags(event, &user);
|
||||||
|
screen_outgoing_event(event, &event_flags, authorized_user)
|
||||||
|
};
|
||||||
|
let filter_events = {
|
||||||
|
let config = &*GLOBALS.config.read();
|
||||||
|
GLOBALS.store.get().unwrap().find_events(
|
||||||
|
&filter,
|
||||||
|
config.allow_scraping,
|
||||||
|
config.allow_scrape_if_limited_to,
|
||||||
|
config.allow_scrape_if_max_seconds,
|
||||||
|
screen,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
events.extend(filter_events);
|
||||||
|
events.sort_by(|a, b| {
|
||||||
|
a.created_at()
|
||||||
|
.cmp(&b.created_at())
|
||||||
|
.then(a.id().cmp(&b.id()))
|
||||||
|
});
|
||||||
|
events.dedup();
|
||||||
|
|
||||||
|
let mut nsv = NegentropyStorageVector::with_capacity(events.len());
|
||||||
|
for event in &events {
|
||||||
|
let id = negentropy::Id::from_slice(event.id().as_slice())?;
|
||||||
|
let time = event.created_at().as_u64();
|
||||||
|
nsv.insert(time, id)?;
|
||||||
|
}
|
||||||
|
nsv.seal()?;
|
||||||
|
|
||||||
|
// Save the matching events under the subscription Id
|
||||||
|
self.neg_subscriptions.insert(subid.clone(), nsv);
|
||||||
|
|
||||||
|
// Look it up again immediately
|
||||||
|
let Some(nsv) = self.neg_subscriptions.get(&subid) else {
|
||||||
|
return Err(ChorusError::General(
|
||||||
|
"NEG-OPEN inserted data is immediately missing!".to_owned(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut neg = Negentropy::new(nsv, 1024 * 1024)?; // websocket frame size limit
|
||||||
|
match neg.reconcile(&Bytes::from(incoming_msg)) {
|
||||||
|
Ok(response) => {
|
||||||
|
let reply = NostrReply::NegMsg(&subid, response.as_bytes().to_owned());
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let reply = NostrReply::NegErr(&subid, format!("{e}"));
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn neg_msg(&mut self, msg: &str, mut inpos: usize) -> Result<(), Error> {
|
||||||
|
let input = msg.as_bytes();
|
||||||
|
|
||||||
|
// ["NEG-MSG", "<subid>", "<hex-message>"]
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b',', &mut inpos)?;
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
|
||||||
|
let mut outpos = 0;
|
||||||
|
|
||||||
|
// Read the subid into the session buffer
|
||||||
|
let subid = {
|
||||||
|
verify_char(input, b'"', &mut inpos)?;
|
||||||
|
let (inlen, outlen) = json_unescape(&input[inpos..], &mut self.buffer[outpos..])?;
|
||||||
|
inpos += inlen;
|
||||||
|
let subid = unsafe {
|
||||||
|
String::from_utf8_unchecked(self.buffer[outpos..outpos + outlen].to_owned())
|
||||||
|
};
|
||||||
|
outpos += outlen;
|
||||||
|
verify_char(input, b'"', &mut inpos)?; // FIXME: json_unescape should eat the closing quote
|
||||||
|
subid
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the negentropy message
|
||||||
|
let incoming_msg = {
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b',', &mut inpos)?;
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b'"', &mut inpos)?;
|
||||||
|
let (inlen, outlen) = json_unescape(&input[inpos..], &mut self.buffer[outpos..])?;
|
||||||
|
inpos += inlen;
|
||||||
|
verify_char(input, b'"', &mut inpos)?;
|
||||||
|
let mut msg = vec![0; outlen / 2];
|
||||||
|
read_hex!(&self.buffer[outpos..outpos + outlen], &mut msg, outlen / 2)?;
|
||||||
|
// outpos += outlen;
|
||||||
|
msg
|
||||||
|
};
|
||||||
|
|
||||||
|
// NEG-ERR if the message was empty
|
||||||
|
if incoming_msg.is_empty() {
|
||||||
|
let reply = NostrReply::NegErr(&subid, "Empty negentropy message".to_owned());
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the version is too high, return an error (version negotiation should
|
||||||
|
// have already happened in NEG-OPEN)
|
||||||
|
if incoming_msg[0] != 0x61 {
|
||||||
|
let reply = NostrReply::NegErr(&subid, "Version mismatch".to_owned());
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the events we have
|
||||||
|
let Some(nsv) = self.neg_subscriptions.get(&subid) else {
|
||||||
|
let reply = NostrReply::NegErr(&subid, "Subscription not found".to_owned());
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut neg = Negentropy::new(nsv, 1024 * 1024)?; // websocket frame size limit
|
||||||
|
match neg.reconcile(&Bytes::from(incoming_msg)) {
|
||||||
|
Ok(response) => {
|
||||||
|
let reply = NostrReply::NegMsg(&subid, response.as_bytes().to_owned());
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let reply = NostrReply::NegErr(&subid, format!("{e}"));
|
||||||
|
self.send(Message::text(reply.as_json())).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn neg_close(&mut self, msg: &str, mut inpos: usize) -> Result<(), Error> {
|
||||||
|
let input = msg.as_bytes();
|
||||||
|
|
||||||
|
// ["NEG-CLOSE", "<subid>"]
|
||||||
|
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
verify_char(input, b',', &mut inpos)?;
|
||||||
|
eat_whitespace(input, &mut inpos);
|
||||||
|
|
||||||
|
// Read the subid into the session buffer
|
||||||
|
let subid = {
|
||||||
|
verify_char(input, b'"', &mut inpos)?;
|
||||||
|
let (inlen, outlen) = json_unescape(&input[inpos..], &mut self.buffer)?;
|
||||||
|
inpos += inlen;
|
||||||
|
let subid = unsafe { String::from_utf8_unchecked(self.buffer[..outlen].to_owned()) };
|
||||||
|
verify_char(input, b'"', &mut inpos)?; // FIXME: json_unescape should eat the closing quote
|
||||||
|
subid
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the subscription
|
||||||
|
self.neg_subscriptions.remove(&subid);
|
||||||
|
|
||||||
|
// No need to reply to the client
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn screen_incoming_event(
|
async fn screen_incoming_event(
|
||||||
|
|||||||
14
src/reply.rs
14
src/reply.rs
@ -1,4 +1,4 @@
|
|||||||
use pocket_types::{Event, Hll8, Id};
|
use pocket_types::{write_hex, Event, Hll8, Id};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@ -40,6 +40,8 @@ pub enum NostrReply<'a> {
|
|||||||
Closed(&'a str, NostrReplyPrefix, String),
|
Closed(&'a str, NostrReplyPrefix, String),
|
||||||
Notice(String),
|
Notice(String),
|
||||||
Count(&'a str, usize, Option<Hll8>),
|
Count(&'a str, usize, Option<Hll8>),
|
||||||
|
NegErr(&'a str, String),
|
||||||
|
NegMsg(&'a str, Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrReply<'_> {
|
impl NostrReply<'_> {
|
||||||
@ -61,6 +63,16 @@ impl NostrReply<'_> {
|
|||||||
format!(r#"["COUNT","{subid}",{{"count":{c}}}]"#)
|
format!(r#"["COUNT","{subid}",{{"count":{c}}}]"#)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NostrReply::NegErr(subid, reason) => {
|
||||||
|
format!(r#"["NEG-ERR","{subid}","{reason}"]"#)
|
||||||
|
}
|
||||||
|
NostrReply::NegMsg(subid, msg) => {
|
||||||
|
// write msg as hex
|
||||||
|
let mut buf: Vec<u8> = vec![0; msg.len() * 2];
|
||||||
|
write_hex!(msg, &mut buf, msg.len()).unwrap();
|
||||||
|
let msg_hex = unsafe { std::str::from_utf8_unchecked(&buf) };
|
||||||
|
format!(r#"["NEG-MSG","{subid}","{}"]"#, msg_hex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user