use std::collections::BTreeSet;
use std::str;
use nostr::secp256k1::rand::rngs::OsRng;
use nostr::secp256k1::rand::Rng;
use nostr::{PublicKey, RelayUrl};
use openmls::extensions::{Extension, ExtensionType};
use openmls::group::{GroupContext, MlsGroup};
use tls_codec::{
DeserializeBytes, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSerializeBytes, TlsSize,
};
use crate::constant::NOSTR_GROUP_DATA_EXTENSION_TYPE;
use crate::error::Error;
#[derive(
Debug,
Clone,
PartialEq,
Eq,
TlsSerialize,
TlsDeserialize,
TlsDeserializeBytes,
TlsSerializeBytes,
TlsSize,
)]
pub(crate) struct RawNostrGroupDataExtension {
pub nostr_group_id: [u8; 32],
pub name: Vec<u8>,
pub description: Vec<u8>,
pub admin_pubkeys: Vec<Vec<u8>>,
pub relays: Vec<Vec<u8>>,
pub image_url: Vec<u8>,
pub image_key: Vec<u8>,
pub image_nonce: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NostrGroupDataExtension {
pub nostr_group_id: [u8; 32],
pub name: String,
pub description: String,
pub admins: BTreeSet<PublicKey>,
pub relays: BTreeSet<RelayUrl>,
pub image_url: Option<String>,
pub image_key: Option<Vec<u8>>,
pub image_nonce: Option<Vec<u8>>,
}
impl NostrGroupDataExtension {
pub const EXTENSION_TYPE: u16 = NOSTR_GROUP_DATA_EXTENSION_TYPE;
pub fn new<T1, T2, T3, IA, IR>(
name: T1,
description: T2,
admins: IA,
relays: IR,
image_url: Option<T3>,
image_key: Option<Vec<u8>>,
image_nonce: Option<Vec<u8>>,
) -> Self
where
T1: Into<String>,
T2: Into<String>,
T3: Into<String>,
IA: IntoIterator<Item = PublicKey>,
IR: IntoIterator<Item = RelayUrl>,
{
let mut rng = OsRng;
let random_bytes: [u8; 32] = rng.gen();
Self {
nostr_group_id: random_bytes,
name: name.into(),
description: description.into(),
admins: admins.into_iter().collect(),
relays: relays.into_iter().collect(),
image_url: image_url.map(Into::into),
image_key,
image_nonce,
}
}
pub(crate) fn from_raw(raw: RawNostrGroupDataExtension) -> Result<Self, Error> {
let mut admins = BTreeSet::new();
for admin in raw.admin_pubkeys {
let bytes = hex::decode(&admin)?;
let pk = PublicKey::from_slice(&bytes)?;
admins.insert(pk);
}
let mut relays = BTreeSet::new();
for relay in raw.relays {
let url: &str = str::from_utf8(&relay)?;
let url = RelayUrl::parse(url)?;
relays.insert(url);
}
let image_url = if raw.image_url.is_empty() {
None
} else {
Some(String::from_utf8(raw.image_url)?)
};
let image_key = if raw.image_key.is_empty() {
None
} else {
Some(raw.image_key)
};
let image_nonce = if raw.image_nonce.is_empty() {
None
} else {
Some(raw.image_nonce)
};
Ok(Self {
nostr_group_id: raw.nostr_group_id,
name: String::from_utf8(raw.name)?,
description: String::from_utf8(raw.description)?,
admins,
relays,
image_url,
image_key,
image_nonce,
})
}
pub fn from_group_context(group_context: &GroupContext) -> Result<Self, Error> {
let group_data_extension = match group_context.extensions().iter().find(|ext| {
ext.extension_type() == ExtensionType::Unknown(NOSTR_GROUP_DATA_EXTENSION_TYPE)
}) {
Some(Extension::Unknown(_, ext)) => ext,
Some(_) => return Err(Error::UnexpectedExtensionType),
None => return Err(Error::NostrGroupDataExtensionNotFound),
};
let (deserialized, _) =
RawNostrGroupDataExtension::tls_deserialize_bytes(&group_data_extension.0)?;
Self::from_raw(deserialized)
}
pub fn from_group(group: &MlsGroup) -> Result<Self, Error> {
let group_data_extension = match group.extensions().iter().find(|ext| {
ext.extension_type() == ExtensionType::Unknown(NOSTR_GROUP_DATA_EXTENSION_TYPE)
}) {
Some(Extension::Unknown(_, ext)) => ext,
Some(_) => return Err(Error::UnexpectedExtensionType),
None => return Err(Error::NostrGroupDataExtensionNotFound),
};
let (deserialized, _) =
RawNostrGroupDataExtension::tls_deserialize_bytes(&group_data_extension.0)?;
Self::from_raw(deserialized)
}
pub fn nostr_group_id(&self) -> String {
hex::encode(self.nostr_group_id)
}
#[inline]
pub fn extension_type(&self) -> u16 {
Self::EXTENSION_TYPE
}
pub fn set_nostr_group_id(&mut self, nostr_group_id: [u8; 32]) {
self.nostr_group_id = nostr_group_id;
}
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn set_name(&mut self, name: String) {
self.name = name;
}
pub fn description(&self) -> &str {
self.description.as_str()
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
pub fn add_admin(&mut self, public_key: PublicKey) {
self.admins.insert(public_key);
}
pub fn remove_admin(&mut self, public_key: &PublicKey) {
self.admins.remove(public_key);
}
pub fn add_relay(&mut self, relay: RelayUrl) {
self.relays.insert(relay);
}
pub fn remove_relay(&mut self, relay: &RelayUrl) {
self.relays.remove(relay);
}
pub fn image_url(&self) -> Option<&str> {
self.image_url.as_deref()
}
pub fn set_image_url(&mut self, image_url: Option<String>) {
self.image_url = image_url;
}
pub fn image_key(&self) -> Option<&Vec<u8>> {
self.image_key.as_ref()
}
pub fn image_nonce(&self) -> Option<&Vec<u8>> {
self.image_nonce.as_ref()
}
pub fn set_image_key(&mut self, image_key: Option<Vec<u8>>) {
self.image_key = image_key;
}
pub fn set_image_nonce(&mut self, image_nonce: Option<Vec<u8>>) {
self.image_nonce = image_nonce;
}
pub(crate) fn as_raw(&self) -> RawNostrGroupDataExtension {
RawNostrGroupDataExtension {
nostr_group_id: self.nostr_group_id,
name: self.name.as_bytes().to_vec(),
description: self.description.as_bytes().to_vec(),
admin_pubkeys: self
.admins
.iter()
.map(|pk| pk.to_hex().into_bytes())
.collect(),
relays: self
.relays
.iter()
.map(|url| url.to_string().into_bytes())
.collect(),
image_url: self
.image_url
.as_ref()
.map_or_else(Vec::new, |img| img.as_bytes().to_vec()),
image_key: self.image_key.clone().unwrap_or_default(),
image_nonce: self.image_nonce.clone().unwrap_or_default(),
}
}
}
#[cfg(test)]
mod tests {
use aes_gcm::aead::OsRng;
use aes_gcm::{Aes128Gcm, KeyInit};
use rand::RngCore;
use super::*;
pub fn generate_encryption_key() -> Vec<u8> {
Aes128Gcm::generate_key(OsRng).to_vec()
}
const ADMIN_1: &str = "npub1a6awmmklxfmspwdv52qq58sk5c07kghwc4v2eaudjx2ju079cdqs2452ys";
const ADMIN_2: &str = "npub1t5sdrgt7md8a8lf77ka02deta4vj35p3ktfskd5yz68pzmt9334qy6qks0";
const RELAY_1: &str = "wss://relay1.com";
const RELAY_2: &str = "wss://relay2.com";
fn create_test_extension() -> NostrGroupDataExtension {
let pk1 = PublicKey::parse(ADMIN_1).unwrap();
let pk2 = PublicKey::parse(ADMIN_2).unwrap();
let relay1 = RelayUrl::parse(RELAY_1).unwrap();
let relay2 = RelayUrl::parse(RELAY_2).unwrap();
let key = generate_encryption_key();
let image = "http://blossom_test:4443/fake_img.png";
let mut image_nonce = [0u8; 12];
::rand::rng().fill_bytes(&mut image_nonce);
NostrGroupDataExtension::new(
"Test Group",
"Test Description",
[pk1, pk2],
[relay1, relay2],
Some(image),
Some(key),
Some(image_nonce.to_vec()),
)
}
#[test]
fn test_new_and_getters() {
let extension = create_test_extension();
let pk1 = PublicKey::parse(ADMIN_1).unwrap();
let pk2 = PublicKey::parse(ADMIN_2).unwrap();
let relay1 = RelayUrl::parse(RELAY_1).unwrap();
let relay2 = RelayUrl::parse(RELAY_2).unwrap();
assert_eq!(extension.nostr_group_id.len(), 32);
assert_eq!(extension.name(), "Test Group");
assert_eq!(extension.description(), "Test Description");
assert!(extension.admins.contains(&pk1));
assert!(extension.admins.contains(&pk2));
assert!(extension.relays.contains(&relay1));
assert!(extension.relays.contains(&relay2));
}
#[test]
fn test_group_id_operations() {
let mut extension = create_test_extension();
let new_id = [42u8; 32];
extension.set_nostr_group_id(new_id);
assert_eq!(extension.nostr_group_id(), hex::encode(new_id));
}
#[test]
fn test_name_operations() {
let mut extension = create_test_extension();
extension.set_name("New Name".to_string());
assert_eq!(extension.name(), "New Name");
}
#[test]
fn test_description_operations() {
let mut extension = create_test_extension();
extension.set_description("New Description".to_string());
assert_eq!(extension.description(), "New Description");
}
#[test]
fn test_admin_pubkey_operations() {
let mut extension = create_test_extension();
let admin1 = PublicKey::parse(ADMIN_1).unwrap();
let admin2 = PublicKey::parse(ADMIN_2).unwrap();
let admin3 =
PublicKey::parse("npub13933f9shzt90uccjaf4p4f4arxlfcy3q6037xnx8a2kxaafrn5yqtzehs6")
.unwrap();
extension.add_admin(admin3);
assert_eq!(extension.admins.len(), 3);
assert!(extension.admins.contains(&admin1));
assert!(extension.admins.contains(&admin2));
assert!(extension.admins.contains(&admin3));
extension.remove_admin(&admin2);
assert_eq!(extension.admins.len(), 2);
assert!(extension.admins.contains(&admin1));
assert!(!extension.admins.contains(&admin2)); assert!(extension.admins.contains(&admin3));
}
#[test]
fn test_relay_operations() {
let mut extension = create_test_extension();
let relay1 = RelayUrl::parse(RELAY_1).unwrap();
let relay2 = RelayUrl::parse(RELAY_2).unwrap();
let relay3 = RelayUrl::parse("wss://relay3.com").unwrap();
extension.add_relay(relay3.clone());
assert_eq!(extension.relays.len(), 3);
assert!(extension.relays.contains(&relay1));
assert!(extension.relays.contains(&relay2));
assert!(extension.relays.contains(&relay3));
extension.remove_relay(&relay2);
assert_eq!(extension.relays.len(), 2);
assert!(extension.relays.contains(&relay1));
assert!(!extension.relays.contains(&relay2)); assert!(extension.relays.contains(&relay3));
}
#[test]
fn test_image_operations() {
let mut extension = create_test_extension();
let image_url = Some("https://example.com/image.png".to_string());
extension.set_image_url(image_url.clone());
assert_eq!(extension.image_url(), image_url.as_deref());
let image_key = generate_encryption_key();
extension.set_image_key(Some(image_key));
assert!(extension.image_key().is_some());
let image_nonce = vec![0u8; 12];
extension.set_image_nonce(Some(image_nonce));
assert!(extension.image_nonce().is_some());
extension.set_image_url(None);
extension.set_image_key(None);
extension.set_image_nonce(None);
assert!(extension.image_url().is_none());
assert!(extension.image_key().is_none());
assert!(extension.image_nonce().is_none());
}
#[test]
fn test_new_fields_in_serialization() {
let mut extension = create_test_extension();
let image_url = "https://example.com/test.png".to_string();
let image_key = generate_encryption_key();
let image_nonce = vec![7; 12];
extension.set_image_url(Some(image_url.clone()));
extension.set_image_key(Some(image_key.clone()));
extension.set_image_nonce(Some(image_nonce.clone()));
let raw = extension.as_raw();
let reconstructed = NostrGroupDataExtension::from_raw(raw).unwrap();
assert_eq!(reconstructed.image_url(), Some(image_url.as_str()));
assert_eq!(reconstructed.image_nonce(), Some(&image_nonce));
assert!(reconstructed.image_key().is_some());
assert_eq!(reconstructed.image_key().unwrap(), &image_key[..]);
}
}