mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-10 07:08:05 -05:00
app: add fud plugin, add FileMessage to MessageBuffer
This commit is contained in:
125
bin/app/Cargo.lock
generated
125
bin/app/Cargo.lock
generated
@@ -765,6 +765,15 @@ dependencies = [
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "1.0.3"
|
||||
@@ -1505,6 +1514,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"atomic_float",
|
||||
"blake3",
|
||||
"bs58",
|
||||
"chrono",
|
||||
"clap 4.5.53",
|
||||
"colored",
|
||||
@@ -1515,6 +1525,7 @@ dependencies = [
|
||||
"file-rotate",
|
||||
"fluent",
|
||||
"freetype-rs",
|
||||
"fud",
|
||||
"futures",
|
||||
"glam",
|
||||
"harfbuzz-sys",
|
||||
@@ -1909,6 +1920,33 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "dynasm"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynasmrt"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"dynasm",
|
||||
"fnv",
|
||||
"memmap2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easy-parallel"
|
||||
version = "3.3.1"
|
||||
@@ -2053,6 +2091,19 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "equix"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89178c5241f5cc0c8f2b5ac5008f3c7a32caad341b1ec747a6e1e51d2e877110"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"hashx",
|
||||
"num-traits",
|
||||
"thiserror 2.0.17",
|
||||
"visibility",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -2228,6 +2279,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||
|
||||
[[package]]
|
||||
name = "fixed-capacity-vec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b31a14f5ee08ed1a40e1252b35af18bed062e3f39b69aab34decde36bc43e40"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.5"
|
||||
@@ -2450,6 +2507,37 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fud"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"blake2",
|
||||
"blake3",
|
||||
"bs58",
|
||||
"darkfi",
|
||||
"darkfi-sdk",
|
||||
"darkfi-serial",
|
||||
"easy-parallel",
|
||||
"equix",
|
||||
"futures",
|
||||
"num-bigint",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"sha2",
|
||||
"signal-hook",
|
||||
"signal-hook-async-std",
|
||||
"sled-overlay",
|
||||
"smol",
|
||||
"structopt",
|
||||
"structopt-toml",
|
||||
"tinyjson",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@@ -2815,6 +2903,21 @@ dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashx"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb639748a589a17df2126f8015897ab416e81113afb82f56df5d47fa1486ab1"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"blake2",
|
||||
"dynasmrt",
|
||||
"fixed-capacity-vec",
|
||||
"hex",
|
||||
"rand_core 0.9.3",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
@@ -5181,6 +5284,28 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-async-std"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ae704a5db1c75caeec797eccb081a9980c8e374e8774c7cb7b26799cc727b38"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"futures-lite",
|
||||
"libc",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.7"
|
||||
|
||||
@@ -27,6 +27,8 @@ zeromq = { version = "0.4.1", default-features = false, features = ["async-std-r
|
||||
darkfi = {path = "../../", features = ["async-daemonize", "event-graph", "net", "util", "system", "zk"]}
|
||||
#darkfi-sdk = {path = "../../src/sdk", features = ["async"]}
|
||||
darkfi-serial = {version = "0.5.0", features = ["async"]}
|
||||
fud = { path = "../fud/fud/" }
|
||||
bs58 = "0.5.1"
|
||||
thiserror = "2.0.12"
|
||||
smol = "2.0.2"
|
||||
atomic_float = "1.1.0"
|
||||
|
||||
@@ -486,6 +486,8 @@ pub fn create_chatview(name: &str) -> SceneNode {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
node.add_method("update_file", vec![("hash", "Hash", CallArgType::Str)], None).unwrap();
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
|
||||
@@ -563,6 +563,7 @@ pub async fn make(
|
||||
window_scale.clone(),
|
||||
app.render_api.clone(),
|
||||
app.text_shaper.clone(),
|
||||
app.sg_root.clone(),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -283,6 +283,14 @@ async fn load_plugins(
|
||||
})
|
||||
.await;
|
||||
|
||||
let fud = create_fud("fud");
|
||||
let sg_root2 = sg_root.clone();
|
||||
let fud = fud
|
||||
.setup(|me| async {
|
||||
plugin::Fud::new(me, sg_root2, ex.clone()).await.expect("Fud pimpl setup")
|
||||
})
|
||||
.await;
|
||||
|
||||
let (slot, recvr) = Slot::new("recvmsg");
|
||||
darkirc.register("recv", slot).unwrap();
|
||||
let sg_root2 = sg_root.clone();
|
||||
@@ -399,6 +407,7 @@ async fn load_plugins(
|
||||
});
|
||||
|
||||
plugin.link(darkirc);
|
||||
plugin.link(fud);
|
||||
|
||||
i!("Plugins loaded");
|
||||
futures::join!(listen_recv, listen_connect);
|
||||
@@ -446,6 +455,19 @@ pub fn create_darkirc(name: &str) -> SceneNode {
|
||||
node
|
||||
}
|
||||
|
||||
pub fn create_fud(name: &str) -> SceneNode {
|
||||
t!("create_fud({name})");
|
||||
let mut node = SceneNode::new(name, SceneNodeType::Plugin);
|
||||
|
||||
let mut prop = Property::new("ready", PropertyType::Bool, PropertySubType::Null);
|
||||
prop.set_defaults_bool(vec![false]).unwrap();
|
||||
node.add_property(prop).unwrap();
|
||||
|
||||
node.add_method("get", vec![("hash", "Hash", CallArgType::Str)], None).unwrap();
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
/// Simple program to greet a person
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
|
||||
@@ -32,6 +32,7 @@ pub const COLOR_LIGHTGREY: Color = [0.7, 0.7, 0.7, 1.];
|
||||
pub const COLOR_GREEN: Color = [0., 1., 0., 1.];
|
||||
pub const COLOR_BLUE: Color = [0., 0., 1., 1.];
|
||||
pub const COLOR_PINK: Color = [0.8, 0.3, 0.8, 1.];
|
||||
pub const COLOR_CYAN: Color = [0., 1., 1., 1.];
|
||||
#[allow(dead_code)]
|
||||
pub const COLOR_PURPLE: Color = [1., 0., 1., 1.];
|
||||
pub const COLOR_WHITE: Color = [1., 1., 1., 1.];
|
||||
@@ -112,6 +113,61 @@ impl MeshBuilder {
|
||||
self.draw_box(obj, color, &uv);
|
||||
}
|
||||
|
||||
pub fn draw_box_shadow(&mut self, obj: &Rectangle, color: Color, spread: f32) {
|
||||
let (x1, y1) = obj.pos().unpack();
|
||||
let (x2, y2) = obj.corner().unpack();
|
||||
|
||||
let uv = Rectangle::zero();
|
||||
let (u1, v1) = uv.pos().unpack();
|
||||
let (u2, v2) = uv.corner().unpack();
|
||||
|
||||
let color2 = [color[0], color[1], color[2], 0.];
|
||||
|
||||
// left
|
||||
self.append(
|
||||
vec![
|
||||
Vertex { pos: [x1, y1], color, uv: [u1, v1] },
|
||||
Vertex { pos: [x1 - spread, y1 - spread], color: color2, uv: [u2, v1] },
|
||||
Vertex { pos: [x1, y2], color, uv: [u1, v2] },
|
||||
Vertex { pos: [x1 - spread, y2 + spread], color: color2, uv: [u2, v2] },
|
||||
],
|
||||
vec![0, 2, 1, 1, 2, 3],
|
||||
);
|
||||
|
||||
// top
|
||||
self.append(
|
||||
vec![
|
||||
Vertex { pos: [x1, y1], color, uv: [u1, v1] },
|
||||
Vertex { pos: [x1 - spread, y1 - spread], color: color2, uv: [u2, v1] },
|
||||
Vertex { pos: [x2, y1], color, uv: [u1, v2] },
|
||||
Vertex { pos: [x2 + spread, y1 - spread], color: color2, uv: [u2, v2] },
|
||||
],
|
||||
vec![0, 2, 1, 1, 2, 3],
|
||||
);
|
||||
|
||||
// right
|
||||
self.append(
|
||||
vec![
|
||||
Vertex { pos: [x2, y1], color, uv: [u1, v1] },
|
||||
Vertex { pos: [x2 + spread, y1 - spread], color: color2, uv: [u2, v1] },
|
||||
Vertex { pos: [x2, y2], color, uv: [u1, v2] },
|
||||
Vertex { pos: [x2 + spread, y2 + spread], color: color2, uv: [u2, v2] },
|
||||
],
|
||||
vec![0, 2, 1, 1, 2, 3],
|
||||
);
|
||||
|
||||
// bottom
|
||||
self.append(
|
||||
vec![
|
||||
Vertex { pos: [x1, y2], color, uv: [u1, v1] },
|
||||
Vertex { pos: [x1 - spread, y2 + spread], color: color2, uv: [u2, v1] },
|
||||
Vertex { pos: [x2, y2], color, uv: [u1, v2] },
|
||||
Vertex { pos: [x2 + spread, y2 + spread], color: color2, uv: [u2, v2] },
|
||||
],
|
||||
vec![0, 2, 1, 1, 2, 3],
|
||||
);
|
||||
}
|
||||
|
||||
pub fn draw_outline(&mut self, obj: &Rectangle, color: Color, thickness: f32) {
|
||||
let (x1, y1) = obj.pos().unpack();
|
||||
let (dist_x, dist_y) = (obj.w, obj.h);
|
||||
|
||||
507
bin/app/src/plugin/fud.rs
Normal file
507
bin/app/src/plugin/fud.rs
Normal file
@@ -0,0 +1,507 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use darkfi::{
|
||||
net::{
|
||||
session::{SESSION_DIRECT, SESSION_INBOUND},
|
||||
settings::Settings as NetSettings,
|
||||
P2p, P2pPtr,
|
||||
},
|
||||
system::{sleep, Publisher, PublisherPtr},
|
||||
};
|
||||
use darkfi_serial::{Decodable, Encodable};
|
||||
use sled_overlay::sled;
|
||||
use smol::lock::Mutex;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::Cursor,
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock, Weak},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use fud::{
|
||||
event::FudEvent, proto::ProtocolFud, settings::Args as FudSettings, util::hash_to_string, Fud,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyBool, Role},
|
||||
scene::{MethodCallSub, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak},
|
||||
ui::OnModify,
|
||||
ExecutorPtr,
|
||||
};
|
||||
|
||||
use super::PluginSettings;
|
||||
|
||||
const P2P_RETRY_TIME: u64 = 20;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod paths {
|
||||
use crate::android::{get_appdata_path, get_external_storage_path};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn get_base_path() -> PathBuf {
|
||||
get_external_storage_path().join("fud")
|
||||
}
|
||||
pub fn get_db_path() -> PathBuf {
|
||||
get_external_storage_path().join("fud/db")
|
||||
}
|
||||
pub fn get_downloads_path() -> PathBuf {
|
||||
get_external_storage_path().join("fud/downloads")
|
||||
}
|
||||
pub fn get_use_tor_filename() -> PathBuf {
|
||||
get_external_storage_path().join("use_tor.txt")
|
||||
}
|
||||
|
||||
pub fn p2p_datastore_path() -> PathBuf {
|
||||
get_appdata_path().join("fud/p2p")
|
||||
}
|
||||
pub fn hostlist_path() -> PathBuf {
|
||||
get_appdata_path().join("fud/hostlist.tsv")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod paths {
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn get_base_path() -> PathBuf {
|
||||
dirs::data_local_dir().unwrap().join("darkfi/app/fud")
|
||||
}
|
||||
pub fn get_db_path() -> PathBuf {
|
||||
dirs::data_local_dir().unwrap().join("darkfi/app/fud/db")
|
||||
}
|
||||
pub fn get_downloads_path() -> PathBuf {
|
||||
dirs::data_local_dir().unwrap().join("darkfi/app/fud/downloads")
|
||||
}
|
||||
pub fn get_use_tor_filename() -> PathBuf {
|
||||
dirs::data_local_dir().unwrap().join("darkfi/app/use_tor.txt")
|
||||
}
|
||||
|
||||
pub fn p2p_datastore_path() -> PathBuf {
|
||||
dirs::cache_dir().unwrap().join("darkfi/app/fud/p2p")
|
||||
}
|
||||
pub fn hostlist_path() -> PathBuf {
|
||||
dirs::cache_dir().unwrap().join("darkfi/app/fud/hostlist.tsv")
|
||||
}
|
||||
}
|
||||
|
||||
use paths::*;
|
||||
|
||||
macro_rules! t { ($($arg:tt)*) => { trace!(target: "plugin::fud", $($arg)*); } }
|
||||
macro_rules! d { ($($arg:tt)*) => { debug!(target: "plugin::fud", $($arg)*); } }
|
||||
macro_rules! i { ($($arg:tt)*) => { info!(target: "plugin::fud", $($arg)*); } }
|
||||
macro_rules! e { ($($arg:tt)*) => { error!(target: "plugin::fud", $($arg)*); } }
|
||||
|
||||
pub type FudPluginPtr = Arc<FudPlugin>;
|
||||
|
||||
pub struct FudPlugin {
|
||||
node: SceneNodeWeak,
|
||||
sg_root: SceneNodePtr,
|
||||
tasks: OnceLock<Vec<smol::Task<()>>>,
|
||||
|
||||
p2p: P2pPtr,
|
||||
event_pub: PublisherPtr<FudEvent>,
|
||||
fud: Arc<Fud>,
|
||||
|
||||
download_on_ready: Arc<Mutex<HashSet<Url>>>,
|
||||
|
||||
settings: PluginSettings,
|
||||
}
|
||||
|
||||
impl FudPlugin {
|
||||
pub async fn new(node: SceneNodeWeak, sg_root: SceneNodePtr, ex: ExecutorPtr) -> Result<Pimpl> {
|
||||
let node_ref = &node.upgrade().unwrap();
|
||||
// let fud_node_id = PropertyStr::wrap(node_ref, Role::Internal, "node_id", 0).unwrap();
|
||||
let fud_ready = PropertyBool::wrap(node_ref, Role::Internal, "ready", 0).unwrap();
|
||||
fud_ready.set(&mut PropertyAtomicGuard::none(), false);
|
||||
|
||||
let setting_root = Arc::new(SceneNode::new("setting", SceneNodeType::SettingRoot));
|
||||
node_ref.clone().link(setting_root.clone());
|
||||
|
||||
let basedir = get_base_path();
|
||||
|
||||
i!("Starting Fud backend");
|
||||
let db_path = get_db_path();
|
||||
let db = match sled::open(&db_path) {
|
||||
Ok(db) => db,
|
||||
Err(err) => {
|
||||
e!("Sled database '{}' failed to open: {err}!", db_path.display());
|
||||
return Err(Error::SledDbErr)
|
||||
}
|
||||
};
|
||||
|
||||
let setting_tree = db.open_tree("settings")?;
|
||||
let settings = PluginSettings { setting_root, sled_tree: setting_tree };
|
||||
|
||||
let mut fud_settings: FudSettings = Default::default();
|
||||
fud_settings.base_dir = basedir.to_string_lossy().to_string();
|
||||
let mut p2p_settings: NetSettings = fud_settings.net.clone().into();
|
||||
p2p_settings.app_version = semver::Version::parse("0.5.0").unwrap();
|
||||
if get_use_tor_filename().exists() {
|
||||
i!("Setup P2P network [tor]");
|
||||
p2p_settings.outbound_connect_timeout = 60;
|
||||
p2p_settings.channel_handshake_timeout = 55;
|
||||
p2p_settings.channel_heartbeat_interval = 90;
|
||||
p2p_settings.outbound_peer_discovery_cooloff_time = 60;
|
||||
|
||||
p2p_settings.seeds.push(
|
||||
url::Url::parse(
|
||||
"tor://g7fxelebievvpr27w7gt24lflptpw3jeeuvafovgliq5utdst6xyruyd.onion:24442",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
p2p_settings.seeds.push(
|
||||
url::Url::parse(
|
||||
"tor://yvklzjnfmwxhyodhrkpomawjcdvcaushsj6torjz2gyd7e25f3gfunyd.onion:24442",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
p2p_settings.allowed_transports = vec!["tor".to_string()];
|
||||
|
||||
fud_settings.pow.btc_electrum_nodes.push(
|
||||
url::Url::parse(
|
||||
"tor://hezojf7rda2c33yxgcgcvvsxflechdz5vkm64gwlszgx2r4gc5e42kqd.onion:50001",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
fud_settings.pow.btc_electrum_nodes.push(
|
||||
url::Url::parse(
|
||||
"tor://n4widoxtm3xpo2fjvtdffhb63q5td3utaxkolaegnpzb5khbwxvdrlad.onion:50001",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
fud_settings.pow.btc_electrum_nodes.push(
|
||||
url::Url::parse(
|
||||
"tor://duras25aqnp3tnn2zgma7pusms6c7umtunyu2sp6e5byotr3c4c6rzad.onion:50001",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
fud_settings.pow.btc_electrum_nodes.push(
|
||||
url::Url::parse(
|
||||
"tor://n3dz6thzxobyphuosoftgtf36rnsxlsjknke4yrbdys55zvd7nsx7qid.onion:50001",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
} else {
|
||||
i!("Setup P2P network [clearnet]");
|
||||
p2p_settings.outbound_connect_timeout = 40;
|
||||
p2p_settings.channel_handshake_timeout = 30;
|
||||
|
||||
p2p_settings.seeds.push(url::Url::parse("tcp+tls://lilith0.dark.fi:24441").unwrap());
|
||||
p2p_settings.seeds.push(url::Url::parse("tcp+tls://lilith1.dark.fi:24441").unwrap());
|
||||
|
||||
fud_settings
|
||||
.pow
|
||||
.btc_electrum_nodes
|
||||
.push(url::Url::parse("tcp+tls://erbium1.sytes.net:50002").unwrap());
|
||||
fud_settings
|
||||
.pow
|
||||
.btc_electrum_nodes
|
||||
.push(url::Url::parse("tcp+tls://ecdsa.net:110").unwrap());
|
||||
fud_settings
|
||||
.pow
|
||||
.btc_electrum_nodes
|
||||
.push(url::Url::parse("tcp+tls://electrum.no-ip.org:50002").unwrap());
|
||||
fud_settings
|
||||
.pow
|
||||
.btc_electrum_nodes
|
||||
.push(url::Url::parse("tcp+tls://electrumx.not.fyi:50002").unwrap());
|
||||
}
|
||||
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);
|
||||
// TODO: add other fud 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)
|
||||
}
|
||||
};
|
||||
|
||||
p2p.session_direct().start_peer_discovery();
|
||||
|
||||
let event_pub = Publisher::new();
|
||||
let fud: Arc<Fud> =
|
||||
match Fud::new(fud_settings, p2p.clone(), &db, event_pub.clone(), ex.clone()).await {
|
||||
Ok(fud) => fud,
|
||||
Err(err) => {
|
||||
e!("Cannot create fud instance: {err}");
|
||||
return Err(Error::ServiceFailed)
|
||||
}
|
||||
};
|
||||
|
||||
let self_ = Arc::new(Self {
|
||||
node: node.clone(),
|
||||
sg_root,
|
||||
tasks: OnceLock::new(),
|
||||
p2p,
|
||||
event_pub,
|
||||
fud,
|
||||
download_on_ready: Arc::new(Mutex::new(HashSet::new())),
|
||||
settings,
|
||||
});
|
||||
self_.clone().start(ex).await;
|
||||
Ok(Pimpl::Fud(self_))
|
||||
}
|
||||
|
||||
async fn apply_settings(self_: Arc<Self>, _batch: 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);
|
||||
|
||||
// TODO: add other fud settings
|
||||
}
|
||||
|
||||
async fn start(self: Arc<Self>, ex: ExecutorPtr) {
|
||||
i!("Registering Fud protocol");
|
||||
let registry = self.p2p.protocol_registry();
|
||||
let fud = self.fud.clone();
|
||||
let p2p = self.p2p.clone();
|
||||
registry
|
||||
.register(SESSION_DIRECT | SESSION_INBOUND, move |channel, _| {
|
||||
let fud_ = fud.clone();
|
||||
let p2p_ = p2p.clone();
|
||||
async move { ProtocolFud::init(fud_, channel, p2p_).await.unwrap() }
|
||||
})
|
||||
.await;
|
||||
|
||||
let me = Arc::downgrade(&self);
|
||||
|
||||
let node = &self.node.upgrade().unwrap();
|
||||
|
||||
let method_sub = node.subscribe_method_call("get").unwrap();
|
||||
let me2 = me.clone();
|
||||
let get_method_task =
|
||||
ex.spawn(async move { while Self::process_get(&me2, &method_sub).await {} });
|
||||
|
||||
let event_pub = self.event_pub.clone();
|
||||
let me2 = me.clone();
|
||||
let ev_task = ex.spawn(async move {
|
||||
Self::process_events(&me2, event_pub).await;
|
||||
});
|
||||
|
||||
let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
|
||||
|
||||
// `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 fud = self.fud.clone();
|
||||
let start_task = ex.spawn(async move {
|
||||
while fud.start().await.is_err() {
|
||||
sleep(10).await;
|
||||
}
|
||||
});
|
||||
|
||||
let mut tasks = vec![get_method_task, ev_task, start_task];
|
||||
tasks.append(&mut on_modify.tasks);
|
||||
self.tasks.set(tasks).unwrap();
|
||||
|
||||
i!("Starting Fud P2P");
|
||||
while let Err(err) = self.p2p.clone().start().await {
|
||||
// This usually means we cannot listen on the inbound ports
|
||||
e!("Failed to start fud's 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;
|
||||
}
|
||||
}
|
||||
|
||||
fn string_to_hash(str: &str) -> std::io::Result<blake3::Hash> {
|
||||
let mut hash_buf = vec![];
|
||||
|
||||
match bs58::decode(str).onto(&mut hash_buf) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid fud hash"))
|
||||
}
|
||||
}
|
||||
|
||||
if hash_buf.len() != 32 {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Invalid fud hash"))
|
||||
}
|
||||
|
||||
let mut hash_buf_arr = [0u8; 32];
|
||||
hash_buf_arr.copy_from_slice(&hash_buf);
|
||||
|
||||
Ok(blake3::Hash::from_bytes(hash_buf_arr))
|
||||
}
|
||||
|
||||
async fn process_get(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
|
||||
let Ok(method_call) = sub.receive().await else {
|
||||
d!("Fud event relayer closed");
|
||||
return false
|
||||
};
|
||||
|
||||
t!("method called: get({method_call:?})");
|
||||
assert!(method_call.send_res.is_none());
|
||||
|
||||
fn decode_data(data: &[u8]) -> std::io::Result<(String, Url)> {
|
||||
let mut cur = Cursor::new(&data);
|
||||
let url = Url::decode(&mut cur)?;
|
||||
let Some(hash_string) = url.host_str().clone() else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Missing fud hash"))
|
||||
};
|
||||
let hash_string = hash_string.to_string();
|
||||
|
||||
Ok((hash_string, url))
|
||||
}
|
||||
|
||||
let Some(self_) = me.upgrade() else {
|
||||
// Should not happen
|
||||
panic!("self destroyed before get_method_task was stopped!");
|
||||
};
|
||||
|
||||
let Ok((hash_string, url)) = decode_data(&method_call.data) else {
|
||||
e!("get() method invalid arg data");
|
||||
return true
|
||||
};
|
||||
|
||||
let Ok(hash) = FudPlugin::string_to_hash(&hash_string) else {
|
||||
let mut data = vec![];
|
||||
"invalid fud url".encode(&mut data).unwrap();
|
||||
self_.update_file(hash_string, "error", data).await;
|
||||
return true
|
||||
};
|
||||
|
||||
if self_.node.upgrade().unwrap().get_property_bool("ready").unwrap() {
|
||||
let file_selection = match url.path() {
|
||||
"/" | "" => fud::util::FileSelection::All,
|
||||
path => {
|
||||
let mut selection = HashSet::new();
|
||||
selection.insert(PathBuf::from(path.strip_prefix("/").unwrap_or(path)));
|
||||
fud::util::FileSelection::Set(selection)
|
||||
}
|
||||
};
|
||||
let _ = self_
|
||||
.fud
|
||||
.get(&hash, &get_downloads_path().join(&hash_string), file_selection)
|
||||
.await;
|
||||
} else {
|
||||
self_.download_on_ready.lock().await.insert(url);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
async fn update_file(self: &Arc<Self>, hash: String, status: &str, encoded_data: Vec<u8>) {
|
||||
let window = self.sg_root.lookup_node("/window");
|
||||
if window.is_none() {
|
||||
return
|
||||
}
|
||||
let window = window.unwrap();
|
||||
|
||||
for child in window.get_children() {
|
||||
if let Some(chatty) = child.lookup_node("/content/chatty") {
|
||||
let mut data = vec![];
|
||||
hash.encode(&mut data).unwrap();
|
||||
status.encode(&mut data).unwrap();
|
||||
data.extend(encoded_data.clone());
|
||||
let _ = chatty.call_method("update_file", data).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_events(me: &Weak<Self>, publisher: PublisherPtr<FudEvent>) {
|
||||
let Some(self_) = me.upgrade() else {
|
||||
// Should not happen
|
||||
panic!("self destroyed before ev_task was stopped!");
|
||||
};
|
||||
|
||||
let sub = publisher.subscribe().await;
|
||||
|
||||
loop {
|
||||
match sub.receive().await {
|
||||
FudEvent::Ready => {
|
||||
let atom = &mut PropertyAtomicGuard::none();
|
||||
self_
|
||||
.node
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
.set_property_bool(atom, Role::App, "ready", true)
|
||||
.unwrap();
|
||||
|
||||
let window = self_.sg_root.lookup_node("/window");
|
||||
if window.is_none() {
|
||||
continue
|
||||
}
|
||||
|
||||
for url in self_.download_on_ready.lock().await.iter() {
|
||||
let mut data = vec![];
|
||||
url.encode(&mut data).unwrap();
|
||||
let _ = self_.node.upgrade().unwrap().call_method("get", data).await;
|
||||
}
|
||||
}
|
||||
FudEvent::DownloadStarted(ev) => {
|
||||
let mut data = vec![];
|
||||
let bytes_downloaded = ev.resource.target_bytes_downloaded as f32;
|
||||
let bytes_size = ev.resource.target_bytes_size as f32;
|
||||
let progress =
|
||||
if bytes_size != 0.0 { bytes_downloaded / bytes_size * 100.0 } else { 0.0 };
|
||||
progress.encode(&mut data).unwrap();
|
||||
self_.update_file(hash_to_string(&ev.resource.hash), "downloading", data).await;
|
||||
}
|
||||
FudEvent::ChunkDownloadCompleted(ev) => {
|
||||
let mut data = vec![];
|
||||
let bytes_downloaded = ev.resource.target_bytes_downloaded as f32;
|
||||
let bytes_size = ev.resource.target_bytes_size as f32;
|
||||
let progress =
|
||||
if bytes_size != 0.0 { bytes_downloaded / bytes_size * 100.0 } else { 0.0 };
|
||||
progress.encode(&mut data).unwrap();
|
||||
self_.update_file(hash_to_string(&ev.resource.hash), "downloading", data).await;
|
||||
}
|
||||
FudEvent::DownloadCompleted(ev) => {
|
||||
let mut data = vec![];
|
||||
let path_string = ev.resource.path.to_string_lossy().to_string();
|
||||
path_string.encode(&mut data).unwrap();
|
||||
self_.update_file(hash_to_string(&ev.resource.hash), "downloaded", data).await;
|
||||
}
|
||||
FudEvent::DownloadError(ev) => {
|
||||
let mut data = vec![];
|
||||
ev.error.encode(&mut data).unwrap();
|
||||
self_.update_file(hash_to_string(&ev.hash), "error", data).await;
|
||||
}
|
||||
FudEvent::MissingChunks(ev) => {
|
||||
let mut data = vec![];
|
||||
"missing chunks".encode(&mut data).unwrap();
|
||||
self_.update_file(hash_to_string(&ev.hash), "error", data).await;
|
||||
}
|
||||
FudEvent::MetadataNotFound(ev) => {
|
||||
let mut data = vec![];
|
||||
"missing metadata".encode(&mut data).unwrap();
|
||||
self_.update_file(hash_to_string(&ev.hash), "error", data).await;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ pub mod darkirc;
|
||||
pub use darkirc::DarkIrc;
|
||||
pub use darkirc::DarkIrcPtr;
|
||||
|
||||
pub mod fud;
|
||||
pub use fud::{FudPlugin as Fud, FudPluginPtr as FudPtr};
|
||||
|
||||
use darkfi::net::Settings as NetSettings;
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -546,6 +546,7 @@ pub enum Pimpl {
|
||||
Gesture(ui::GesturePtr),
|
||||
EmojiPicker(ui::EmojiPickerPtr),
|
||||
DarkIrc(plugin::DarkIrcPtr),
|
||||
Fud(plugin::FudPtr),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Pimpl {
|
||||
|
||||
@@ -24,6 +24,7 @@ use darkfi_serial::{deserialize, Decodable, Encodable, SerialDecodable, SerialEn
|
||||
use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use rand::{rngs::OsRng, Rng};
|
||||
use regex::Regex;
|
||||
use sled_overlay::sled;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
@@ -34,9 +35,10 @@ use std::{
|
||||
},
|
||||
};
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
mod page;
|
||||
use page::MessageBuffer;
|
||||
use page::{FileMessageStatus, MessageBuffer};
|
||||
|
||||
use crate::{
|
||||
gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi},
|
||||
@@ -44,7 +46,7 @@ use crate::{
|
||||
BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
|
||||
PropertyFloat32, PropertyRect, PropertyUint32, Role,
|
||||
},
|
||||
scene::{MethodCallSub, Pimpl, SceneNodeWeak},
|
||||
scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
|
||||
text::TextShaperPtr,
|
||||
ExecutorPtr,
|
||||
};
|
||||
@@ -73,6 +75,11 @@ fn max(a: f32, b: f32) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_url(text: &String) -> Option<Url> {
|
||||
let url_regex = Regex::new(r"fud://[^\s]+").unwrap();
|
||||
url_regex.find(text).and_then(|match_| Url::parse(match_.as_str()).ok())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
|
||||
pub struct ChatMsg {
|
||||
pub nick: String,
|
||||
@@ -147,6 +154,7 @@ pub struct ChatView {
|
||||
node: SceneNodeWeak,
|
||||
tasks: SyncMutex<Vec<smol::Task<()>>>,
|
||||
render_api: RenderApi,
|
||||
sg_root: SceneNodePtr,
|
||||
|
||||
tree: sled::Tree,
|
||||
msgbuf: AsyncMutex<MessageBuffer>,
|
||||
@@ -189,6 +197,7 @@ impl ChatView {
|
||||
window_scale: PropertyFloat32,
|
||||
render_api: RenderApi,
|
||||
text_shaper: TextShaperPtr,
|
||||
sg_root: SceneNodePtr,
|
||||
) -> Pimpl {
|
||||
t!("ChatView::new()");
|
||||
|
||||
@@ -230,6 +239,7 @@ impl ChatView {
|
||||
node: node.clone(),
|
||||
tasks: SyncMutex::new(vec![]),
|
||||
render_api: render_api.clone(),
|
||||
sg_root,
|
||||
|
||||
tree,
|
||||
msgbuf: AsyncMutex::new(MessageBuffer::new(
|
||||
@@ -444,11 +454,31 @@ impl ChatView {
|
||||
t!("Mark sent message as confirmed");
|
||||
} else {
|
||||
t!("Inserting new message");
|
||||
|
||||
// Insert the privmsg since it doesn't already exist
|
||||
if msgbuf.insert_privmsg(timest, msg_id, nick, text).is_none() {
|
||||
let privmsg = msgbuf.insert_privmsg(timest, msg_id.clone(), nick.clone(), text.clone());
|
||||
if privmsg.is_none() {
|
||||
// Not visible so no need to redraw
|
||||
return
|
||||
}
|
||||
|
||||
if let Some(url) = get_file_url(&text) {
|
||||
if let Some(fud) = self.sg_root.lookup_node("/plugin/fud") {
|
||||
msgbuf.insert_filemsg(
|
||||
timest,
|
||||
msg_id,
|
||||
FileMessageStatus::Initializing,
|
||||
nick,
|
||||
url.clone(),
|
||||
);
|
||||
|
||||
let mut data = vec![];
|
||||
url.encode(&mut data).unwrap();
|
||||
fud.call_method("get", data).await.unwrap();
|
||||
}
|
||||
} else {
|
||||
error!(target: "ui::chatview", "Fud plugin has not been loaded");
|
||||
}
|
||||
}
|
||||
|
||||
let atom = self.render_api.make_guard(gfxtag!("ChatView::handle_insert_line"));
|
||||
@@ -559,6 +589,11 @@ impl ChatView {
|
||||
}
|
||||
};
|
||||
|
||||
let Some(fud) = self.sg_root.lookup_node("/plugin/fud") else {
|
||||
error!(target: "ui::chatview", "Fud plugin has not been loaded");
|
||||
return
|
||||
};
|
||||
|
||||
let mut do_redraw = false;
|
||||
for entry in iter {
|
||||
let Ok((k, v)) = entry else { break };
|
||||
@@ -569,7 +604,26 @@ impl ChatView {
|
||||
let chatmsg: ChatMsg = deserialize(&v).unwrap();
|
||||
|
||||
//t!("{timest:?} {chatmsg:?} [trace_id={trace_id}]");
|
||||
let msg_height = msgbuf.push_privmsg(timest, msg_id, chatmsg.nick, chatmsg.text);
|
||||
let msg_height = msgbuf.push_privmsg(
|
||||
timest,
|
||||
msg_id.clone(),
|
||||
chatmsg.nick.clone(),
|
||||
chatmsg.text.clone(),
|
||||
);
|
||||
|
||||
if let Some(url) = get_file_url(&chatmsg.text) {
|
||||
msgbuf.insert_filemsg(
|
||||
timest,
|
||||
msg_id,
|
||||
FileMessageStatus::Initializing,
|
||||
chatmsg.nick.clone(),
|
||||
url.clone(),
|
||||
);
|
||||
|
||||
let mut data = vec![];
|
||||
url.encode(&mut data).unwrap();
|
||||
fud.call_method("get", data).await.unwrap();
|
||||
}
|
||||
|
||||
remaining_load_height -= msg_height;
|
||||
if remaining_load_height <= 0. {
|
||||
@@ -754,6 +808,22 @@ impl UIObject for ChatView {
|
||||
}
|
||||
});
|
||||
|
||||
let method_sub = node_ref.subscribe_method_call("update_file").unwrap();
|
||||
let self_ = self.clone();
|
||||
let update_file_task = ex.spawn(async move {
|
||||
loop {
|
||||
let Ok(method_call) = method_sub.receive().await else {
|
||||
d!("Event relayer closed");
|
||||
return
|
||||
};
|
||||
let mut msgbuf = self_.msgbuf.lock().await;
|
||||
msgbuf.update_file(&method_call.data).await;
|
||||
msgbuf.adjust_params();
|
||||
let atom = self_.render_api.make_guard(gfxtag!("ChatView::update_file_task"));
|
||||
self_.redraw_cached(atom.batch_id, &mut msgbuf).await;
|
||||
}
|
||||
});
|
||||
|
||||
let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
|
||||
|
||||
async fn reload_view(self_: Arc<ChatView>, batch: BatchGuardPtr) {
|
||||
@@ -783,8 +853,13 @@ impl UIObject for ChatView {
|
||||
on_modify.when_change(self.rect.prop(), redraw);
|
||||
//on_modify.when_change(self.debug.prop(), redraw);
|
||||
|
||||
let mut tasks =
|
||||
vec![insert_line_method_task, insert_unconf_line_method_task, motion_task, bgload_task];
|
||||
let mut tasks = vec![
|
||||
insert_line_method_task,
|
||||
insert_unconf_line_method_task,
|
||||
motion_task,
|
||||
bgload_task,
|
||||
update_file_task,
|
||||
];
|
||||
tasks.append(&mut on_modify.tasks);
|
||||
|
||||
*self.tasks.lock() = tasks;
|
||||
|
||||
@@ -17,18 +17,27 @@
|
||||
*/
|
||||
|
||||
use async_gen::{gen as async_gen, AsyncIter};
|
||||
use async_trait::async_trait;
|
||||
use chrono::{Local, NaiveDate, TimeZone};
|
||||
use darkfi_serial::{Decodable, FutAsyncWriteExt, SerialDecodable, SerialEncodable};
|
||||
use futures::stream::{Stream, StreamExt};
|
||||
use image::{ImageBuffer, ImageReader, Rgba};
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
io::Cursor,
|
||||
pin::pin,
|
||||
sync::Arc,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use super::{max, MessageId, Timestamp};
|
||||
use crate::{
|
||||
gfx::{gfxtag, DrawMesh, Rectangle, RenderApi},
|
||||
mesh::{Color, MeshBuilder, COLOR_BLUE, COLOR_PINK, COLOR_WHITE},
|
||||
gfx::{gfxtag, DrawMesh, ManagedTexturePtr, Rectangle, RenderApi},
|
||||
mesh::{
|
||||
Color, MeshBuilder, COLOR_BLUE, COLOR_CYAN, COLOR_GREEN, COLOR_PINK, COLOR_RED, COLOR_WHITE,
|
||||
},
|
||||
prop::{PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr},
|
||||
text::{self, Glyph, GlyphPositionIter, TextShaper, TextShaperPtr},
|
||||
util::enumerate_mut,
|
||||
@@ -484,11 +493,300 @@ impl std::fmt::Debug for DateMessage {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, SerialEncodable, SerialDecodable)]
|
||||
pub enum FileMessageStatus {
|
||||
Initializing,
|
||||
Downloading { progress: f32 },
|
||||
Downloaded { path: String },
|
||||
Error { msg: String },
|
||||
}
|
||||
|
||||
type GenericImageBuffer = ImageBuffer<Rgba<u8>, Vec<u8>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FileMessage {
|
||||
font_size: f32,
|
||||
window_scale: f32,
|
||||
max_width: f32,
|
||||
|
||||
file_url: Url,
|
||||
status: FileMessageStatus,
|
||||
imgbuf: Arc<SyncMutex<Option<GenericImageBuffer>>>,
|
||||
timestamp: Timestamp,
|
||||
glyphs: Vec<Vec<Glyph>>,
|
||||
|
||||
atlas: text::RenderedAtlas,
|
||||
}
|
||||
|
||||
impl FileMessage {
|
||||
const GLOW_SIZE: f32 = 20.;
|
||||
const MARGIN_TOP: f32 = 4.;
|
||||
const MARGIN_BOTTOM: f32 = 10.;
|
||||
const BOX_PADDING_TOP: f32 = 15.;
|
||||
const BOX_PADDING_BOTTOM: f32 = 8.;
|
||||
const BOX_PADDING_X: f32 = 15.;
|
||||
const IMG_MAX_HEIGHT: f32 = 500.;
|
||||
|
||||
pub fn new(
|
||||
font_size: f32,
|
||||
window_scale: f32,
|
||||
|
||||
file_url: Url,
|
||||
status: FileMessageStatus,
|
||||
timestamp: Timestamp,
|
||||
_nick: String,
|
||||
|
||||
text_shaper: &TextShaper,
|
||||
render_api: &RenderApi,
|
||||
) -> Message {
|
||||
let mut glyphs = Vec::new();
|
||||
let mut atlas = text::Atlas::new(render_api, gfxtag!("chatview_filemsg"));
|
||||
|
||||
for str in Self::filestr(&file_url, &status) {
|
||||
let glyphs_ = text_shaper.shape(str, font_size, window_scale);
|
||||
atlas.push(&glyphs_);
|
||||
glyphs.push(glyphs_);
|
||||
}
|
||||
|
||||
let atlas = atlas.make();
|
||||
|
||||
Message::File(Self {
|
||||
font_size,
|
||||
window_scale,
|
||||
max_width: 0.,
|
||||
file_url,
|
||||
status,
|
||||
imgbuf: Arc::new(SyncMutex::new(None)),
|
||||
timestamp,
|
||||
glyphs,
|
||||
atlas,
|
||||
})
|
||||
}
|
||||
|
||||
fn filestr(file_url: &Url, status: &FileMessageStatus) -> Vec<String> {
|
||||
let status_str = match status {
|
||||
FileMessageStatus::Initializing => "starting fud".to_string(),
|
||||
FileMessageStatus::Downloading { progress } => format!("downloading [{progress:.1}%]"),
|
||||
FileMessageStatus::Downloaded { .. } => "downloaded".to_string(),
|
||||
FileMessageStatus::Error { msg } => msg.to_lowercase(),
|
||||
};
|
||||
|
||||
vec![
|
||||
file_url
|
||||
.host_str()
|
||||
.map(|file_hash| {
|
||||
if file_hash.len() >= 12 {
|
||||
let first_part = &file_hash[..4];
|
||||
let last_part = &file_hash[file_hash.len() - 4..];
|
||||
format!("{}...{}", first_part, last_part)
|
||||
} else {
|
||||
file_hash.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or("???".to_string()),
|
||||
status_str,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: &FileMessageStatus) {
|
||||
self.status = status.clone();
|
||||
|
||||
if let FileMessageStatus::Downloaded { .. } = status {
|
||||
let mut imgbuf = self.imgbuf.lock();
|
||||
*imgbuf = self.load_img();
|
||||
}
|
||||
}
|
||||
|
||||
fn adjust_params(
|
||||
&mut self,
|
||||
font_size: f32,
|
||||
window_scale: f32,
|
||||
text_shaper: &TextShaper,
|
||||
render_api: &RenderApi,
|
||||
) {
|
||||
self.font_size = font_size;
|
||||
self.window_scale = window_scale;
|
||||
|
||||
self.glyphs = Vec::new();
|
||||
let mut atlas = text::Atlas::new(render_api, gfxtag!("chatview_filemsg"));
|
||||
|
||||
for str in Self::filestr(&self.file_url, &self.status) {
|
||||
let glyphs = text_shaper.shape(str, font_size, window_scale);
|
||||
atlas.push(&glyphs);
|
||||
self.glyphs.push(glyphs);
|
||||
}
|
||||
|
||||
self.atlas = atlas.make();
|
||||
}
|
||||
|
||||
fn adjust_width(&mut self, line_width: f32, timestamp_width: f32) {
|
||||
let width = line_width - timestamp_width;
|
||||
// clamp to > 0
|
||||
self.max_width = max(width, 0.);
|
||||
}
|
||||
|
||||
fn clear_mesh(&mut self) {}
|
||||
|
||||
fn get_img_size(&self, imgbuf: &ImageBuffer<Rgba<u8>, Vec<u8>>) -> (f32, f32) {
|
||||
let img_w = imgbuf.width() as f32;
|
||||
let img_h = imgbuf.height() as f32;
|
||||
|
||||
let width_scale = (self.max_width - Self::GLOW_SIZE) / img_w;
|
||||
let height_scale = Self::IMG_MAX_HEIGHT / img_h;
|
||||
|
||||
let scale = width_scale.min(height_scale);
|
||||
(img_w * scale, img_h * scale)
|
||||
}
|
||||
|
||||
fn gen_mesh(
|
||||
&mut self,
|
||||
_clip: &Rectangle,
|
||||
line_height: f32,
|
||||
baseline: f32,
|
||||
timestamp_width: f32,
|
||||
_nick_colors: &[Color],
|
||||
timestamp_color: Color,
|
||||
_text_color: Color,
|
||||
_debug_render: bool,
|
||||
render_api: &RenderApi,
|
||||
) -> Vec<DrawMesh> {
|
||||
let uv_rect = Rectangle::from([0., 0., 1., 1.]);
|
||||
|
||||
let imgbuf_ = self.imgbuf.lock();
|
||||
if let Some(ref imgbuf) = *imgbuf_ {
|
||||
let (img_w, img_h) = self.get_img_size(imgbuf);
|
||||
drop(imgbuf_);
|
||||
|
||||
let mesh_rect =
|
||||
Rectangle::from([timestamp_width, -img_h - Self::MARGIN_BOTTOM, img_w, img_h]);
|
||||
let texture = self.load_texture(render_api);
|
||||
let mut mesh_gradient = MeshBuilder::new(gfxtag!("file_gradient"));
|
||||
let glow_color = [timestamp_color[0], timestamp_color[1], timestamp_color[2], 0.5];
|
||||
mesh_gradient.draw_box_shadow(&mesh_rect, glow_color, Self::GLOW_SIZE);
|
||||
|
||||
let mesh_gradient = mesh_gradient.alloc(render_api);
|
||||
let mesh_gradient = mesh_gradient.draw_untextured();
|
||||
|
||||
let mut mesh_img = MeshBuilder::new(gfxtag!("file_img"));
|
||||
mesh_img.draw_box(&mesh_rect, COLOR_WHITE, &uv_rect);
|
||||
let mesh_img = mesh_img.alloc(render_api);
|
||||
let mesh_img = mesh_img.draw_with_texture(texture);
|
||||
return vec![mesh_img, mesh_gradient];
|
||||
}
|
||||
drop(imgbuf_);
|
||||
|
||||
let mut mesh = MeshBuilder::new(gfxtag!("chatview_filemsg"));
|
||||
|
||||
let color = match self.status {
|
||||
FileMessageStatus::Initializing => timestamp_color,
|
||||
FileMessageStatus::Downloading { .. } => COLOR_CYAN,
|
||||
FileMessageStatus::Downloaded { .. } => COLOR_GREEN,
|
||||
FileMessageStatus::Error { .. } => COLOR_RED,
|
||||
};
|
||||
|
||||
let mut text_width = 0.;
|
||||
for (i, glyphs) in self.glyphs.iter().enumerate() {
|
||||
let glyph_pos_iter =
|
||||
GlyphPositionIter::new(self.font_size, self.window_scale, &glyphs, baseline);
|
||||
for (mut glyph_rect, glyph) in glyph_pos_iter.zip(glyphs.iter()) {
|
||||
let uv_rect = self.atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
|
||||
if glyph_rect.x + glyph_rect.w > text_width {
|
||||
text_width = glyph_rect.x + glyph_rect.w;
|
||||
}
|
||||
glyph_rect.x += timestamp_width + Self::BOX_PADDING_X;
|
||||
glyph_rect.y -= line_height * (self.glyphs.len() - i) as f32 +
|
||||
Self::BOX_PADDING_BOTTOM +
|
||||
Self::MARGIN_BOTTOM;
|
||||
mesh.draw_box(&glyph_rect, color, uv_rect);
|
||||
}
|
||||
}
|
||||
|
||||
let box_width = text_width + Self::BOX_PADDING_X * 2.;
|
||||
let box_height = self.glyphs.len() as f32 * line_height +
|
||||
Self::BOX_PADDING_TOP +
|
||||
Self::BOX_PADDING_BOTTOM;
|
||||
let mesh_rect = Rectangle::from([
|
||||
timestamp_width,
|
||||
-box_height - Self::MARGIN_BOTTOM,
|
||||
box_width,
|
||||
box_height,
|
||||
]);
|
||||
mesh.draw_outline(&mesh_rect, color, 1.);
|
||||
|
||||
let glow_color = [color[0], color[1], color[2], 0.3];
|
||||
mesh.draw_box_shadow(&mesh_rect, glow_color, Self::GLOW_SIZE);
|
||||
|
||||
let mesh = mesh.alloc(render_api);
|
||||
let mesh = mesh.draw_with_texture(self.atlas.texture.clone());
|
||||
|
||||
vec![mesh]
|
||||
}
|
||||
|
||||
fn load_img(&self) -> Option<ImageBuffer<Rgba<u8>, Vec<u8>>> {
|
||||
if let FileMessageStatus::Downloaded { path } = &self.status {
|
||||
let path = path.as_str();
|
||||
|
||||
let data = Arc::new(SyncMutex::new(vec![]));
|
||||
let data2 = data.clone();
|
||||
miniquad::fs::load_file(path, move |res| match res {
|
||||
Ok(res) => *data2.lock() = res,
|
||||
Err(e) => {
|
||||
error!("Resource not found! {e}");
|
||||
}
|
||||
});
|
||||
let data = std::mem::take(&mut *data.lock());
|
||||
let Ok(img) =
|
||||
ImageReader::new(Cursor::new(data)).with_guessed_format().unwrap().decode()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
return Some(img.to_rgba8());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn load_texture(&self, render_api: &RenderApi) -> ManagedTexturePtr {
|
||||
let imgbuf = self.imgbuf.lock();
|
||||
let img = imgbuf.as_ref().unwrap();
|
||||
|
||||
let width = img.width() as u16;
|
||||
let height = img.height() as u16;
|
||||
let bmp = img.as_raw().clone();
|
||||
drop(imgbuf);
|
||||
|
||||
render_api.new_texture(width, height, bmp, gfxtag!("file_img_texture"))
|
||||
}
|
||||
|
||||
pub fn height(&self, line_height: f32) -> f32 {
|
||||
let imgbuf = self.imgbuf.lock();
|
||||
imgbuf
|
||||
.as_ref()
|
||||
.map(|buf| self.get_img_size(buf).1 as f32 + Self::MARGIN_TOP + Self::MARGIN_BOTTOM)
|
||||
.unwrap_or(
|
||||
line_height * self.glyphs.len() as f32 +
|
||||
Self::BOX_PADDING_TOP +
|
||||
Self::BOX_PADDING_BOTTOM +
|
||||
Self::MARGIN_TOP +
|
||||
Self::MARGIN_BOTTOM,
|
||||
)
|
||||
}
|
||||
|
||||
fn select(&mut self) {}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FileMessage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "file: {}", self.file_url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Easier than fucking around with traits nonsense
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Priv(PrivMessage),
|
||||
Date(DateMessage),
|
||||
File(FileMessage),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
@@ -496,6 +794,7 @@ impl Message {
|
||||
match self {
|
||||
Self::Priv(m) => m.timestamp,
|
||||
Self::Date(m) => m.timestamp,
|
||||
Self::File(m) => m.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +802,7 @@ impl Message {
|
||||
match self {
|
||||
Self::Priv(m) => m.height(line_height),
|
||||
Self::Date(_) => line_height,
|
||||
Self::File(m) => m.height(line_height),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +827,7 @@ impl Message {
|
||||
render_api,
|
||||
),
|
||||
Self::Date(m) => m.adjust_params(font_size, window_scale, text_shaper, render_api),
|
||||
Self::File(m) => m.adjust_params(font_size, window_scale, text_shaper, render_api),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +835,7 @@ impl Message {
|
||||
match self {
|
||||
Self::Priv(m) => m.adjust_width(line_width, timestamp_width),
|
||||
Self::Date(_) => {}
|
||||
Self::File(m) => m.adjust_width(line_width, timestamp_width),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,6 +843,7 @@ impl Message {
|
||||
match self {
|
||||
Self::Priv(m) => m.clear_mesh(),
|
||||
Self::Date(m) => m.clear_mesh(),
|
||||
Self::File(m) => m.clear_mesh(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,9 +860,9 @@ impl Message {
|
||||
hi_bg_color: Color,
|
||||
debug_render: bool,
|
||||
render_api: &RenderApi,
|
||||
) -> DrawMesh {
|
||||
) -> Vec<DrawMesh> {
|
||||
match self {
|
||||
Self::Priv(m) => m.gen_mesh(
|
||||
Self::Priv(m) => vec![m.gen_mesh(
|
||||
clip,
|
||||
line_height,
|
||||
msg_spacing,
|
||||
@@ -571,8 +874,8 @@ impl Message {
|
||||
hi_bg_color,
|
||||
debug_render,
|
||||
render_api,
|
||||
),
|
||||
Self::Date(m) => m.gen_mesh(
|
||||
)],
|
||||
Self::Date(m) => vec![m.gen_mesh(
|
||||
clip,
|
||||
line_height,
|
||||
baseline,
|
||||
@@ -583,6 +886,17 @@ impl Message {
|
||||
// No hi_bg_color since dates can't be highlighted
|
||||
debug_render,
|
||||
render_api,
|
||||
)],
|
||||
Self::File(m) => m.gen_mesh(
|
||||
clip,
|
||||
line_height,
|
||||
baseline,
|
||||
timestamp_width,
|
||||
nick_colors,
|
||||
timestamp_color,
|
||||
text_color,
|
||||
debug_render,
|
||||
render_api,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -591,6 +905,7 @@ impl Message {
|
||||
match self {
|
||||
Self::Priv(_) => false,
|
||||
Self::Date(_) => true,
|
||||
Self::File(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +913,7 @@ impl Message {
|
||||
match self {
|
||||
Self::Priv(m) => m.select(),
|
||||
Self::Date(_) => {}
|
||||
Self::File(m) => m.select(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +923,13 @@ impl Message {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filemsg_mut(&mut self) -> Option<&mut FileMessage> {
|
||||
match self {
|
||||
Message::File(msg) => Some(msg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_nick_color(nick: &str, nick_colors: &[Color]) -> Color {
|
||||
@@ -925,7 +1248,7 @@ impl MessageBuffer {
|
||||
continue
|
||||
}
|
||||
|
||||
let mesh = msg.gen_mesh(
|
||||
for mesh in msg.gen_mesh(
|
||||
rect,
|
||||
line_height,
|
||||
msg_spacing,
|
||||
@@ -937,9 +1260,9 @@ impl MessageBuffer {
|
||||
hi_bg_color,
|
||||
debug_render,
|
||||
&render_api,
|
||||
);
|
||||
|
||||
meshes.push((current_pos, mesh));
|
||||
) {
|
||||
meshes.push((current_pos, mesh));
|
||||
}
|
||||
|
||||
current_pos += msg_spacing;
|
||||
current_pos += mesh_height;
|
||||
@@ -949,6 +1272,50 @@ impl MessageBuffer {
|
||||
meshes
|
||||
}
|
||||
|
||||
pub fn insert_filemsg(
|
||||
&mut self,
|
||||
timest: Timestamp,
|
||||
msg_id: MessageId,
|
||||
status: FileMessageStatus,
|
||||
nick: String,
|
||||
file_url: Url,
|
||||
) -> Option<&mut FileMessage> {
|
||||
t!("insert_filemsg({timest}, {msg_id}, {nick}, {file_url})");
|
||||
let font_size = self.font_size.get();
|
||||
let window_scale = self.window_scale.get();
|
||||
|
||||
let msg = FileMessage::new(
|
||||
font_size,
|
||||
window_scale,
|
||||
file_url,
|
||||
status,
|
||||
timest,
|
||||
nick,
|
||||
&self.text_shaper,
|
||||
&self.render_api,
|
||||
);
|
||||
|
||||
// Timestamps go from most recent backwards
|
||||
let mut idx = None;
|
||||
for (i, msg) in enumerate_mut(&mut self.msgs) {
|
||||
if timest >= msg.timestamp() {
|
||||
idx = Some(i);
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let idx = match idx {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
let last_page_idx = 0;
|
||||
last_page_idx
|
||||
}
|
||||
};
|
||||
|
||||
self.msgs.insert(idx, msg);
|
||||
self.msgs[idx].get_filemsg_mut()
|
||||
}
|
||||
|
||||
/// Gets around borrow checker with unsafe
|
||||
fn msgs_with_date(&mut self) -> impl Stream<Item = &mut Message> {
|
||||
let font_size = self.font_size.get();
|
||||
@@ -1042,6 +1409,7 @@ impl MessageBuffer {
|
||||
}
|
||||
|
||||
msg.select();
|
||||
|
||||
msg.clear_mesh();
|
||||
break
|
||||
}
|
||||
@@ -1050,4 +1418,38 @@ impl MessageBuffer {
|
||||
current_pos += mesh_height;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_file(&mut self, data: &Vec<u8>) {
|
||||
let mut cur = Cursor::new(data);
|
||||
let hash = String::decode(&mut cur).unwrap();
|
||||
let status = String::decode(&mut cur).unwrap();
|
||||
|
||||
let status = match status.as_str() {
|
||||
"downloading" => {
|
||||
let progress = f32::decode(&mut cur).unwrap();
|
||||
FileMessageStatus::Downloading { progress }
|
||||
}
|
||||
"downloaded" => {
|
||||
let path = String::decode(&mut cur).unwrap();
|
||||
FileMessageStatus::Downloaded { path }
|
||||
}
|
||||
"error" => {
|
||||
let msg = String::decode(&mut cur).unwrap();
|
||||
FileMessageStatus::Error { msg }
|
||||
}
|
||||
_ => FileMessageStatus::Initializing,
|
||||
};
|
||||
|
||||
// TODO: keep a cache of file messages somewhere to avoid looping
|
||||
// over all messages
|
||||
for msg in &mut self.msgs {
|
||||
if let Some(filemsg) = msg.get_filemsg_mut() {
|
||||
if filemsg.file_url.host_str() == Some(&hash) {
|
||||
filemsg.set_status(&status);
|
||||
filemsg.adjust_width(self.line_width, self.timestamp_width.get());
|
||||
filemsg.clear_mesh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user