mirror of
https://github.com/mikedilger/chorus.git
synced 2026-05-03 06:51:42 +00:00
Compare commits
6 Commits
db8b29dfc4
...
97c040b01c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c040b01c | ||
|
|
c03da62981 | ||
|
|
e7044ad0db | ||
|
|
f1851e793b | ||
|
|
add3a9da9c | ||
|
|
fb1bf64062 |
@ -64,6 +64,15 @@ announce upgrade instructions until release.
|
||||
|
||||
## Change Log
|
||||
|
||||
### Version 2.0
|
||||
|
||||
- IMPORTANT: You need to manually [Migrate](docs/MIGRATION.md) your users and moderators.
|
||||
- Management commands have been added: listeventsneedingmoderation, listadmins, listmoderators,
|
||||
grantmoderator, revokemoderator, listusers, grantuser, revokeuser, stats
|
||||
- fix: subscriptions will be CLOSED: auth-required if any matching event requires auth. Chorus
|
||||
will serve the redacted results first, but will not EOSE.
|
||||
- fix: subscriptions will be CLOSED when completed, without EOSE, if the filter has any ids set.
|
||||
|
||||
### Version 1.7.2
|
||||
|
||||
- Support for NIP-62 (PR #1256) Right to Vanish
|
||||
|
||||
25
docs/MIGRATION.md
Normal file
25
docs/MIGRATION.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Migration
|
||||
|
||||
## From 1.0 to 2.0
|
||||
|
||||
1) Add to your config file `admin_hex_keys` to include the nostr hex keys of administrators.
|
||||
These will (eventually) be allowed to manage users and moderators remotely via the management
|
||||
interface. Note that being an admin does NOT automatically grant user or moderator rights,
|
||||
it ONLY grants the right to administer users.
|
||||
|
||||
2) Users and moderators are now dynamically configured in the database. Use `chorus_cmd` to
|
||||
manage them from the command line:
|
||||
|
||||
* Adding a user: `chorus_cmd add_user <pubkey> 0`
|
||||
* Adding a moderator: `chorus_cmd add_user <pubkey> 1`
|
||||
* Removing a user or moderator: `chorus_cmd rm_user <pubkey>`
|
||||
* Listing users and moderators: `chorus_cmd dump_users`
|
||||
|
||||
3) Remove the following from your config file as these are no longer used:
|
||||
|
||||
* `user_hex_keys` - users are now dynamically configured and configuration is in the database.
|
||||
* `moderator_hex_keys` - moderators are now dynamically configured and configuration is in the database.
|
||||
|
||||
4) Configuration setting `public_key_kex` has been renamed `contact_public_key_hex` and is
|
||||
used only for the NIP-11 data.
|
||||
|
||||
16
src/lib.rs
16
src/lib.rs
@ -374,7 +374,7 @@ impl WebSocketService {
|
||||
if m.len() > self.burst_tokens {
|
||||
log::info!(target: "Client", "{}: Rate limited exceeded", self.peer);
|
||||
let reply = NostrReply::Notice("Rate limit exceeded.".into());
|
||||
self.websocket.send(Message::text(reply.as_json())).await?;
|
||||
self.websocket.send(Message::text(reply.as_json()?)).await?;
|
||||
let error = ChorusError::RateLimitExceeded;
|
||||
self.error_punishment += error.punishment();
|
||||
return Err(error.into());
|
||||
@ -419,7 +419,7 @@ impl WebSocketService {
|
||||
|
||||
// Offer AUTH to clients right off the bat
|
||||
let reply = NostrReply::Auth(self.challenge.clone());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
|
||||
let mut last_message_at = Instant::now();
|
||||
|
||||
@ -493,7 +493,7 @@ impl WebSocketService {
|
||||
let message = NostrReply::Event(subid, event);
|
||||
// note, this is not currently counted in throttling
|
||||
self.websocket
|
||||
.send(Message::text(message.as_json()))
|
||||
.send(Message::text(message.as_json()?))
|
||||
.await?;
|
||||
continue 'subs;
|
||||
}
|
||||
@ -529,7 +529,7 @@ impl WebSocketService {
|
||||
if message.len() > self.burst_tokens {
|
||||
log::info!(target: "Client", "{}: Rate limited exceeded", self.peer);
|
||||
let reply = NostrReply::Notice("Rate limit exceeded.".into());
|
||||
self.websocket.send(Message::text(reply.as_json())).await?;
|
||||
self.websocket.send(Message::text(reply.as_json()?)).await?;
|
||||
let error = ChorusError::RateLimitExceeded;
|
||||
self.error_punishment += error.punishment();
|
||||
return Err(error.into());
|
||||
@ -556,15 +556,15 @@ impl WebSocketService {
|
||||
if !self.replied {
|
||||
if let Some(subid) = &self.negentropy_sub {
|
||||
let reply = NostrReply::NegErr(subid, format!("error: {e}"));
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
} else {
|
||||
let reply = NostrReply::Notice(format!("error: {}", e.inner));
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
}
|
||||
}
|
||||
if self.error_punishment >= 1.0 {
|
||||
let reply = NostrReply::Notice("Closing due to error(s)".into());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Err(ChorusError::ErrorClose.into());
|
||||
}
|
||||
}
|
||||
@ -573,7 +573,7 @@ impl WebSocketService {
|
||||
let reply = NostrReply::Notice(
|
||||
"binary messages are not processed by this relay".to_owned(),
|
||||
);
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
log::info!(target: "Client",
|
||||
"{}: Received unhandled binary message: {:02X?}",
|
||||
self.peer,
|
||||
|
||||
70
src/nostr.rs
70
src/nostr.rs
@ -46,7 +46,7 @@ impl WebSocketService {
|
||||
} else {
|
||||
log::warn!(target: "Client", "{}: Received unhandled text message: {}", self.peer, msg);
|
||||
let reply = NostrReply::Notice("Command unrecognized".to_owned());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -106,7 +106,7 @@ impl WebSocketService {
|
||||
}
|
||||
_ => NostrReply::Closed(&subid, NostrReplyPrefix::Error, format!("{}", e.inner)),
|
||||
};
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
Err(e)
|
||||
} else {
|
||||
Ok(())
|
||||
@ -141,7 +141,7 @@ impl WebSocketService {
|
||||
NostrReplyPrefix::AuthRequired,
|
||||
"DM kinds were included in the filters".to_owned(),
|
||||
);
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@ -199,29 +199,35 @@ impl WebSocketService {
|
||||
}
|
||||
}
|
||||
let reply = NostrReply::Count(subid, events.len(), opthll);
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
} else {
|
||||
for event in events.drain(..) {
|
||||
let reply = NostrReply::Event(subid, event);
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
}
|
||||
|
||||
// New policy Feb 2025: Redactions trigger a "CLOSED: auth-required" because
|
||||
// some clients will not AUTH otherwise.
|
||||
// (But we also already sent partial results, which I think is good)
|
||||
if redacted {
|
||||
// They need to AUTH first
|
||||
let reply = NostrReply::Closed(
|
||||
subid,
|
||||
NostrReplyPrefix::AuthRequired,
|
||||
"At least one matching event requires AUTH".to_owned(),
|
||||
);
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if completes {
|
||||
// Closed
|
||||
let reply = if redacted {
|
||||
NostrReply::Closed(subid, NostrReplyPrefix::None, "".to_owned())
|
||||
} else {
|
||||
NostrReply::Closed(
|
||||
subid,
|
||||
NostrReplyPrefix::Redacted,
|
||||
"Some matching events could not be served to you.".to_owned(),
|
||||
)
|
||||
};
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
let reply = NostrReply::Closed(subid, NostrReplyPrefix::None, "".to_owned());
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
} else {
|
||||
// EOSE
|
||||
let reply = NostrReply::Eose(subid);
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -301,11 +307,11 @@ impl WebSocketService {
|
||||
},
|
||||
_ => NostrReply::Ok(id, false, NostrReplyPrefix::Error, format!("{}", e.inner)),
|
||||
};
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
Err(e)
|
||||
} else {
|
||||
let reply = NostrReply::Ok(id, true, NostrReplyPrefix::None, "".to_string());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -382,7 +388,7 @@ impl WebSocketService {
|
||||
// message, and clients just presume it was closed.
|
||||
/*
|
||||
let reply = NostrReply::Closed(subid, NostrReplyPrefix::None, "".to_owned());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
@ -410,11 +416,11 @@ impl WebSocketService {
|
||||
}
|
||||
_ => NostrReply::Ok(id, false, NostrReplyPrefix::Error, format!("{}", e.inner)),
|
||||
};
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
Err(e)
|
||||
} else {
|
||||
let reply = NostrReply::Ok(id, true, NostrReplyPrefix::None, "".to_string());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -493,7 +499,7 @@ impl WebSocketService {
|
||||
if !GLOBALS.config.read().enable_negentropy {
|
||||
let reply =
|
||||
NostrReply::NegErr(&subid, "blocked: Negentropy sync is disabled".to_owned());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -527,14 +533,14 @@ impl WebSocketService {
|
||||
// NEG-ERR if the message was empty
|
||||
if incoming_msg.is_empty() {
|
||||
let reply = NostrReply::NegErr(&subid, "error: Empty negentropy message".to_owned());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
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?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -588,11 +594,11 @@ impl WebSocketService {
|
||||
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?;
|
||||
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?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -628,7 +634,7 @@ impl WebSocketService {
|
||||
if !GLOBALS.config.read().enable_negentropy {
|
||||
let reply =
|
||||
NostrReply::NegErr(&subid, "blocked: Negentropy sync is disabled".to_owned());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -650,7 +656,7 @@ impl WebSocketService {
|
||||
// NEG-ERR if the message was empty
|
||||
if incoming_msg.is_empty() {
|
||||
let reply = NostrReply::NegErr(&subid, "error: Empty negentropy message".to_owned());
|
||||
self.send(Message::text(reply.as_json())).await?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -658,14 +664,14 @@ impl WebSocketService {
|
||||
// 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?;
|
||||
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?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@ -673,11 +679,11 @@ impl WebSocketService {
|
||||
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?;
|
||||
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?;
|
||||
self.send(Message::text(reply.as_json()?)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
src/reply.rs
54
src/reply.rs
@ -1,3 +1,4 @@
|
||||
use crate::Error;
|
||||
use pocket_types::{write_hex, Event, Hll8, Id};
|
||||
use std::fmt;
|
||||
|
||||
@ -9,7 +10,6 @@ pub enum NostrReplyPrefix {
|
||||
Duplicate,
|
||||
Blocked,
|
||||
RateLimited,
|
||||
Redacted,
|
||||
Restricted,
|
||||
Invalid,
|
||||
Error,
|
||||
@ -24,7 +24,6 @@ impl fmt::Display for NostrReplyPrefix {
|
||||
NostrReplyPrefix::Duplicate => write!(f, "duplicate: "),
|
||||
NostrReplyPrefix::Blocked => write!(f, "blocked: "),
|
||||
NostrReplyPrefix::RateLimited => write!(f, "rate-limited: "),
|
||||
NostrReplyPrefix::Redacted => write!(f, "redacted: "),
|
||||
NostrReplyPrefix::Restricted => write!(f, "restricted: "),
|
||||
NostrReplyPrefix::Invalid => write!(f, "invalid: "),
|
||||
NostrReplyPrefix::Error => write!(f, "error: "),
|
||||
@ -47,40 +46,59 @@ pub enum NostrReply<'a> {
|
||||
}
|
||||
|
||||
impl NostrReply<'_> {
|
||||
pub fn as_json(&self) -> String {
|
||||
match self {
|
||||
NostrReply::Auth(challenge) => format!(r#"["AUTH","{challenge}"]"#),
|
||||
NostrReply::Event(subid, event) => format!(r#"["EVENT","{subid}",{}]"#, event),
|
||||
NostrReply::Ok(id, ok, prefix, msg) => format!(r#"["OK","{id}",{ok},"{prefix}{msg}"]"#),
|
||||
NostrReply::Eose(subid) => format!(r#"["EOSE","{subid}"]"#),
|
||||
pub fn as_json(&self) -> Result<String, Error> {
|
||||
Ok(match self {
|
||||
NostrReply::Auth(challenge) => {
|
||||
let esc_challenge = escape(challenge)?;
|
||||
format!(r#"["AUTH","{esc_challenge}"]"#)
|
||||
}
|
||||
NostrReply::Event(subid, event) => {
|
||||
let esc_subid = escape(subid)?;
|
||||
format!(r#"["EVENT","{esc_subid}",{event}]"#)
|
||||
}
|
||||
NostrReply::Ok(id, ok, prefix, msg) => {
|
||||
let esc_msg = escape(msg)?;
|
||||
format!(r#"["OK","{id}",{ok},"{prefix}{esc_msg}"]"#)
|
||||
}
|
||||
NostrReply::Eose(subid) => {
|
||||
let esc_subid = escape(subid)?;
|
||||
format!(r#"["EOSE","{esc_subid}"]"#)
|
||||
}
|
||||
NostrReply::Closed(subid, prefix, msg) => {
|
||||
format!(r#"["CLOSED","{subid}","{prefix}{msg}"]"#)
|
||||
}
|
||||
NostrReply::Notice(msg) => format!(r#"["NOTICE","{msg}"]"#),
|
||||
NostrReply::Notice(msg) => {
|
||||
let esc_msg = escape(msg)?;
|
||||
format!(r#"["NOTICE","{esc_msg}"]"#)
|
||||
}
|
||||
NostrReply::Count(subid, c, opthll) => {
|
||||
let esc_subid = escape(subid)?;
|
||||
if let Some(hll) = opthll {
|
||||
let hll = hll.to_hex_string();
|
||||
format!(r#"["COUNT","{subid}",{{"count":{c}, "hll":"{hll}"}}]"#)
|
||||
format!(r#"["COUNT","{esc_subid}",{{"count":{c}, "hll":"{hll}"}}]"#)
|
||||
} else {
|
||||
format!(r#"["COUNT","{subid}",{{"count":{c}}}]"#)
|
||||
format!(r#"["COUNT","{esc_subid}",{{"count":{c}}}]"#)
|
||||
}
|
||||
}
|
||||
NostrReply::NegErr(subid, reason) => {
|
||||
format!(r#"["NEG-ERR","{subid}","{reason}"]"#)
|
||||
let esc_subid = escape(subid)?;
|
||||
let esc_reason = escape(reason)?;
|
||||
format!(r#"["NEG-ERR","{esc_subid}","{esc_reason}"]"#)
|
||||
}
|
||||
NostrReply::NegMsg(subid, msg) => {
|
||||
let esc_subid = escape(subid)?;
|
||||
// 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)
|
||||
format!(r#"["NEG-MSG","{esc_subid}","{}"]"#, msg_hex)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NostrReply<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_json())
|
||||
}
|
||||
fn escape(s: &str) -> Result<String, Error> {
|
||||
let v: Vec<u8> = Vec::with_capacity(256);
|
||||
let e = pocket_types::json::json_escape(s.as_bytes(), v)?;
|
||||
Ok(unsafe { String::from_utf8_unchecked(e) })
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ fn respond(
|
||||
let s: String = serde_json::to_string(&json)?;
|
||||
let response = Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.header("Access-Control-Allow-Headers", "Authorization, *")
|
||||
.header("Access-Control-Allow-Methods", "*")
|
||||
.header("Content-Type", "application/nostr+json+rpc")
|
||||
.status(status)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user