#[cfg(mobile)]
compile_error!("tauri-plugin-deeprespond-im 只支持桌面:手机 App 用 Flutter SDK(《sdk-tauri/2.功能概述文档》第 1 节)");
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use serde::Serialize;
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
use tauri::{AppHandle, Manager, RunEvent, Runtime};
mod badge;
mod bus;
mod commands;
mod device;
mod engine;
mod error;
mod files;
mod hub;
mod instance;
mod logs;
mod media;
mod notify;
mod paths;
mod secure;
mod store;
mod system;
pub use error::{Error, Result};
pub use hub::ENGINE_LABEL;
pub use secure::Fallback;
pub const PROTOCOL_VERSION: u32 = 1;
pub const PLUGIN_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Debug, Serialize)]
pub struct NotificationClick {
pub id: String,
pub kind: String,
pub conversation_id: Option<String>,
pub seq: Option<i64>,
pub action: Option<String>,
pub data: Option<serde_json::Map<String, serde_json::Value>>,
pub launched: bool,
}
#[derive(Clone, Debug, Serialize)]
pub struct CallAction {
pub action: String,
pub call_id: String,
pub data: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Clone, Copy, Debug, Default, Serialize, PartialEq, Eq)]
pub struct Unread {
pub total: u64,
pub badge: u64,
}
#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EngineState {
Starting,
Ready,
Restarting,
Failed,
}
type AppCallback<R, T> = Box<dyn Fn(&AppHandle<R>, T) + Send + Sync>;
type AppHook<R> = Box<dyn Fn(&AppHandle<R>) + Send + Sync>;
pub(crate) struct Callbacks<R: Runtime> {
pub on_notification_click: Option<AppCallback<R, NotificationClick>>,
pub on_call_action: Option<AppCallback<R, CallAction>>,
pub on_unread_changed: Option<AppCallback<R, Unread>>,
pub on_main_window_missing: Option<AppHook<R>>,
}
#[derive(Clone, Debug)]
pub(crate) struct Config {
pub engine_url: String,
pub main_window: String,
pub exit_when_last_window_closed: bool,
pub fallback: Fallback,
pub log_file: bool,
pub log_dir: Option<PathBuf>,
pub proxy: Option<String>,
pub allow_read_dirs: Vec<PathBuf>,
pub allow_write_dirs: Vec<PathBuf>,
pub disable_app_nap: bool,
}
pub(crate) struct Shared {
pub config: Config,
pub identifier: String,
pub paths: paths::Paths,
pub hub: hub::Hub,
pub bus: bus::Bus,
pub secure: Arc<secure::Secure>,
pub device: device::Device,
pub store: store::StoreManager,
pub files: files::FilesState,
pub notify: notify::Notifier,
pub badge: badge::BadgeState,
pub system: system::SystemState,
pub engine: engine::EngineCtl,
pub logs: logs::LogFiles,
pub unread: Mutex<Unread>,
pub another_instance: bool,
pub _instance_lock: Option<instance::InstanceLock>,
}
pub struct Builder<R: Runtime> {
config: Config,
callbacks: Callbacks<R>,
}
impl<R: Runtime> Default for Builder<R> {
fn default() -> Self {
Self::new()
}
}
impl<R: Runtime> Builder<R> {
pub fn new() -> Self {
Self {
config: Config {
engine_url: "engine.html".into(),
main_window: "main".into(),
exit_when_last_window_closed: true,
fallback: Fallback::File,
log_file: false,
log_dir: None,
proxy: None,
allow_read_dirs: Vec::new(),
allow_write_dirs: Vec::new(),
disable_app_nap: true,
},
callbacks: Callbacks { on_notification_click: None, on_call_action: None, on_unread_changed: None, on_main_window_missing: None },
}
}
pub fn engine_url(mut self, path: impl Into<String>) -> Self {
self.config.engine_url = path.into();
self
}
pub fn main_window(mut self, label: impl Into<String>) -> Self {
self.config.main_window = label.into();
self
}
pub fn on_main_window_missing(mut self, f: impl Fn(&AppHandle<R>) + Send + Sync + 'static) -> Self {
self.callbacks.on_main_window_missing = Some(Box::new(f));
self
}
pub fn exit_when_last_window_closed(mut self, exit: bool) -> Self {
self.config.exit_when_last_window_closed = exit;
self
}
pub fn secure_storage_fallback(mut self, fallback: Fallback) -> Self {
self.config.fallback = fallback;
self
}
pub fn log_file(mut self, enabled: bool) -> Self {
self.config.log_file = enabled;
self
}
pub fn log_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.config.log_dir = Some(dir.into());
self
}
pub fn proxy(mut self, url: impl Into<String>) -> Self {
self.config.proxy = Some(url.into());
self
}
pub fn allow_read_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.config.allow_read_dirs.push(dir.into());
self
}
pub fn allow_write_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.config.allow_write_dirs.push(dir.into());
self
}
pub fn on_unread_changed(mut self, f: impl Fn(&AppHandle<R>, Unread) + Send + Sync + 'static) -> Self {
self.callbacks.on_unread_changed = Some(Box::new(f));
self
}
pub fn on_notification_click(mut self, f: impl Fn(&AppHandle<R>, NotificationClick) + Send + Sync + 'static) -> Self {
self.callbacks.on_notification_click = Some(Box::new(f));
self
}
pub fn on_call_action(mut self, f: impl Fn(&AppHandle<R>, CallAction) + Send + Sync + 'static) -> Self {
self.callbacks.on_call_action = Some(Box::new(f));
self
}
pub fn disable_app_nap(mut self, disable: bool) -> Self {
self.config.disable_app_nap = disable;
self
}
pub fn build(self) -> TauriPlugin<R> {
let Builder { config, callbacks } = self;
let callbacks = Mutex::new(Some(callbacks));
let setup_config = config.clone();
let mut builder = PluginBuilder::<R>::new("deeprespond-im")
.invoke_handler(commands::handler())
.setup(move |app, _api| {
if let Some(cb) = callbacks.lock().unwrap().take() {
app.manage(cb);
}
let shared = setup(app, setup_config.clone())?;
app.manage(shared.clone());
engine::start(app, &shared)?;
system::start(app, &shared);
Ok(())
})
.on_event(|app, event| on_event(app, event))
.on_webview_ready(|webview| {
files::protocol::enable_cors(&webview);
media::allow_own_media(&webview);
})
.on_page_load(|webview, payload| engine::on_page_load(webview, payload));
builder = files::register_protocol(builder);
builder.build()
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new().build()
}
fn setup<R: Runtime>(app: &AppHandle<R>, config: Config) -> std::result::Result<Arc<Shared>, Box<dyn std::error::Error>> {
engine::check_engine_url(&config.engine_url)?;
let identifier = app.config().identifier.clone();
let path = app.path();
let data = path.app_local_data_dir()?.join("deeprespond_im");
let temp = path.app_cache_dir()?.join("deeprespond_im");
let log_dir = match &config.log_dir {
Some(dir) => dir.clone(),
None => path.app_log_dir()?.join("deeprespond_im"),
};
paths::ensure_private_dir(&data)?;
paths::ensure_private_dir(&temp)?;
paths::clean_old_files(&temp, std::time::Duration::from_secs(86400));
let paths = paths::Paths { data: data.clone(), temp: temp.clone(), logs: log_dir.clone() };
let logs = logs::LogFiles::new(log_dir, config.log_file);
let lock = match instance::acquire(&data) {
Ok(lock) => lock,
Err(err) => {
log::warn!("单实例的文件锁不可用:{err}");
None
}
};
let another_instance = lock.is_none();
if another_instance {
logs.plugin("warn", "另一个进程正在使用同一份数据,本进程不创建引擎客户端");
}
let device = device::load_or_create(&data, &identifier, device::machine_id().as_deref())?;
let secure = Arc::new(secure::Secure::new(data.clone(), config.fallback, Box::new(secure::SystemKeyStore::new(&identifier))));
let handle = app.clone();
let store = store::StoreManager::new(
data.clone(),
secure.clone(),
Arc::new(move |app_key: String, freed: usize| {
if let Some(shared) = handle.try_state::<Arc<Shared>>() {
shared.hub.send_engine(serde_json::json!({ "t": "storage_full", "app_key": app_key, "freed_users": freed }));
}
}),
);
let files = files::FilesState::new(paths.clone(), config.allow_read_dirs.clone(), config.allow_write_dirs.clone(), config.proxy.clone());
let notify = notify::Notifier::new(app, &identifier);
let shared = Arc::new(Shared {
identifier,
paths,
hub: hub::Hub::default(),
bus: bus::Bus::default(),
secure,
device,
store,
files,
notify,
badge: badge::BadgeState::default(),
system: system::SystemState::default(),
engine: engine::EngineCtl::default(),
logs,
unread: Mutex::new(Unread::default()),
another_instance,
_instance_lock: lock,
config,
});
Ok(shared)
}
pub(crate) fn release_instance(shared: &Shared, instance: &hub::Instance) {
shared.bus.release_instance(&shared.hub, &instance.label, instance.id);
shared.store.release_instance(&instance.label, instance.id);
shared.files.release_instance(&instance.label, instance.id);
}
fn on_event<R: Runtime>(app: &AppHandle<R>, event: &RunEvent) {
let Some(shared) = app.try_state::<Arc<Shared>>() else { return };
let shared = shared.inner().clone();
match event {
RunEvent::Ready => engine::on_ready(app, &shared),
RunEvent::ExitRequested { code, api, .. } => engine::on_exit_requested(app, &shared, *code, api),
RunEvent::WindowEvent { label, event, .. } => {
engine::on_window_event(app, &shared, label, event);
if let tauri::WindowEvent::DragDrop(tauri::DragDropEvent::Drop { paths, .. }) = event {
files::on_drag_drop(&shared, label, paths);
}
}
RunEvent::Exit => {
shared.store.close_all();
shared.logs.flush();
}
_ => {}
}
}
pub fn exit<R: Runtime>(app: &AppHandle<R>, code: i32) {
match app.try_state::<Arc<Shared>>() {
Some(shared) => engine::begin_exit(app, &shared, code),
None => app.exit(code),
}
}
pub fn handle_args<R: Runtime>(app: &AppHandle<R>, argv: &[String]) {
notify::handle_args(app, argv);
}
pub struct DeeprespondIm<R: Runtime> {
app: AppHandle<R>,
}
impl<R: Runtime> DeeprespondIm<R> {
pub fn unread(&self) -> Unread {
self.app.try_state::<Arc<Shared>>().map(|s| *s.unread.lock().unwrap()).unwrap_or_default()
}
pub fn engine_state(&self) -> EngineState {
self.app.try_state::<Arc<Shared>>().map(|s| s.engine.state()).unwrap_or(EngineState::Starting)
}
pub fn exit(&self, code: i32) {
exit(&self.app, code)
}
}
pub trait DeeprespondImExt<R: Runtime> {
fn deeprespond_im(&self) -> DeeprespondIm<R>;
}
impl<R: Runtime, T: Manager<R>> DeeprespondImExt<R> for T {
fn deeprespond_im(&self) -> DeeprespondIm<R> {
DeeprespondIm { app: self.app_handle().clone() }
}
}