Compare commits

..

10 Commits

Author SHA1 Message Date
Mike Dilger
da897b950e
management "stats" command 2025-02-11 10:52:22 +13:00
Mike Dilger
cbd3d0fa19
Add 7 new management commands:
listadmins,
  listmoderators, grantmoderator, revokemoderator
  listusers, grantuser, revokeuser
2025-02-11 10:32:44 +13:00
Mike Dilger
61d26c0d85
fix: don't let moderation ban authorized users 2025-02-11 10:01:20 +13:00
Mike Dilger
5941425799
Update docs and sample config files 2025-02-11 09:42:58 +13:00
Mike Dilger
ef4ec80fa0
config.admin_keys (makes no difference until moderation code can adjust users) 2025-02-11 09:31:42 +13:00
Mike Dilger
2bc75aa36b
chorus_cmd to manage users (as web management is scarcely available) 2025-02-11 09:22:38 +13:00
Mike Dilger
cb431b6b00
[BREAKING] Switch to database user pubkeys, remove config user pubkeys 2025-02-11 09:21:00 +13:00
Mike Dilger
f02c071474
Add users table (with functions, not used yet) 2025-02-11 09:12:28 +13:00
Mike Dilger
99124ba134
Update pocket-db/pocket-types 2025-02-11 08:51:09 +13:00
Mike Dilger
8ab043698f
v1.7.2 2025-02-11 08:49:59 +13:00
16 changed files with 319 additions and 131 deletions

6
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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 `[]`

View File

@ -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.

View File

@ -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

View File

@ -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());
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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;
}
}

View File

@ -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");
}

View File

@ -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)?))
}

View File

@ -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()),
}

View File

@ -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