/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use std::{
io::Cursor,
sync::{Arc, Mutex as SyncMutex, OnceLock, Weak},
time::UNIX_EPOCH,
};
use async_trait::async_trait;
use darkfi::{
event_graph::{
self,
proto::{EventPut, ProtocolEventGraph},
EventGraph, EventGraphPtr,
},
net::{
session::SESSION_DEFAULT,
settings::{MagicBytes, NetworkProfile, Settings as NetSettings},
ChannelPtr, P2p, P2pPtr,
},
system::{sleep, Subscription},
Result as DarkFiResult,
};
use darkfi_serial::{
deserialize_async, serialize, serialize_async, AsyncEncodable, Decodable, Encodable,
SerialDecodable, SerialEncodable,
};
use sled_overlay::sled;
use crate::{
error::{Error, Result},
prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyStr, Role},
scene::{MethodCallSub, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak, Slot},
ui::{
chatview::{MessageId, Timestamp},
OnModify,
},
ExecutorPtr,
};
use super::PluginSettings;
const P2P_RETRY_TIME: u64 = 20;
const COOLOFF_SLEEP_TIME: u64 = 20;
const COOLOFF_SYNC_ATTEMPTS: usize = 6;
const SYNC_MIN_PEERS: usize = 2;
const P2P_OUTBOUND_ACTIVE: usize = 6;
const P2P_OUTBOUND_SLEEP: usize = 1;
/// Due to drift between different machine's clocks, if the message timestamp is recent
/// then we will just correct it to the current time so messages appear sequential in the UI.
const RECENT_TIME_DIST: u64 = 25_000;
#[cfg(target_os = "android")]
mod paths {
use crate::android::{get_appdata_path, get_external_storage_path};
use std::path::PathBuf;
pub fn get_evgrdb_path() -> PathBuf {
get_external_storage_path().join("evgr")
}
pub fn get_use_tor_filename() -> PathBuf {
get_external_storage_path().join("use_tor.txt")
}
pub fn nick_filename() -> PathBuf {
get_appdata_path().join("/nick.txt")
}
pub fn p2p_datastore_path() -> PathBuf {
get_appdata_path().join("darkirc_p2p")
}
pub fn hostlist_path() -> PathBuf {
get_appdata_path().join("hostlist.tsv")
}
}
#[cfg(not(target_os = "android"))]
mod paths {
use std::path::PathBuf;
pub fn get_evgrdb_path() -> PathBuf {
dirs::data_local_dir().unwrap().join("darkfi/app/evgr")
}
pub fn get_use_tor_filename() -> PathBuf {
dirs::data_local_dir().unwrap().join("darkfi/app/use_tor.txt")
}
pub fn nick_filename() -> PathBuf {
dirs::cache_dir().unwrap().join("darkfi/app/nick.txt")
}
pub fn p2p_datastore_path() -> PathBuf {
dirs::cache_dir().unwrap().join("darkfi/app/darkirc_p2p")
}
pub fn hostlist_path() -> PathBuf {
dirs::cache_dir().unwrap().join("darkfi/app/hostlist.tsv")
}
}
use paths::*;
macro_rules! t { ($($arg:tt)*) => { trace!(target: "plugin::darkirc", $($arg)*); } }
macro_rules! d { ($($arg:tt)*) => { debug!(target: "plugin::darkirc", $($arg)*); } }
macro_rules! i { ($($arg:tt)*) => { info!(target: "plugin::darkirc", $($arg)*); } }
macro_rules! e { ($($arg:tt)*) => { error!(target: "plugin::darkirc", $($arg)*); } }
macro_rules! w { ($($arg:tt)*) => { warn!(target: "plugin::darkirc", $($arg)*); } }
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
pub struct Privmsg {
pub channel: String,
pub nick: String,
pub msg: String,
}
impl Privmsg {
pub fn new(channel: String, nick: String, msg: String) -> Self {
Self { channel, nick, msg }
}
pub fn msg_id(&self, timest: u64) -> MessageId {
let mut hasher = blake3::Hasher::new();
0u8.encode(&mut hasher).unwrap();
0u8.encode(&mut hasher).unwrap();
timest.encode(&mut hasher).unwrap();
self.channel.encode(&mut hasher).unwrap();
self.nick.encode(&mut hasher).unwrap();
self.msg.encode(&mut hasher).unwrap();
MessageId(hasher.finalize().into())
}
}
struct SeenMsg {
id: MessageId,
is_self: bool,
seen_times: usize,
}
struct SeenMessages {
seen: Vec,
}
impl SeenMessages {
fn new() -> Self {
Self { seen: vec![] }
}
fn get_status(&self, id: &MessageId) -> Option<&SeenMsg> {
self.seen.iter().find(|s| s.id == *id)
}
fn push(&mut self, id: MessageId, is_self: bool) {
self.seen.push(SeenMsg { id, is_self, seen_times: 0 });
}
}
pub type DarkIrcPtr = Arc;
pub struct DarkIrc {
node: SceneNodeWeak,
tasks: OnceLock>>,
p2p: P2pPtr,
event_graph: EventGraphPtr,
seen_msgs: SyncMutex,
nick: PropertyStr,
settings: PluginSettings,
}
impl DarkIrc {
pub async fn new(node: SceneNodeWeak, sg_root: SceneNodePtr, ex: ExecutorPtr) -> Result {
let node_ref = &node.upgrade().unwrap();
let nick = PropertyStr::wrap(node_ref, Role::Internal, "nick", 0).unwrap();
let setting_root = Arc::new(SceneNode::new("setting", SceneNodeType::SettingRoot));
node_ref.link(setting_root.clone());
i!("Starting DarkIRC backend");
let evgr_path = get_evgrdb_path();
let db = match sled::open(&evgr_path) {
Ok(db) => db,
Err(err) => {
e!("Sled database '{}' failed to open: {err}!", evgr_path.display());
return Err(Error::SledDbErr)
}
};
let setting_tree = db.open_tree("settings")?;
let settings = PluginSettings { setting_root, sled_tree: setting_tree };
let mut p2p_settings: NetSettings = Default::default();
p2p_settings.magic_bytes = MagicBytes([251, 229, 199, 181]);
p2p_settings.app_version = semver::Version::parse("0.5.0").unwrap();
p2p_settings.app_name = "darkirc".to_string();
if get_use_tor_filename().exists() {
i!("Setup P2P network [tor]");
let mut tor_profile = NetworkProfile::tor_default();
tor_profile.outbound_connect_timeout = 60;
p2p_settings.profiles.insert("tor".to_string(), tor_profile);
p2p_settings.outbound_peer_discovery_cooloff_time = 60;
p2p_settings.seeds.push(
url::Url::parse(
"tor://g7fxelebievvpr27w7gt24lflptpw3jeeuvafovgliq5utdst6xyruyd.onion:25552",
)
.unwrap(),
);
p2p_settings.seeds.push(
url::Url::parse(
"tor://yvklzjnfmwxhyodhrkpomawjcdvcaushsj6torjz2gyd7e25f3gfunyd.onion:25552",
)
.unwrap(),
);
p2p_settings.active_profiles = vec!["tor".to_string()];
} else {
i!("Setup P2P network [clearnet]");
let mut profile = NetworkProfile::default();
profile.outbound_connect_timeout = 40;
profile.channel_handshake_timeout = 30;
p2p_settings.profiles.insert("tcp+tls".to_string(), profile);
p2p_settings.outbound_connections = 5;
p2p_settings.inbound_connections = 2;
p2p_settings.seeds.push(url::Url::parse("tcp+tls://lilith0.dark.fi:25551").unwrap());
p2p_settings.seeds.push(url::Url::parse("tcp+tls://lilith1.dark.fi:25551").unwrap());
p2p_settings.active_profiles = vec!["tcp+tls".to_string()];
}
p2p_settings.p2p_datastore = p2p_datastore_path().into_os_string().into_string().ok();
p2p_settings.hostlist = hostlist_path().into_os_string().into_string().ok();
settings.add_p2p_settings(&p2p_settings);
settings.load_settings();
settings.update_p2p_settings(&mut p2p_settings);
let p2p = match P2p::new(p2p_settings.clone(), ex.clone()).await {
Ok(p2p) => p2p,
Err(err) => {
e!("Create p2p network failed: {err}!");
return Err(Error::ServiceFailed)
}
};
let event_graph = match EventGraph::new(
p2p.clone(),
db.clone(),
std::path::PathBuf::new(),
false,
"darkirc_dag",
1,
ex.clone(),
)
.await
{
Ok(evgr) => evgr,
Err(err) => {
e!("Create event graph failed: {err}!");
return Err(Error::ServiceFailed)
}
};
if let Ok(prev_nick) = std::fs::read_to_string(nick_filename()) {
nick.set(&mut PropertyAtomicGuard::none(), prev_nick);
}
let self_ = Arc::new(Self {
node: node.clone(),
tasks: OnceLock::new(),
p2p,
event_graph,
seen_msgs: SyncMutex::new(SeenMessages::new()),
nick,
settings,
});
self_.clone().start(sg_root, ex).await;
Ok(Pimpl::DarkIrc(self_))
}
async fn dag_sync(self: Arc, channel_sub: Subscription>) {
i!("Starting p2p network");
while let Err(err) = self.p2p.clone().start().await {
// This usually means we cannot listen on the inbound ports
e!("Failed to start p2p network: {err}!");
e!("Usually this means there is another process listening on the same ports.");
e!("Trying again in {P2P_RETRY_TIME} secs");
sleep(P2P_RETRY_TIME).await;
}
i!("Waiting for some P2P connections...");
let mut sync_attempt = 0;
loop {
// Wait for a channel
if let Err(err) = channel_sub.receive().await {
w!("There was an error listening for channels. The service closed unexpectedly with error: {err}");
continue
}
let peers_count = self.p2p.peers_count();
self.notify_connect(peers_count, false).await;
// Wait until we have enough connections
if peers_count < SYNC_MIN_PEERS {
i!("Connected to {peers_count} peers. Waiting for more connections.");
continue
}
sync_attempt += 1;
// Cool off periodically
if sync_attempt > COOLOFF_SYNC_ATTEMPTS {
i!("Wasn't able to sync yet. Cooling off for {COOLOFF_SLEEP_TIME} then will try again.");
sleep(COOLOFF_SLEEP_TIME).await;
sync_attempt = 0;
}
i!("Syncing event DAG (attempt #{sync_attempt})");
match self.event_graph.dag_sync().await {
Ok(()) => break,
Err(e) => {
// TODO: Maybe at this point we should prune or something?
// TODO: Or maybe just tell the user to delete the DAG from FS.
w!("Failed DAG sync: ({e}). Waiting for more connections before retry.");
}
}
}
let peers_count = self.p2p.peers_count();
self.notify_connect(peers_count, true).await;
// Initial sync finished. Now just notify of connection changes
loop {
// Wait for a channel
if let Err(err) = channel_sub.receive().await {
w!("There was an error listening for channels. The service closed unexpectedly with error: {err}");
continue
}
let peers_count = self.p2p.peers_count();
self.notify_connect(peers_count, true).await;
}
}
async fn notify_connect(&self, peers_count: usize, is_dag_synced: bool) {
let node = self.node.upgrade().unwrap();
node.trigger("connect", serialize(&(peers_count as u32, is_dag_synced))).await.unwrap();
}
async fn relay_events(self: Arc, ev_sub: Subscription) {
loop {
let ev = ev_sub.receive().await;
// Try to deserialize the `Event`'s content into a `Privmsg`
let privmsg: Privmsg = match deserialize_async(ev.content()).await {
Ok(v) => v,
Err(e) => {
e!("[IRC CLIENT] Failed deserializing incoming Privmsg event: {}", e);
continue
}
};
let mut timest = ev.timestamp;
let msg_id = privmsg.msg_id(timest);
t!(
"Relaying ev_id={:?}, ev={ev:?}, msg_id={msg_id}, privmsg={privmsg:?}, timest={timest}",
ev.id(),
);
let is_self = {
let mut is_self = false;
let mut seen = self.seen_msgs.lock().unwrap();
match seen.get_status(&msg_id) {
Some(msg) => {
is_self = msg.is_self;
if !msg.is_self || msg.seen_times > 1 {
warn!(target: "plugin::darkirc", "Skipping duplicate seen message: {msg_id}");
continue
}
}
None => {
seen.push(msg_id.clone(), false);
}
}
is_self
};
// This is a hack to make messages appear sequentially in the UI
let now_timest = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
if !is_self && timest.abs_diff(now_timest) < RECENT_TIME_DIST {
d!("Applied timestamp correction: <{timest}> => <{now_timest}>");
timest = now_timest;
}
// Strip off starting #
let mut channel = privmsg.channel;
if channel.is_empty() {
warn!(target: "plugin::darkirc", "Received privmsg with empty channel!");
continue
}
if channel.chars().next().unwrap() != '#' {
warn!(target: "plugin::darkirc", "Skipping encrypted channel: {channel}");
continue
}
channel.remove(0);
// Workaround for the chatview hack. This nick is off limits!
let mut nick = privmsg.nick;
if nick == "NOTICE" {
nick = "noticer".to_string();
}
let mut arg_data = vec![];
channel.encode(&mut arg_data).unwrap();
timest.encode(&mut arg_data).unwrap();
msg_id.encode(&mut arg_data).unwrap();
nick.encode(&mut arg_data).unwrap();
privmsg.msg.encode(&mut arg_data).unwrap();
let node = self.node.upgrade().unwrap();
node.trigger("recv", arg_data).await.unwrap();
}
}
async fn process_send(me: &Weak, sub: &MethodCallSub) -> bool {
let Ok(method_call) = sub.receive().await else {
d!("Event relayer closed");
return false
};
t!("method called: send({method_call:?})");
assert!(method_call.send_res.is_none());
fn decode_data(data: &[u8]) -> std::io::Result<(Timestamp, String, String)> {
let mut cur = Cursor::new(&data);
let timest = Timestamp::decode(&mut cur).unwrap();
let channel = String::decode(&mut cur)?;
let msg = String::decode(&mut cur)?;
Ok((timest, channel, msg))
}
let Ok((timest, channel, msg)) = decode_data(&method_call.data) else {
e!("send() method invalid arg data");
return true
};
let Some(self_) = me.upgrade() else {
// Should not happen
panic!("self destroyed before send_method_task was stopped!");
};
self_.handle_send(timest, channel, msg).await;
true
}
async fn handle_send(&self, timest: Timestamp, channel: String, msg: String) {
let nick = self.nick.get();
// Send text to channel
d!("Sending privmsg: {timest} {channel}: <{nick}> {msg}");
let msg = Privmsg::new(channel, nick, msg);
let evgr = self.event_graph.clone();
let mut event = event_graph::Event::new(serialize_async(&msg).await, &evgr).await;
event.timestamp = timest;
let msg_id = msg.msg_id(timest);
// Keep track of our own messages so we don't apply timestamp correction to them
// which messes up the msg id.
{
let mut seen = self.seen_msgs.lock().unwrap();
seen.push(msg_id.clone(), true);
}
let mut arg_data = vec![];
timest.encode_async(&mut arg_data).await.unwrap();
msg_id.encode_async(&mut arg_data).await.unwrap();
msg.nick.encode_async(&mut arg_data).await.unwrap();
msg.msg.encode_async(&mut arg_data).await.unwrap();
// Broadcast the msg
if let Err(e) = evgr.dag_insert(&[event.clone()]).await {
error!(target: "darkirc", "Failed inserting new event to DAG: {}", e);
}
self.p2p.broadcast(&EventPut(event)).await;
}
async fn apply_settings(self_: Arc, _: BatchGuardPtr) {
self_.settings.save_settings();
let p2p_settings = self_.p2p.settings();
let mut write_guard = p2p_settings.write().await;
self_.settings.update_p2p_settings(&mut write_guard);
}
async fn start(self: Arc, sg_root: SceneNodePtr, ex: ExecutorPtr) {
i!("Registering EventGraph P2P protocol");
let event_graph_ = Arc::clone(&self.event_graph);
let registry = self.p2p.protocol_registry();
registry
.register(SESSION_DEFAULT, move |channel, _| {
let event_graph_ = event_graph_.clone();
async move { ProtocolEventGraph::init(event_graph_, channel).await.unwrap() }
})
.await;
let me = Arc::downgrade(&self);
let node = &self.node.upgrade().unwrap();
let method_sub = node.subscribe_method_call("send").unwrap();
let me2 = me.clone();
let send_method_task =
ex.spawn(async move { while Self::process_send(&me2, &method_sub).await {} });
let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
async fn save_nick(self_: Arc, _batch: BatchGuardPtr) {
let _ = std::fs::write(nick_filename(), self_.nick.get());
}
on_modify.when_change(self.nick.prop(), save_nick);
// `apply_settings` is triggered if any setting changes
for setting_node in self.settings.setting_root.get_children().iter() {
on_modify.when_change(
setting_node.get_property("value").clone().unwrap(),
Self::apply_settings,
);
}
let ev_sub = self.event_graph.event_pub.clone().subscribe().await;
let ev_task = ex.spawn(self.clone().relay_events(ev_sub));
// Sync the DAG
let channel_sub = self.p2p.hosts().subscribe_channel().await;
let dag_task = ex.spawn(self.clone().dag_sync(channel_sub));
// Subscribe to window start/stop signals for dynamic outbound connections
let window_node = sg_root.lookup_node("/window").unwrap();
let (start_slot, start_recv) = Slot::new("app_start");
window_node.register("start", start_slot).unwrap();
let p2p = self.p2p.clone();
let start_task = ex.spawn(async move {
while let Ok(_) = start_recv.recv().await {
i!("App started: set outbound connections to {P2P_OUTBOUND_ACTIVE}");
p2p.settings().write().await.outbound_connections = P2P_OUTBOUND_ACTIVE;
p2p.clone().reload().await;
}
});
let (stop_slot, stop_recv) = Slot::new("app_stop");
window_node.register("stop", stop_slot).unwrap();
let p2p = self.p2p.clone();
let stop_task = ex.spawn(async move {
while let Ok(_) = stop_recv.recv().await {
i!("App stopped: set outbound connections to {P2P_OUTBOUND_SLEEP}");
p2p.settings().write().await.outbound_connections = P2P_OUTBOUND_SLEEP;
p2p.clone().reload().await;
}
});
let mut tasks = vec![send_method_task, ev_task, dag_task, start_task, stop_task];
tasks.append(&mut on_modify.tasks);
self.tasks.set(tasks).unwrap();
}
}