use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;
pub use vector_core::{
self, Attachment, AttachmentFile, CoreConfig, DeleteOutcome, EditEntry, EventEmitter,
ImageMetadata, InboundEventHandler, LoginResult, Message, NoOpEventHandler, Reaction, Result,
SendResult, SerializableChat, SiteMetadata, SlimProfile, Status, SyncPriority, VectorCore,
VectorError,
};
pub use vector_core::VectorError as Error;
pub mod nostr {
pub use nostr_sdk::prelude::{FromBech32, Keys, PublicKey, SecretKey, ToBech32};
}
use nostr_sdk::prelude::{FromBech32 as _, ToBech32 as _};
#[derive(Clone, Debug)]
pub enum InvitePolicy {
Manual,
Public,
Whitelist(Vec<String>),
}
impl InvitePolicy {
fn accepts(&self, inviter_npub: Option<&str>) -> bool {
match self {
InvitePolicy::Manual => false,
InvitePolicy::Public => true,
InvitePolicy::Whitelist(list) => {
inviter_npub.is_some_and(|npub| list.iter().any(|w| w == npub))
}
}
}
}
#[derive(Clone)]
pub struct VectorBot {
core: VectorCore,
npub: String,
invite_policy: Arc<InvitePolicy>,
}
impl VectorBot {
pub fn builder() -> VectorBotBuilder {
VectorBotBuilder::default()
}
pub fn generate_nsec() -> Result<String> {
VectorCore.generate_nsec()
}
pub fn npub(&self) -> &str {
&self.npub
}
pub fn core(&self) -> VectorCore {
self.core
}
pub fn invite_policy(&self) -> &InvitePolicy {
&self.invite_policy
}
pub fn pending_invites(&self) -> Result<Vec<serde_json::Value>> {
self.core.list_pending_invites()
}
pub async fn accept_invite(&self, community_id: &str) -> Result<serde_json::Value> {
let res = self.core.accept_pending_invite(community_id).await?;
if let Some(client) = vector_core::state::nostr_client() {
vector_core::community::realtime::refresh_subscription(&client).await;
}
Ok(res)
}
pub async fn process_pending_invites(&self) {
if matches!(*self.invite_policy, InvitePolicy::Manual) {
return;
}
let Ok(invites) = self.core.list_pending_invites() else { return };
for inv in invites {
let Some(cid) = inv.get("community_id").and_then(|c| c.as_str()) else { continue };
let inviter = inv.get("inviter_npub").and_then(|n| n.as_str());
if self.invite_policy.accepts(inviter) {
let _ = self.accept_invite(cid).await;
}
}
}
pub async fn apply_invite_policy(&self, community_id: &str) {
if matches!(*self.invite_policy, InvitePolicy::Manual) {
return;
}
let inviter = self
.core
.list_pending_invites()
.ok()
.and_then(|invites| {
invites.into_iter().find_map(|i| {
(i.get("community_id").and_then(|c| c.as_str()) == Some(community_id))
.then(|| i.get("inviter_npub").and_then(|n| n.as_str()).map(String::from))
.flatten()
})
});
if self.invite_policy.accepts(inviter.as_deref()) {
let _ = self.accept_invite(community_id).await;
}
}
pub fn channel(&self, id: impl Into<String>) -> Channel {
let id = id.into();
let kind = channel_kind_for(&id);
Channel { core: self.core, id, kind }
}
pub fn dm(&self, npub: impl Into<String>) -> Channel {
Channel { core: self.core, id: npub.into(), kind: ChannelKind::Dm }
}
pub fn community(&self, community_id: impl Into<String>) -> Community {
Community { core: self.core, id: community_id.into() }
}
pub async fn communities(&self) -> Vec<Community> {
self.core
.list_communities()
.await
.into_iter()
.filter_map(|v| {
v.get("community_id")
.or_else(|| v.get("id"))
.and_then(|i| i.as_str())
.map(|id| self.community(id.to_string()))
})
.collect()
}
pub async fn on_message<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(VectorBot, IncomingMessage) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.prepare_listen().await;
let adapter = ClosureHandler {
bot: self.clone(),
handler: Arc::new(handler),
};
self.core.listen(Arc::new(adapter)).await
}
pub async fn on_event<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(VectorBot, BotEvent) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.prepare_listen().await;
let adapter = EventClosureHandler {
bot: self.clone(),
handler: Arc::new(handler),
};
self.core.listen(Arc::new(adapter)).await
}
async fn prepare_listen(&self) {
let _ = self.core.sync_dms(None, &NoOpEventHandler).await;
self.process_pending_invites().await;
}
pub async fn listen_with(&self, handler: Arc<dyn InboundEventHandler>) -> Result<()> {
self.core.listen(handler).await
}
pub async fn sync_dms(&self, since_days: Option<u64>) -> Result<(u32, u32)> {
self.core.sync_dms(since_days, &NoOpEventHandler).await
}
pub async fn sync_communities(&self) -> Result<()> {
self.core.sync_communities().await
}
pub async fn fetch_profile(&self, npub: &str) -> Option<SlimProfile> {
self.core.load_profile(npub).await;
self.core.get_profile(npub).await
}
pub async fn cached_profile(&self, npub: &str) -> Option<SlimProfile> {
self.core.get_profile(npub).await
}
pub async fn update_profile(&self, name: &str, avatar: &str, banner: &str, about: &str) -> bool {
self.core.update_bot_profile(name, avatar, banner, about).await
}
pub async fn set_status(&self, status: &str) -> bool {
self.core.update_status(status).await
}
pub async fn block(&self, npub: &str) -> bool {
self.core.block_user(npub).await
}
pub async fn unblock(&self, npub: &str) -> bool {
self.core.unblock_user(npub).await
}
pub async fn set_nickname(&self, npub: &str, nickname: &str) -> bool {
self.core.set_nickname(npub, nickname).await
}
pub async fn blocked_users(&self) -> Vec<SlimProfile> {
self.core.get_blocked_users().await
}
pub async fn download_attachment(&self, attachment: &Attachment) -> Result<Vec<u8>> {
self.core.download_attachment(attachment).await
}
pub async fn upload_image(&self, path: impl AsRef<std::path::Path>) -> Result<String> {
let path = path.as_ref().to_string_lossy().into_owned();
self.core.upload_public_image(&path).await
}
pub async fn save_attachment(&self, attachment: &Attachment, path: impl Into<PathBuf>) -> Result<PathBuf> {
let path = path.into();
let bytes = self.core.download_attachment(attachment).await?;
std::fs::write(&path, bytes).map_err(VectorError::Io)?;
Ok(path)
}
pub async fn logout(&self) {
self.core.logout().await
}
}
#[derive(Default)]
pub struct VectorBotBuilder {
key: Option<String>,
password: Option<String>,
data_dir: Option<PathBuf>,
event_emitter: Option<Box<dyn EventEmitter>>,
invite_policy: Option<InvitePolicy>,
#[cfg(feature = "tor")]
tor: bool,
#[cfg(feature = "tor")]
tor_bridges: Vec<String>,
}
impl VectorBotBuilder {
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn nsec(self, nsec: impl Into<String>) -> Self {
self.key(nsec)
}
pub fn mnemonic(self, phrase: impl Into<String>) -> Self {
self.key(phrase)
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn invite_policy(mut self, policy: InvitePolicy) -> Self {
self.invite_policy = Some(policy);
self
}
pub fn public(self) -> Self {
self.invite_policy(InvitePolicy::Public)
}
pub fn whitelist(self, npubs: impl IntoIterator<Item = impl Into<String>>) -> Self {
let normalized = npubs
.into_iter()
.filter_map(|n| {
let s = n.into();
nostr_sdk::PublicKey::parse(&s).ok().and_then(|pk| pk.to_bech32().ok())
})
.collect();
self.invite_policy(InvitePolicy::Whitelist(normalized))
}
pub fn data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.data_dir = Some(dir.into());
self
}
pub fn event_emitter(mut self, emitter: Box<dyn EventEmitter>) -> Self {
self.event_emitter = Some(emitter);
self
}
#[cfg(feature = "tor")]
pub fn tor(mut self) -> Self {
self.tor = true;
self
}
#[cfg(feature = "tor")]
pub fn tor_bridges(mut self, bridges: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.tor = true;
self.tor_bridges = bridges.into_iter().map(Into::into).collect();
self
}
pub async fn build(self) -> Result<VectorBot> {
let data_dir = self.data_dir.unwrap_or_else(default_data_dir);
std::fs::create_dir_all(&data_dir).ok();
let core = VectorCore::init(CoreConfig {
data_dir: data_dir.clone(),
event_emitter: self.event_emitter,
})?;
let (key, fresh_identity) = match self.key {
Some(key) => (key, None),
None => {
let (nsec, path, created) = load_or_create_identity(core, &data_dir)?;
(nsec, created.then_some(path))
}
};
#[cfg(feature = "tor")]
if self.tor {
use nostr_sdk::prelude::*;
let keys = if key.starts_with("nsec1") {
Keys::new(
SecretKey::from_bech32(&key)
.map_err(|e| VectorError::Other(format!("invalid nsec: {e}")))?,
)
} else {
Keys::from_mnemonic(&key, None)
.map_err(|e| VectorError::Other(format!("invalid mnemonic: {e}")))?
};
let npub = keys.public_key().to_bech32().map_err(|e| VectorError::Other(e.to_string()))?;
vector_core::db::set_current_account(npub.clone()).map_err(VectorError::Other)?;
vector_core::db::init_database(&npub).map_err(VectorError::Other)?;
vector_core::db::settings::set_sql_setting("tor_enabled".to_string(), "1".to_string())
.map_err(VectorError::Other)?;
vector_core::tor::set_tor_enabled_pref(true);
let tor_dir = vector_core::db::account_dir(&npub).map_err(VectorError::Other)?.join("tor");
let (state_dir, cache_dir) = (tor_dir.join("state"), tor_dir.join("cache"));
std::fs::create_dir_all(&state_dir).ok();
std::fs::create_dir_all(&cache_dir).ok();
vector_core::net::rebuild_shared_http_client().map_err(VectorError::Other)?;
vector_core::tor::TorService::start(state_dir, cache_dir, &self.tor_bridges)
.await
.map_err(VectorError::Other)?;
vector_core::net::rebuild_shared_http_client().map_err(VectorError::Other)?;
}
let result = core.login(&key, self.password.as_deref()).await?;
if let Some(path) = fresh_identity {
eprintln!(
"[vector-sdk] Created a new bot identity {} (stored at {}). \
Back it up — that file is the bot.",
result.npub,
path.display()
);
}
Ok(VectorBot {
core,
npub: result.npub,
invite_policy: Arc::new(self.invite_policy.unwrap_or(InvitePolicy::Manual)),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChannelKind {
Dm,
Community,
}
#[derive(Clone)]
pub struct Channel {
core: VectorCore,
id: String,
kind: ChannelKind,
}
impl Channel {
pub fn id(&self) -> &str {
&self.id
}
pub fn kind(&self) -> ChannelKind {
self.kind
}
pub fn is_dm(&self) -> bool {
matches!(self.kind, ChannelKind::Dm)
}
pub fn is_community(&self) -> bool {
matches!(self.kind, ChannelKind::Community)
}
pub async fn send(&self, text: &str) -> Result<String> {
match self.kind {
ChannelKind::Dm => self
.core
.send_dm(&self.id, text)
.await
.map(|r| r.event_id.unwrap_or(r.pending_id)),
ChannelKind::Community => self.core.send_community_message(&self.id, text, None).await,
}
}
pub async fn reply(&self, replied_to: &str, text: &str) -> Result<String> {
match self.kind {
ChannelKind::Dm => self
.core
.send_dm_reply(&self.id, replied_to, text)
.await
.map(|r| r.event_id.unwrap_or(r.pending_id)),
ChannelKind::Community => {
self.core.send_community_message(&self.id, text, Some(replied_to)).await
}
}
}
pub async fn react(&self, message_id: &str, emoji: &str) -> Result<()> {
match self.kind {
ChannelKind::Dm => self.core.send_reaction(&self.id, message_id, emoji, None).await.map(|_| ()),
ChannelKind::Community => self.core.send_community_reaction(&self.id, message_id, emoji, None).await,
}
}
pub async fn react_custom(&self, message_id: &str, shortcode_emoji: &str, image_url: &str) -> Result<()> {
match self.kind {
ChannelKind::Dm => self.core.send_reaction(&self.id, message_id, shortcode_emoji, Some(image_url)).await.map(|_| ()),
ChannelKind::Community => self.core.send_community_reaction(&self.id, message_id, shortcode_emoji, Some(image_url)).await,
}
}
pub async fn typing(&self) -> Result<()> {
match self.kind {
ChannelKind::Dm => self.core.send_typing(&self.id).await,
ChannelKind::Community => self.core.send_community_typing(&self.id).await,
}
}
pub async fn edit(&self, message_id: &str, new_content: &str) -> Result<()> {
match self.kind {
ChannelKind::Dm => self.core.edit_dm(&self.id, message_id, new_content).await.map(|_| ()),
ChannelKind::Community => self.core.edit_community_message(&self.id, message_id, new_content).await,
}
}
pub async fn delete(&self, message_id: &str) -> Result<()> {
match self.kind {
ChannelKind::Dm => self.core.delete_dm(message_id).await.map(|_| ()),
ChannelKind::Community => self.core.delete_community_message(message_id).await,
}
}
pub async fn send_file(&self, path: impl AsRef<std::path::Path>) -> Result<String> {
let path = path.as_ref().to_string_lossy().into_owned();
match self.kind {
ChannelKind::Dm => self
.core
.send_file(&self.id, &path)
.await
.map(|r| r.event_id.unwrap_or(r.pending_id)),
ChannelKind::Community => self.core.send_community_file(&self.id, &path).await,
}
}
}
#[derive(Clone, Debug)]
pub struct IncomingMessage {
pub chat_id: String,
pub is_group: bool,
pub is_file: bool,
pub message: Message,
}
impl IncomingMessage {
pub fn channel(&self) -> Channel {
Channel {
core: VectorCore,
id: self.chat_id.clone(),
kind: if self.is_group { ChannelKind::Community } else { ChannelKind::Dm },
}
}
pub async fn reply(&self, text: &str) -> Result<String> {
self.channel().reply(&self.message.id, text).await
}
pub async fn react(&self, emoji: &str) -> Result<()> {
self.channel().react(&self.message.id, emoji).await
}
pub fn community(&self) -> Option<Community> {
if !self.is_group {
return None;
}
let community_id = vector_core::db::community::community_id_for_channel(&self.chat_id)
.ok()
.flatten()?;
Some(Community { core: VectorCore, id: community_id })
}
pub fn member(&self) -> Option<Member> {
let community = self.community()?;
let npub = self.message.npub.clone()?;
Some(Member { core: VectorCore, community_id: community.id, npub })
}
pub fn text(&self) -> &str {
&self.message.content
}
pub fn is_mine(&self) -> bool {
self.message.mine
}
}
#[derive(Clone)]
pub struct Community {
core: VectorCore,
id: String,
}
impl Community {
pub fn id(&self) -> &str {
&self.id
}
pub fn member(&self, npub: impl Into<String>) -> Member {
Member { core: self.core, community_id: self.id.clone(), npub: npub.into() }
}
pub async fn members(&self) -> Vec<Member> {
self.core
.get_community_members(&self.id)
.await
.into_iter()
.filter_map(|v| v.get("npub").and_then(|n| n.as_str()).map(|n| self.member(n.to_string())))
.collect()
}
pub async fn invite(&self, npub: &str) -> Result<()> {
self.core.invite_to_community(&self.id, npub).await.map(|_| ())
}
pub async fn create_invite(&self) -> Result<String> {
self.core.create_public_invite(&self.id).await
}
pub async fn edit(&self, name: Option<&str>, description: Option<&str>) -> Result<()> {
self.core.edit_community_metadata(&self.id, name, description).await
}
pub async fn leave(&self) -> Result<()> {
self.core.leave_community(&self.id).await
}
pub async fn dissolve(&self) -> Result<()> {
self.core.dissolve_community(&self.id).await
}
pub fn capabilities(&self) -> Result<serde_json::Value> {
self.core.community_capabilities(&self.id)
}
pub fn roles(&self) -> Result<serde_json::Value> {
self.core.community_roles(&self.id)
}
}
#[derive(Clone)]
pub struct Member {
core: VectorCore,
community_id: String,
npub: String,
}
impl Member {
pub fn npub(&self) -> &str {
&self.npub
}
pub fn community_id(&self) -> &str {
&self.community_id
}
pub async fn kick(&self) -> Result<()> {
self.core.kick_member(&self.community_id, &self.npub).await
}
pub async fn ban(&self) -> Result<()> {
self.core.set_member_banned(&self.community_id, &self.npub, true).await
}
pub async fn unban(&self) -> Result<()> {
self.core.set_member_banned(&self.community_id, &self.npub, false).await
}
pub async fn grant_admin(&self) -> Result<()> {
self.core.grant_admin(&self.community_id, &self.npub).await
}
pub async fn revoke_admin(&self) -> Result<()> {
self.core.revoke_admin(&self.community_id, &self.npub).await
}
pub async fn profile(&self) -> Option<SlimProfile> {
self.core.load_profile(&self.npub).await;
self.core.get_profile(&self.npub).await
}
pub fn is_owner(&self) -> bool {
self.core
.community_roles(&self.community_id)
.ok()
.and_then(|r| r.get("owner").and_then(|o| o.as_str()).map(|o| o == self.npub))
.unwrap_or(false)
}
pub fn is_admin(&self) -> bool {
let Ok(roles) = self.core.community_roles(&self.community_id) else { return false };
let owner = roles.get("owner").and_then(|o| o.as_str()) == Some(self.npub.as_str());
let admin = roles
.get("admins")
.and_then(|a| a.as_array())
.map(|arr| arr.iter().any(|n| n.as_str() == Some(self.npub.as_str())))
.unwrap_or(false);
owner || admin
}
}
struct ClosureHandler<F> {
bot: VectorBot,
handler: Arc<F>,
}
impl<F, Fut> ClosureHandler<F>
where
F: Fn(VectorBot, IncomingMessage) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
fn dispatch(&self, chat_id: &str, msg: &Message, is_file: bool, is_group: bool) {
let handler = self.handler.clone();
let bot = self.bot.clone();
let incoming = IncomingMessage {
chat_id: chat_id.to_string(),
is_group,
is_file,
message: msg.clone(),
};
tokio::spawn(async move {
handler(bot, incoming).await;
});
}
}
impl<F, Fut> InboundEventHandler for ClosureHandler<F>
where
F: Fn(VectorBot, IncomingMessage) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
fn on_dm_received(&self, chat_id: &str, msg: &Message, _is_new: bool) {
self.dispatch(chat_id, msg, false, false);
}
fn on_file_received(&self, chat_id: &str, msg: &Message, _is_new: bool) {
self.dispatch(chat_id, msg, true, false);
}
fn on_community_message(&self, chat_id: &str, msg: &Message, _is_new: bool) {
self.dispatch(chat_id, msg, !msg.attachments.is_empty(), true);
}
fn on_community_invite(&self, community_id: &str) {
let bot = self.bot.clone();
let community_id = community_id.to_string();
tokio::spawn(async move {
bot.apply_invite_policy(&community_id).await;
});
}
}
#[derive(Clone, Debug)]
pub enum BotEvent {
Message(IncomingMessage),
MessageUpdate { chat_id: String, message: Message },
Delete { chat_id: String, message_id: String },
MemberJoin { channel_id: String, npub: String },
MemberLeave { channel_id: String, npub: String },
Typing { chat_id: String, npub: String, until: u64 },
Invite { community_id: String },
Removed { community_id: String },
}
struct EventClosureHandler<F> {
bot: VectorBot,
handler: Arc<F>,
}
impl<F, Fut> EventClosureHandler<F>
where
F: Fn(VectorBot, BotEvent) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
fn emit(&self, event: BotEvent) {
let handler = self.handler.clone();
let bot = self.bot.clone();
tokio::spawn(async move {
handler(bot, event).await;
});
}
fn message(&self, chat_id: &str, msg: &Message, is_group: bool, is_file: bool) {
self.emit(BotEvent::Message(IncomingMessage {
chat_id: chat_id.to_string(),
is_group,
is_file,
message: msg.clone(),
}));
}
}
impl<F, Fut> InboundEventHandler for EventClosureHandler<F>
where
F: Fn(VectorBot, BotEvent) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
fn on_dm_received(&self, chat_id: &str, msg: &Message, _is_new: bool) {
self.message(chat_id, msg, false, false);
}
fn on_file_received(&self, chat_id: &str, msg: &Message, _is_new: bool) {
self.message(chat_id, msg, false, true);
}
fn on_community_message(&self, chat_id: &str, msg: &Message, _is_new: bool) {
self.message(chat_id, msg, true, !msg.attachments.is_empty());
}
fn on_reaction_received(&self, chat_id: &str, msg: &Message) {
self.emit(BotEvent::MessageUpdate { chat_id: chat_id.to_string(), message: msg.clone() });
}
fn on_community_update(&self, chat_id: &str, _target_id: &str, msg: &Message) {
self.emit(BotEvent::MessageUpdate { chat_id: chat_id.to_string(), message: msg.clone() });
}
fn on_message_deleted(&self, chat_id: &str, message_id: &str) {
self.emit(BotEvent::Delete { chat_id: chat_id.to_string(), message_id: message_id.to_string() });
}
fn on_community_removed(&self, chat_id: &str, target_id: &str) {
self.emit(BotEvent::Delete { chat_id: chat_id.to_string(), message_id: target_id.to_string() });
}
fn on_community_presence(
&self,
chat_id: &str,
npub: &str,
joined: bool,
_event_id: &str,
_created_at: u64,
_invited_by: Option<&str>,
_invited_label: Option<&str>,
) {
let (channel_id, npub) = (chat_id.to_string(), npub.to_string());
self.emit(if joined {
BotEvent::MemberJoin { channel_id, npub }
} else {
BotEvent::MemberLeave { channel_id, npub }
});
}
fn on_community_typing(&self, chat_id: &str, npub: &str, until: u64) {
self.emit(BotEvent::Typing { chat_id: chat_id.to_string(), npub: npub.to_string(), until });
}
fn on_community_self_removed(&self, community_id: &str) {
self.emit(BotEvent::Removed { community_id: community_id.to_string() });
}
fn on_community_invite(&self, community_id: &str) {
let bot = self.bot.clone();
let cid = community_id.to_string();
tokio::spawn(async move {
bot.apply_invite_policy(&cid).await;
});
self.emit(BotEvent::Invite { community_id: community_id.to_string() });
}
}
fn channel_kind_for(id: &str) -> ChannelKind {
if nostr_sdk::PublicKey::from_bech32(id).is_ok() {
ChannelKind::Dm
} else {
ChannelKind::Community
}
}
fn load_or_create_identity(core: VectorCore, data_dir: &std::path::Path) -> Result<(String, PathBuf, bool)> {
let path = data_dir.join("identity.nsec");
if let Ok(contents) = std::fs::read_to_string(&path) {
let nsec = contents.trim();
if !nsec.is_empty() {
return Ok((nsec.to_string(), path, false));
}
}
let nsec = core.generate_nsec()?;
std::fs::write(&path, &nsec).map_err(VectorError::Io)?;
restrict_to_owner(&path);
Ok((nsec, path, true))
}
#[cfg(unix)]
fn restrict_to_owner(path: &std::path::Path) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
#[cfg(not(unix))]
fn restrict_to_owner(_path: &std::path::Path) {}
fn default_data_dir() -> PathBuf {
#[cfg(target_os = "macos")]
{
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join("Library/Application Support/io.vectorapp/sdk");
}
}
#[cfg(target_os = "linux")]
{
if let Ok(data) = std::env::var("XDG_DATA_HOME") {
return PathBuf::from(data).join("io.vectorapp/sdk");
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".local/share/io.vectorapp/sdk");
}
}
#[cfg(target_os = "windows")]
{
if let Ok(appdata) = std::env::var("APPDATA") {
return PathBuf::from(appdata).join("io.vectorapp/sdk");
}
}
PathBuf::from("vector-sdk-data")
}
#[cfg(test)]
mod tests {
use super::*;
use nostr_sdk::Keys;
#[test]
fn invite_policy_matrix() {
let a = Keys::generate().public_key().to_bech32().unwrap();
let b = Keys::generate().public_key().to_bech32().unwrap();
assert!(!InvitePolicy::Manual.accepts(Some(&a)));
assert!(!InvitePolicy::Manual.accepts(None));
assert!(InvitePolicy::Public.accepts(Some(&a)));
assert!(InvitePolicy::Public.accepts(None));
let wl = InvitePolicy::Whitelist(vec![a.clone()]);
assert!(wl.accepts(Some(&a)), "whitelisted inviter must be accepted");
assert!(!wl.accepts(Some(&b)), "non-whitelisted inviter must be rejected");
assert!(!wl.accepts(None), "missing inviter must be rejected under whitelist");
}
#[test]
fn channel_kind_auto_detection() {
let npub = Keys::generate().public_key().to_bech32().unwrap();
assert_eq!(channel_kind_for(&npub), ChannelKind::Dm);
assert_eq!(channel_kind_for(&"a".repeat(64)), ChannelKind::Community);
assert_eq!(channel_kind_for(&Keys::generate().public_key().to_hex()), ChannelKind::Community);
}
}