mirror of
https://github.com/mikedilger/chorus.git
synced 2026-05-03 06:51:42 +00:00
Compare commits
10 Commits
95ee2a572a
...
da897b950e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da897b950e | ||
|
|
cbd3d0fa19 | ||
|
|
61d26c0d85 | ||
|
|
5941425799 | ||
|
|
ef4ec80fa0 | ||
|
|
2bc75aa36b | ||
|
|
cb431b6b00 | ||
|
|
f02c071474 | ||
|
|
99124ba134 | ||
|
|
8ab043698f |
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -262,7 +262,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chorus"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitcoin_hashes 0.14.0",
|
||||
@ -1281,7 +1281,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
[[package]]
|
||||
name = "pocket-db"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mikedilger/pocket?branch=master#16a27ec6d8b4b294bae943a57fc525389e446489"
|
||||
source = "git+https://github.com/mikedilger/pocket?branch=master#e6d55e205cc140cc04728960c2ca4e6e906808ef"
|
||||
dependencies = [
|
||||
"heed",
|
||||
"libc",
|
||||
@ -1292,7 +1292,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "pocket-types"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/mikedilger/pocket?branch=master#16a27ec6d8b4b294bae943a57fc525389e446489"
|
||||
source = "git+https://github.com/mikedilger/pocket?branch=master#e6d55e205cc140cc04728960c2ca4e6e906808ef"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"secp256k1 0.28.2",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chorus"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
description = "A personal relay for nostr"
|
||||
authors = ["Mike Dilger <mike@mikedilger.com>"]
|
||||
license = "MIT"
|
||||
|
||||
@ -64,9 +64,12 @@ announce upgrade instructions until release.
|
||||
|
||||
## Change Log
|
||||
|
||||
### Master
|
||||
### Version 1.7.2
|
||||
|
||||
- Support for NIP-62 (PR #1256) Right to Vanish
|
||||
- Fix NIP-86 management request header determination
|
||||
- Management adds: numconnections, uptime
|
||||
- chorus_cmd: fetch_by_id
|
||||
|
||||
### Version 1.7.1
|
||||
|
||||
|
||||
@ -126,21 +126,13 @@ key_pem_path = "/opt/chorus/etc/tls/privkey.pem"
|
||||
# open_relay = false
|
||||
|
||||
|
||||
# These are the public keys (hex format) of your relay's authorized users. See BEHAVIOR.md
|
||||
# to understand how chorus uses these.
|
||||
# These are the public keys (hex format) of your relay's administrators. This does NOT
|
||||
# automatically make them a relay user, but it will eventually allow them to add/remove users
|
||||
# and moderators.
|
||||
#
|
||||
# Default is []
|
||||
#
|
||||
user_hex_keys = []
|
||||
|
||||
|
||||
# These are the public keys (hex format) of your relay's moderators.
|
||||
# Moderators can moderate the relay using the following NIP PR:
|
||||
# https://github.com/nostr-protocol/nips/pull/1325
|
||||
#
|
||||
# Default is []
|
||||
#
|
||||
moderator_hex_keys = []
|
||||
admin_hex_keys = []
|
||||
|
||||
|
||||
# This is a boolean indicating whether or not chorus verifies incoming events.
|
||||
|
||||
@ -116,15 +116,9 @@ If open_relay true, the relay behaves as an open public relay.
|
||||
|
||||
Default is false.
|
||||
|
||||
### user_hex_keys
|
||||
### admin_hex_keys
|
||||
|
||||
These are the public keys (hex format) of your relay's authorized users. See [BEHAVIOR.md](BEHAVIOR.md) to understand how chorus uses these.
|
||||
|
||||
Default is `[]`
|
||||
|
||||
### moderator_hex_keys
|
||||
|
||||
These are the public keys (hex format) of your relay's moderators. Moderators can moderate the relay using the [NIP 86: Relay Management API](https://github.com/nostr-protocol/nips/pull/1325)
|
||||
These are the public keys (hex format) of your relay's administrators. This does NOT automatically make them a relay user, but it will eventually allow them to add/remove users and moderators.
|
||||
|
||||
Default is `[]`
|
||||
|
||||
|
||||
@ -1,8 +1,27 @@
|
||||
# Chorus Management
|
||||
|
||||
This page is about using the online Management API.
|
||||
This page is about using the online Management API or command line [TOOLS.md](TOOLS.md)
|
||||
to manage users and moderate events.
|
||||
|
||||
You can also do management and moderation via command line [TOOLS.md](TOOLS.md).
|
||||
## Managing users
|
||||
|
||||
To list users: `chorus_cmd <configtoml> dump_users`
|
||||
|
||||
To add a user: `chorus_cmd <configtoml> add_user <pubkeyhex> 0`
|
||||
|
||||
To add a moderator: `chorus_cmd <configtoml> add_user <pubkeyhex> 1`
|
||||
|
||||
To remove moderator flag, just add the user again with 0.
|
||||
|
||||
To remove a user: `chorus_cmd <configtoml> rm_user <pubkeyhex>`
|
||||
|
||||
## Managing events
|
||||
|
||||
To cycle through all events needing moderation, and address each one, use `chorus_moderate <configtoml>`
|
||||
|
||||
To remove an event by id: `chorus_cmd <configtoml> delete_by_id <idhex>`
|
||||
|
||||
To remove multiple events by pubkey: `chorus_cmd <configtoml> delete_by_pubkey <pubkeyhex>`
|
||||
|
||||
## Relay Management NIP
|
||||
|
||||
@ -10,11 +29,11 @@ The Relay Management API is in flux currently. It is still a pull request on the
|
||||
|
||||
This document may go out of date as things are changing rapidly.
|
||||
|
||||
## The status of a pubkey
|
||||
## The status of a pubkey (user)
|
||||
|
||||
Users can be in one of four states: Authorized, Approved, Banned, and Default.
|
||||
Users can be in one of four moderation states: Authorized, Approved, Banned, and Default.
|
||||
|
||||
**Authorized**: These are the users you configured in your chorus.toml file, the users that the chorus was setup to serve, or the paying users (if you sell service). These user's events are always accepted and these users can read everything (except other user's DMs for example). These users are statically configured in the config file and cannot be changed dynamically.
|
||||
**Authorized**: These are the users you have added as authorized (whether or not they are a moderator). These user's events are always accepted and these users can read everything (except other user's DMs for example).
|
||||
|
||||
**Approved**: These are users who are not authorized, but which via moderation have been approved. Approved user's posts are publically available to anybody to read. Because they are not authorized, they can only post replies to authorized users.
|
||||
|
||||
|
||||
@ -12,10 +12,7 @@ name = "Chorus Sample"
|
||||
description = "A sample run of the Chorus relay"
|
||||
# icon_url =
|
||||
open_relay = false
|
||||
user_hex_keys = [
|
||||
"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"
|
||||
]
|
||||
moderator_hex_keys = [
|
||||
admin_hex_keys = [
|
||||
"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"
|
||||
]
|
||||
verify_events = true
|
||||
|
||||
@ -67,6 +67,33 @@ fn main() -> Result<(), Error> {
|
||||
println!("Not found.");
|
||||
}
|
||||
}
|
||||
"dump_users" => {
|
||||
let users = chorus::dump_authorized_users(&store)?;
|
||||
for (pubkey, moderator) in users.iter() {
|
||||
println!("{} {}", pubkey, if *moderator { "moderator" } else { "" });
|
||||
}
|
||||
}
|
||||
"add_user" => {
|
||||
let pubstr = args.next().ok_or::<Error>(
|
||||
ChorusError::General("Pubkey argument missing".to_owned()).into(),
|
||||
)?;
|
||||
let pk: Pubkey = Pubkey::read_hex(pubstr.as_bytes())?;
|
||||
|
||||
let moderator = args.next().ok_or::<Error>(
|
||||
ChorusError::General("Moderator argument missing".to_owned()).into(),
|
||||
)?;
|
||||
let moderator: bool = moderator == "1";
|
||||
|
||||
chorus::add_authorized_user(&store, pk, moderator)?;
|
||||
}
|
||||
"rm_user" => {
|
||||
let pubstr = args.next().ok_or::<Error>(
|
||||
ChorusError::General("Pubkey argument missing".to_owned()).into(),
|
||||
)?;
|
||||
let pk: Pubkey = Pubkey::read_hex(pubstr.as_bytes())?;
|
||||
|
||||
chorus::rm_authorized_user(&store, pk)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(ChorusError::General("Unknown command.".to_owned()).into());
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ fn main() -> Result<(), Error> {
|
||||
}
|
||||
|
||||
// Skip if the author is authorized user
|
||||
if config.user_keys.contains(&event.pubkey()) {
|
||||
if chorus::is_authorized_user(event.pubkey()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -23,8 +23,7 @@ pub struct FriendlyConfig {
|
||||
pub contact: Option<String>,
|
||||
pub public_key_hex: Option<String>,
|
||||
pub open_relay: bool,
|
||||
pub user_hex_keys: Vec<String>,
|
||||
pub moderator_hex_keys: Vec<String>,
|
||||
pub admin_hex_keys: Vec<String>,
|
||||
pub verify_events: bool,
|
||||
pub allow_scraping: bool,
|
||||
pub allow_scrape_if_limited_to: u32,
|
||||
@ -64,8 +63,7 @@ impl Default for FriendlyConfig {
|
||||
contact: None,
|
||||
public_key_hex: None,
|
||||
open_relay: false,
|
||||
user_hex_keys: vec![],
|
||||
moderator_hex_keys: vec![],
|
||||
admin_hex_keys: vec![],
|
||||
verify_events: true,
|
||||
allow_scraping: false,
|
||||
allow_scrape_if_limited_to: 100,
|
||||
@ -107,8 +105,7 @@ impl FriendlyConfig {
|
||||
contact,
|
||||
public_key_hex,
|
||||
open_relay,
|
||||
user_hex_keys,
|
||||
moderator_hex_keys,
|
||||
admin_hex_keys,
|
||||
verify_events,
|
||||
allow_scraping,
|
||||
allow_scrape_if_limited_to,
|
||||
@ -135,14 +132,9 @@ impl FriendlyConfig {
|
||||
public_key = Some(Pubkey::read_hex(pkh.as_bytes())?);
|
||||
};
|
||||
|
||||
let mut user_keys: Vec<Pubkey> = Vec::with_capacity(user_hex_keys.len());
|
||||
for pkh in user_hex_keys.iter() {
|
||||
user_keys.push(Pubkey::read_hex(pkh.as_bytes())?);
|
||||
}
|
||||
|
||||
let mut moderator_keys: Vec<Pubkey> = Vec::with_capacity(moderator_hex_keys.len());
|
||||
for pkh in moderator_hex_keys.iter() {
|
||||
moderator_keys.push(Pubkey::read_hex(pkh.as_bytes())?);
|
||||
let mut admin_keys: Vec<Pubkey> = Vec::with_capacity(admin_hex_keys.len());
|
||||
for pkh in admin_hex_keys.iter() {
|
||||
admin_keys.push(Pubkey::read_hex(pkh.as_bytes())?);
|
||||
}
|
||||
|
||||
let hostname = Host::parse(&hostname)?;
|
||||
@ -170,10 +162,8 @@ impl FriendlyConfig {
|
||||
contact,
|
||||
public_key,
|
||||
open_relay,
|
||||
user_keys,
|
||||
user_hex_keys,
|
||||
moderator_keys,
|
||||
moderator_hex_keys,
|
||||
admin_keys,
|
||||
admin_hex_keys,
|
||||
verify_events,
|
||||
allow_scraping,
|
||||
allow_scrape_if_limited_to,
|
||||
@ -214,10 +204,8 @@ pub struct Config {
|
||||
pub contact: Option<String>,
|
||||
pub public_key: Option<Pubkey>,
|
||||
pub open_relay: bool,
|
||||
pub user_keys: Vec<Pubkey>,
|
||||
pub user_hex_keys: Vec<String>,
|
||||
pub moderator_keys: Vec<Pubkey>,
|
||||
pub moderator_hex_keys: Vec<String>,
|
||||
pub admin_keys: Vec<Pubkey>,
|
||||
pub admin_hex_keys: Vec<String>,
|
||||
pub verify_events: bool,
|
||||
pub allow_scraping: bool,
|
||||
pub allow_scrape_if_limited_to: u32,
|
||||
|
||||
87
src/lib.rs
87
src/lib.rs
@ -478,7 +478,7 @@ impl WebSocketService {
|
||||
.get_event_by_offset(new_event_offset)?;
|
||||
|
||||
let event_flags = nostr::event_flags(event, &self.user);
|
||||
let authorized_user = nostr::authorized_user(&self.user);
|
||||
let authorized_user = self.user.map(|pk| is_authorized_user(pk)).unwrap_or(false);
|
||||
|
||||
'subs: for (subid, filters) in self.subscriptions.iter() {
|
||||
for filter in filters.iter() {
|
||||
@ -673,6 +673,7 @@ pub fn setup_store(config: &Config) -> Result<Store, Error> {
|
||||
"approved-events", // id.as_slice() -> u8(bool)
|
||||
"approved-pubkeys", // pubkey.as_slice() -> u8(bool)
|
||||
"ip_data", // HashedIp.0 -> IpData
|
||||
"users", // pubkey.as_slice() -> u8(bool) true if moderator
|
||||
],
|
||||
)?;
|
||||
Ok(store)
|
||||
@ -755,7 +756,9 @@ pub fn get_event_approval(store: &Store, id: Id) -> Result<Option<bool>, Error>
|
||||
"approved-events",
|
||||
)))?;
|
||||
let txn = store.read_txn()?;
|
||||
Ok(approved_events.get(&txn, id.as_slice())?.map(|u| u[0] != 0)) // FIXME in case data is zero length this will panic
|
||||
Ok(approved_events
|
||||
.get(&txn, id.as_slice())?
|
||||
.map(|u| !u.is_empty() && u[0] != 0))
|
||||
}
|
||||
|
||||
/// Dump all event approval statuses
|
||||
@ -770,7 +773,7 @@ pub fn dump_event_approvals(store: &Store) -> Result<Vec<(Id, bool)>, Error> {
|
||||
for i in approved_events.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let id = Id::from_bytes(key.try_into().unwrap());
|
||||
let approval: bool = val[0] != 0; // FIXME in case data is zero length this will panic
|
||||
let approval: bool = !val.is_empty() && val[0] != 0;
|
||||
output.push((id, approval));
|
||||
}
|
||||
Ok(output)
|
||||
@ -812,7 +815,7 @@ pub fn get_pubkey_approval(store: &Store, pubkey: Pubkey) -> Result<Option<bool>
|
||||
let txn = store.read_txn()?;
|
||||
Ok(approved_pubkeys
|
||||
.get(&txn, pubkey.as_slice())?
|
||||
.map(|u| u[0] != 0)) // FIXME in case data is zero length this will panic
|
||||
.map(|u| !u.is_empty() && u[0] != 0))
|
||||
}
|
||||
|
||||
/// Dump all pubkey approval statuses
|
||||
@ -827,8 +830,82 @@ pub fn dump_pubkey_approvals(store: &Store) -> Result<Vec<(Pubkey, bool)>, Error
|
||||
for i in approved_pubkeys.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let pubkey = Pubkey::from_bytes(key.try_into().unwrap());
|
||||
let approval: bool = val[0] != 0; // FIXME in case data is zero length this will panic
|
||||
let approval: bool = !val.is_empty() && val[0] != 0;
|
||||
output.push((pubkey, approval));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Add authorized user (or change moderator flag)
|
||||
pub fn add_authorized_user(store: &Store, pubkey: Pubkey, moderator: bool) -> Result<(), Error> {
|
||||
let users = store
|
||||
.extra_table("users")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable("users")))?;
|
||||
let mut txn = store.write_txn()?;
|
||||
users.put(&mut txn, pubkey.as_slice(), &[moderator as u8])?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove authorized user
|
||||
pub fn rm_authorized_user(store: &Store, pubkey: Pubkey) -> Result<(), Error> {
|
||||
let users = store
|
||||
.extra_table("users")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable("users")))?;
|
||||
let mut txn = store.write_txn()?;
|
||||
users.delete(&mut txn, pubkey.as_slice())?;
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get authorized user
|
||||
pub fn get_authorized_user(store: &Store, pubkey: Pubkey) -> Result<Option<bool>, Error> {
|
||||
let users = store
|
||||
.extra_table("users")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable("users")))?;
|
||||
let txn = store.read_txn()?;
|
||||
Ok(users
|
||||
.get(&txn, pubkey.as_slice())?
|
||||
.map(|u| !u.is_empty() && u[0] != 0))
|
||||
}
|
||||
|
||||
/// Dump all authorized users
|
||||
pub fn dump_authorized_users(store: &Store) -> Result<Vec<(Pubkey, bool)>, Error> {
|
||||
let mut output: Vec<(Pubkey, bool)> = Vec::new();
|
||||
let users = store
|
||||
.extra_table("users")
|
||||
.ok_or(Into::<Error>::into(ChorusError::MissingTable("users")))?;
|
||||
let txn = store.read_txn()?;
|
||||
for i in users.iter(&txn)? {
|
||||
let (key, val) = i?;
|
||||
let pubkey = Pubkey::from_bytes(key.try_into().unwrap());
|
||||
let moderator: bool = !val.is_empty() && val[0] != 0;
|
||||
output.push((pubkey, moderator));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Is the pubkey an authorized user?
|
||||
pub fn is_authorized_user(pubkey: Pubkey) -> bool {
|
||||
let store = GLOBALS.store.get().unwrap();
|
||||
match get_authorized_user(&store, pubkey) {
|
||||
Err(_) => false,
|
||||
Ok(None) => false,
|
||||
Ok(Some(_)) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the pubkey a moderator?
|
||||
pub fn is_moderator(pubkey: Pubkey) -> bool {
|
||||
let store = GLOBALS.store.get().unwrap();
|
||||
match get_authorized_user(&store, pubkey) {
|
||||
Err(_) => false,
|
||||
Ok(None) => false,
|
||||
Ok(Some(moderator)) => moderator,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the pubkey an admin?
|
||||
pub fn is_admin(pubkey: Pubkey) -> bool {
|
||||
GLOBALS.config.read().admin_keys.contains(&pubkey)
|
||||
}
|
||||
|
||||
43
src/nostr.rs
43
src/nostr.rs
@ -124,7 +124,10 @@ impl WebSocketService {
|
||||
}
|
||||
|
||||
let user = self.user;
|
||||
let authorized_user = authorized_user(&user);
|
||||
let authorized_user = self
|
||||
.user
|
||||
.map(|pk| crate::is_authorized_user(pk))
|
||||
.unwrap_or(false);
|
||||
|
||||
if user.is_none() {
|
||||
for filter in filters.iter() {
|
||||
@ -292,7 +295,10 @@ impl WebSocketService {
|
||||
|
||||
async fn event_inner(&mut self) -> Result<(), Error> {
|
||||
let user = self.user;
|
||||
let authorized_user = authorized_user(&user);
|
||||
let authorized_user = self
|
||||
.user
|
||||
.map(|pk| crate::is_authorized_user(pk))
|
||||
.unwrap_or(false);
|
||||
|
||||
// Delineate the event back out of the session buffer
|
||||
let event = unsafe { Event::delineate(&self.buffer)? };
|
||||
@ -519,7 +525,10 @@ impl WebSocketService {
|
||||
}
|
||||
|
||||
let user = self.user;
|
||||
let authorized_user = authorized_user(&user);
|
||||
let authorized_user = self
|
||||
.user
|
||||
.map(|pk| crate::is_authorized_user(pk))
|
||||
.unwrap_or(false);
|
||||
|
||||
// Find all matching events
|
||||
let mut events: Vec<&Event> = Vec::new();
|
||||
@ -698,6 +707,12 @@ async fn screen_incoming_event(
|
||||
event_flags: EventFlags,
|
||||
authorized_user: bool,
|
||||
) -> Result<bool, Error> {
|
||||
// Accept anything from authenticated authorized users
|
||||
// We do this before checking moderation since authorized overrides moderation
|
||||
if authorized_user {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Reject if event approval is false
|
||||
if let Some(false) = crate::get_event_approval(GLOBALS.store.get().unwrap(), event.id())? {
|
||||
return Err(ChorusError::BannedEvent.into());
|
||||
@ -724,11 +739,6 @@ async fn screen_incoming_event(
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Accept anything from authenticated authorized users
|
||||
if authorized_user {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Accept relay lists from anybody
|
||||
if GLOBALS.config.read().serve_relay_lists
|
||||
&& (event.kind() == Kind::from(10002) || event.kind() == Kind::from(10050))
|
||||
@ -742,7 +752,7 @@ async fn screen_incoming_event(
|
||||
}
|
||||
|
||||
// If the author is one of our users, always accept it
|
||||
if GLOBALS.config.read().user_keys.contains(&event.pubkey()) {
|
||||
if crate::is_authorized_user(event.pubkey()) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@ -750,8 +760,8 @@ async fn screen_incoming_event(
|
||||
for mut tag in event.tags()?.iter() {
|
||||
if tag.next() == Some(b"p") {
|
||||
if let Some(value) = tag.next() {
|
||||
for ukhex in &GLOBALS.config.read().user_hex_keys {
|
||||
if value == ukhex.as_bytes() {
|
||||
if let Ok(pk) = Pubkey::read_hex(value) {
|
||||
if crate::is_authorized_user(pk) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
@ -821,13 +831,6 @@ pub fn screen_outgoing_event(
|
||||
false
|
||||
}
|
||||
|
||||
pub fn authorized_user(user: &Option<Pubkey>) -> bool {
|
||||
match user {
|
||||
None => false,
|
||||
Some(pk) => GLOBALS.config.read().user_keys.contains(pk),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventFlags {
|
||||
pub author_is_an_authorized_user: bool,
|
||||
pub author_is_current_user: bool,
|
||||
@ -836,7 +839,7 @@ pub struct EventFlags {
|
||||
}
|
||||
|
||||
pub fn event_flags(event: &Event, user: &Option<Pubkey>) -> EventFlags {
|
||||
let author_is_an_authorized_user = GLOBALS.config.read().user_keys.contains(&event.pubkey());
|
||||
let author_is_an_authorized_user = crate::is_authorized_user(event.pubkey());
|
||||
|
||||
let author_is_current_user = match user {
|
||||
None => false,
|
||||
@ -857,7 +860,7 @@ pub fn event_flags(event: &Event, user: &Option<Pubkey>) -> EventFlags {
|
||||
}
|
||||
}
|
||||
|
||||
if GLOBALS.config.read().user_keys.contains(&tagged_pk) {
|
||||
if crate::is_authorized_user(tagged_pk) {
|
||||
tags_an_authorized_user = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use crate::error::{ChorusError, Error};
|
||||
use crate::globals::GLOBALS;
|
||||
use base64::prelude::*;
|
||||
use http::header::AUTHORIZATION;
|
||||
use hyper::body::Incoming;
|
||||
@ -68,7 +67,7 @@ fn verify_auth_inner(request: &Request<Incoming>) -> Result<AuthData, Error> {
|
||||
}
|
||||
|
||||
// Nostr event must be signed by a chorus user
|
||||
if !GLOBALS.config.read().user_keys.contains(&event.pubkey()) {
|
||||
if !crate::is_authorized_user(event.pubkey()) {
|
||||
return s_err("You are not an authorized user");
|
||||
}
|
||||
|
||||
|
||||
@ -5,15 +5,15 @@ use http::header::AUTHORIZATION;
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::body::Incoming;
|
||||
use hyper::Request;
|
||||
use pocket_types::Event;
|
||||
use pocket_types::{Event, Pubkey};
|
||||
use secp256k1::hashes::{sha256, Hash};
|
||||
use serde_json::Value;
|
||||
|
||||
fn s_err(s: &str) -> Result<Value, Error> {
|
||||
fn s_err(s: &str) -> Result<(Pubkey, Value), Error> {
|
||||
Err(ChorusError::ManagementAuthFailure(s.to_owned()).into())
|
||||
}
|
||||
|
||||
pub async fn check_auth(request: Request<Incoming>) -> Result<Value, Error> {
|
||||
pub async fn check_auth(request: Request<Incoming>) -> Result<(Pubkey, Value), Error> {
|
||||
// Must be POST
|
||||
if request.method() != hyper::Method::POST {
|
||||
return s_err("Management RPC only supports POST method");
|
||||
@ -56,12 +56,7 @@ pub async fn check_auth(request: Request<Incoming>) -> Result<Value, Error> {
|
||||
}
|
||||
|
||||
// Nostr event must be signed by a moderator
|
||||
if !GLOBALS
|
||||
.config
|
||||
.read()
|
||||
.moderator_keys
|
||||
.contains(&event.pubkey())
|
||||
{
|
||||
if !crate::is_moderator(event.pubkey()) {
|
||||
return s_err("Authorization failed as user is not a moderator");
|
||||
}
|
||||
|
||||
@ -117,5 +112,5 @@ pub async fn check_auth(request: Request<Incoming>) -> Result<Value, Error> {
|
||||
return s_err("Authorization event payload missing");
|
||||
}
|
||||
|
||||
Ok(serde_json::from_slice(&body)?)
|
||||
Ok((event.pubkey(), serde_json::from_slice(&body)?))
|
||||
}
|
||||
|
||||
@ -32,8 +32,8 @@ pub async fn handle(
|
||||
_peer: HashedPeer,
|
||||
request: Request<Incoming>,
|
||||
) -> Result<Response<BoxBody<Bytes, Error>>, Error> {
|
||||
let command: Value = match auth::check_auth(request).await {
|
||||
Ok(v) => v,
|
||||
let (pubkey, command) = match auth::check_auth(request).await {
|
||||
Ok((pk, v)) => (pk, v),
|
||||
Err(e) => {
|
||||
let result = json!({
|
||||
"result": {},
|
||||
@ -43,7 +43,7 @@ pub async fn handle(
|
||||
}
|
||||
};
|
||||
|
||||
match handle_inner(command) {
|
||||
match handle_inner(pubkey, command) {
|
||||
Ok(Some(value)) => respond(value, StatusCode::OK),
|
||||
Ok(None) => {
|
||||
let result = json!({
|
||||
@ -80,7 +80,7 @@ pub async fn handle(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
pub fn handle_inner(pubkey: Pubkey, command: Value) -> Result<Option<Value>, Error> {
|
||||
let obj = match command.as_object() {
|
||||
Some(o) => o,
|
||||
None => return Err(ChorusError::BadRequest("Command was not a JSON object").into()),
|
||||
@ -107,11 +107,17 @@ pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
"listbannedpubkeys",
|
||||
"supportedmethods",
|
||||
"numconnections",
|
||||
"uptime"
|
||||
"uptime",
|
||||
"stats",
|
||||
"listadmins",
|
||||
"listmoderators",
|
||||
"grantmoderator",
|
||||
"revokemoderator",
|
||||
"listusers",
|
||||
"grantuser",
|
||||
"revokeuser",
|
||||
]
|
||||
}))),
|
||||
|
||||
// Pubkeys
|
||||
"banpubkey" => {
|
||||
let pk = get_pubkey_param(obj)?;
|
||||
crate::mark_pubkey_approval(GLOBALS.store.get().unwrap(), pk, false)?;
|
||||
@ -130,7 +136,7 @@ pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
if *appr {
|
||||
None
|
||||
} else {
|
||||
Some(pk.as_hex_string().unwrap())
|
||||
Some(pk.as_hex_string())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@ -144,7 +150,7 @@ pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
.iter()
|
||||
.filter_map(|(pk, appr)| {
|
||||
if *appr {
|
||||
Some(pk.as_hex_string().unwrap())
|
||||
Some(pk.as_hex_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -154,7 +160,6 @@ pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
"result": pubkeys
|
||||
})))
|
||||
}
|
||||
// Events
|
||||
"banevent" => {
|
||||
let id = get_id_param(obj)?;
|
||||
crate::mark_event_approval(GLOBALS.store.get().unwrap(), id, false)?;
|
||||
@ -173,7 +178,7 @@ pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
if *appr {
|
||||
None
|
||||
} else {
|
||||
Some(id.as_hex_string().unwrap())
|
||||
Some(id.as_hex_string())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@ -187,7 +192,7 @@ pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
.iter()
|
||||
.filter_map(|(id, appr)| {
|
||||
if *appr {
|
||||
Some(id.as_hex_string().unwrap())
|
||||
Some(id.as_hex_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -197,39 +202,131 @@ pub fn handle_inner(command: Value) -> Result<Option<Value>, Error> {
|
||||
"result": ids
|
||||
})))
|
||||
}
|
||||
|
||||
"listeventsneedingmoderation" => Err(ChorusError::NotImplemented.into()),
|
||||
|
||||
// Kinds
|
||||
"allowkind" => Err(ChorusError::NotImplemented.into()),
|
||||
"disallowkind" => Err(ChorusError::NotImplemented.into()),
|
||||
"listbannedkinds" => Err(ChorusError::NotImplemented.into()),
|
||||
"listallowedkinds" => Err(ChorusError::NotImplemented.into()),
|
||||
|
||||
// IP addresses
|
||||
"blockip" => Err(ChorusError::NotImplemented.into()),
|
||||
"unblockip" => Err(ChorusError::NotImplemented.into()),
|
||||
"listblockedips" => Err(ChorusError::NotImplemented.into()),
|
||||
|
||||
// Config
|
||||
"changerelayname" => Err(ChorusError::NotImplemented.into()),
|
||||
"changerelaydescription" => Err(ChorusError::NotImplemented.into()),
|
||||
"changerelayicon" => Err(ChorusError::NotImplemented.into()),
|
||||
|
||||
// System
|
||||
"numconnections" => {
|
||||
let num = &GLOBALS.num_connections;
|
||||
Ok(Some(json!({
|
||||
"result": num,
|
||||
})))
|
||||
}
|
||||
|
||||
"uptime" => {
|
||||
let uptime_in_secs = GLOBALS.start_time.elapsed().as_secs();
|
||||
Ok(Some(json!({
|
||||
"result": uptime_in_secs,
|
||||
})))
|
||||
}
|
||||
"stats" => {
|
||||
let store_stats = GLOBALS.store.get().unwrap().stats()?;
|
||||
Ok(Some(json!({
|
||||
"result": {
|
||||
"uptime": GLOBALS.start_time.elapsed().as_secs(),
|
||||
"num_connections": &GLOBALS.num_connections,
|
||||
"bytes_received": &GLOBALS.bytes_inbound,
|
||||
"bytes_sent": &GLOBALS.bytes_outbound,
|
||||
"event_bytes": store_stats.event_bytes,
|
||||
"num_events": store_stats.index_stats.i_index_entries,
|
||||
"index_disk_usage": store_stats.index_stats.disk_usage,
|
||||
"index_memory_usage": store_stats.index_stats.memory_usage,
|
||||
}
|
||||
})))
|
||||
}
|
||||
"listadmins" => {
|
||||
let keys = GLOBALS.config.read().admin_hex_keys.clone();
|
||||
Ok(Some(json!({
|
||||
"result": keys
|
||||
})))
|
||||
}
|
||||
"listmoderators" => {
|
||||
let moderators: Vec<String> =
|
||||
crate::dump_authorized_users(GLOBALS.store.get().unwrap())?
|
||||
.iter()
|
||||
.filter_map(|(pk, moderator)| {
|
||||
if *moderator {
|
||||
Some(pk.as_hex_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Some(json!({
|
||||
"result": moderators
|
||||
})))
|
||||
}
|
||||
"grantmoderator" => {
|
||||
if !crate::is_admin(pubkey) {
|
||||
Ok(Some(json!({
|
||||
"result": {},
|
||||
"error": "Unauthorized: Only admins can grant moderator status"
|
||||
})))
|
||||
} else {
|
||||
let pk = get_pubkey_param(obj)?;
|
||||
crate::add_authorized_user(GLOBALS.store.get().unwrap(), pk, true)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
"revokemoderator" => {
|
||||
if !crate::is_admin(pubkey) {
|
||||
Ok(Some(json!({
|
||||
"result": {},
|
||||
"error": "Unauthorized: Only admins can revoke moderator status"
|
||||
})))
|
||||
} else {
|
||||
let pk = get_pubkey_param(obj)?;
|
||||
|
||||
// Do not do this if they aren't already an authorized user
|
||||
if !crate::is_authorized_user(pk) {
|
||||
Ok(None)
|
||||
} else {
|
||||
crate::add_authorized_user(GLOBALS.store.get().unwrap(), pk, false)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
"listusers" => {
|
||||
let users: Vec<String> = crate::dump_authorized_users(GLOBALS.store.get().unwrap())?
|
||||
.iter()
|
||||
.map(|(pk, _moderator)| pk.as_hex_string())
|
||||
.collect();
|
||||
Ok(Some(json!({
|
||||
"result": users
|
||||
})))
|
||||
}
|
||||
"grantuser" => {
|
||||
if !crate::is_admin(pubkey) {
|
||||
Ok(Some(json!({
|
||||
"result": {},
|
||||
"error": "Unauthorized: Only admins can grant user status"
|
||||
})))
|
||||
} else {
|
||||
let pk = get_pubkey_param(obj)?;
|
||||
crate::add_authorized_user(GLOBALS.store.get().unwrap(), pk, false)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
"revokeuser" => {
|
||||
if !crate::is_admin(pubkey) {
|
||||
Ok(Some(json!({
|
||||
"result": {},
|
||||
"error": "Unauthorized: Only admins can revoke user status"
|
||||
})))
|
||||
} else {
|
||||
let pk = get_pubkey_param(obj)?;
|
||||
crate::rm_authorized_user(GLOBALS.store.get().unwrap(), pk)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// Commands we do not support (yet)
|
||||
"listeventsneedingmoderation" => Err(ChorusError::NotImplemented.into()),
|
||||
"allowkind" => Err(ChorusError::NotImplemented.into()),
|
||||
"disallowkind" => Err(ChorusError::NotImplemented.into()),
|
||||
"listbannedkinds" => Err(ChorusError::NotImplemented.into()),
|
||||
"listallowedkinds" => Err(ChorusError::NotImplemented.into()),
|
||||
"blockip" => Err(ChorusError::NotImplemented.into()),
|
||||
"unblockip" => Err(ChorusError::NotImplemented.into()),
|
||||
"listblockedips" => Err(ChorusError::NotImplemented.into()),
|
||||
"changerelayname" => Err(ChorusError::NotImplemented.into()),
|
||||
"changerelaydescription" => Err(ChorusError::NotImplemented.into()),
|
||||
"changerelayicon" => Err(ChorusError::NotImplemented.into()),
|
||||
|
||||
_ => Err(ChorusError::NotImplemented.into()),
|
||||
}
|
||||
|
||||
@ -12,10 +12,7 @@ name = "Chorus Sample"
|
||||
description = "A sample run of the Chorus relay"
|
||||
# icon_url =
|
||||
open_relay = false
|
||||
user_hex_keys = [
|
||||
"12bb541d03bfc3cab0f4a8e4db28947f60faae6fca4e315eb27f809c6eff9a0b"
|
||||
]
|
||||
moderator_hex_keys = [
|
||||
admin_hex_keys = [
|
||||
"12bb541d03bfc3cab0f4a8e4db28947f60faae6fca4e315eb27f809c6eff9a0b"
|
||||
]
|
||||
verify_events = true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user