use crate::events::{ConnectionState, Error, Event, Result};
#[cfg(feature = "scripts")]
use crate::extensions::scripts::ScriptManager;
use crate::types::ClientId;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
pub use teamtalk_sys as ffi;
use super::bus;
#[cfg(feature = "state")]
use super::cache;
use super::hooks;
mod init;
mod runtime;
static NEXT_CLIENT_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug)]
pub struct TTPtr(pub *mut ffi::TTInstance);
unsafe impl Send for TTPtr {}
unsafe impl Sync for TTPtr {}
pub struct Client {
pub name: Option<String>,
pub(crate) ptr: TTPtr,
pub(crate) id: ClientId,
pub(crate) backend: Arc<dyn super::backend::TeamTalkBackend>,
pub(crate) label: Mutex<Option<String>>,
pub(crate) state: Mutex<ConnectionState>,
pub(crate) hooks: Mutex<hooks::ClientHooks>,
pub(crate) hooks_revision: AtomicU64,
pub(crate) bus: Mutex<bus::EventBus>,
pub(crate) bus_revision: AtomicU64,
#[cfg(feature = "scripts")]
pub(crate) scripts: Mutex<Option<ScriptManager>>,
pub(crate) auto_reconnect: Mutex<AutoReconnectState>,
#[cfg(feature = "state")]
pub(crate) cache: Mutex<cache::CacheState>,
#[cfg(windows)]
pub(crate) init_mode: ClientInitMode,
}
unsafe impl Send for Client {}
unsafe impl Sync for Client {}
pub struct ClientEvents(pub Arc<Client>);
impl ClientEvents {
pub fn poll(&self, timeout_ms: i32) -> Option<(Event, Message)> {
self.0.poll(timeout_ms)
}
}
#[derive(Clone)]
pub struct ClientCommands(pub Arc<Client>);
impl std::ops::Deref for ClientCommands {
type Target = Client;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Client {
pub(crate) fn with_initialized_backend(
backend: Arc<dyn super::backend::TeamTalkBackend>,
ptr: *mut ffi::TTInstance,
#[cfg(windows)] init_mode: ClientInitMode,
) -> Result<Self> {
if ptr.is_null() {
Err(Error::InitFailed)
} else {
Ok(Self {
name: None,
ptr: TTPtr(ptr),
id: ClientId(NEXT_CLIENT_ID.fetch_add(1, Ordering::Relaxed)),
backend,
label: Mutex::new(None),
state: Mutex::new(ConnectionState::Idle),
hooks: Mutex::new(hooks::ClientHooks::default()),
hooks_revision: AtomicU64::new(0),
bus: Mutex::new(bus::EventBus::default()),
bus_revision: AtomicU64::new(0),
#[cfg(feature = "scripts")]
scripts: Mutex::new(None),
auto_reconnect: Mutex::new(AutoReconnectState::default()),
#[cfg(feature = "state")]
cache: Mutex::new(cache::CacheState::default()),
#[cfg(windows)]
init_mode,
})
}
}
pub fn new() -> Result<Self> {
init::new_client()
}
pub fn split(self) -> (ClientEvents, ClientCommands) {
let shared = Arc::new(self);
(ClientEvents(shared.clone()), ClientCommands(shared))
}
#[cfg(windows)]
pub unsafe fn with_hwnd(hwnd: ffi::HWND, msg: u32) -> Result<Self> {
unsafe { init::new_client_with_hwnd(hwnd, msg) }
}
#[cfg(windows)]
pub unsafe fn swap_hwnd(&self, hwnd: ffi::HWND) -> bool {
unsafe { ffi::api().TT_SwapTeamTalkHWND(self.ptr.0, hwnd) == 1 }
}
pub(crate) fn backend(&self) -> &dyn super::backend::TeamTalkBackend {
self.backend.as_ref()
}
#[cfg(windows)]
pub(crate) fn is_hwnd_client(&self) -> bool {
matches!(self.init_mode, ClientInitMode::Hwnd)
}
#[cfg(feature = "mock")]
pub fn with_backend(backend: Arc<dyn super::backend::TeamTalkBackend>) -> Result<Self> {
init::new_client_with_backend(backend)
}
#[cfg(feature = "mock")]
pub fn mock_set_connection_state_for_tests(&self, state: ConnectionState) {
self.set_connection_state(state);
}
#[cfg(feature = "mock")]
pub fn mock_set_pending_commands_for_tests(&self, login: Option<i32>, join: Option<i32>) {
let mut auto = self
.auto_reconnect
.lock()
.unwrap_or_else(|e| e.into_inner());
auto.pending_login_cmd = login;
auto.pending_join_cmd = join;
}
#[cfg(feature = "mock")]
pub fn mock_pending_commands_for_tests(&self) -> (Option<i32>, Option<i32>) {
let auto = self
.auto_reconnect
.lock()
.unwrap_or_else(|e| e.into_inner());
(auto.pending_login_cmd, auto.pending_join_cmd)
}
#[cfg(feature = "mock")]
pub fn mock_last_channel_password_for_tests(&self) -> Option<String> {
self.auto_reconnect
.lock()
.unwrap_or_else(|e| e.into_inner())
.last_channel_password
.clone()
}
#[cfg(feature = "mock")]
pub fn mock_apply_event_for_tests(&self, event: Event, source: i32) {
let mut raw = unsafe { std::mem::zeroed::<ffi::TTMessage>() };
raw.nSource = source;
let message = Message::from_raw(event, raw);
self.update_state_for_event(event, &message);
}
pub fn with_name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
pub fn with_label(self, label: &str) -> Self {
*self.label.lock().unwrap_or_else(|e| e.into_inner()) = Some(label.to_string());
self
}
pub fn id(&self) -> ClientId {
self.id
}
pub fn label(&self) -> Option<String> {
self.label.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn set_label(&self, label: Option<&str>) {
*self.label.lock().unwrap_or_else(|e| e.into_inner()) =
label.map(|value| value.to_string());
}
pub fn connection_state(&self) -> ConnectionState {
*self.state.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn on_event(&self, event: Event) -> bus::SubscriptionBuilder<'_> {
bus::SubscriptionBuilder::new(self, Some(event))
}
pub fn on_any(&self) -> bus::SubscriptionBuilder<'_> {
bus::SubscriptionBuilder::new(self, None)
}
pub fn unsubscribe_event(&self, id: bus::EventSubscriptionId) -> bool {
let removed = self
.bus
.lock()
.unwrap_or_else(|e| e.into_inner())
.unsubscribe(id);
if removed {
self.bus_revision.fetch_add(1, Ordering::Relaxed);
}
removed
}
pub fn clear_event_subscriptions(&self) {
let mut bus = self.bus.lock().unwrap_or_else(|e| e.into_inner());
if bus.len() > 0 {
bus.clear();
self.bus_revision.fetch_add(1, Ordering::Relaxed);
}
}
pub fn unsubscribe_event_group(&self, group: impl AsRef<str>) -> usize {
let group = bus::EventSubscriptionGroup::new(group.as_ref());
let removed = self
.bus
.lock()
.unwrap_or_else(|e| e.into_inner())
.unsubscribe_group(&group);
if removed > 0 {
self.bus_revision.fetch_add(1, Ordering::Relaxed);
}
removed
}
pub fn event_subscription_count(&self) -> usize {
self.bus.lock().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn set_hooks(&self, hooks: hooks::ClientHooks) {
*self.hooks.lock().unwrap_or_else(|e| e.into_inner()) = hooks;
self.hooks_revision.fetch_add(1, Ordering::Relaxed);
}
pub fn clear_hooks(&self) {
*self.hooks.lock().unwrap_or_else(|e| e.into_inner()) = hooks::ClientHooks::default();
self.hooks_revision.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "scripts")]
pub fn enable_scripts(&self) {
let mut scripts = self.scripts.lock().unwrap_or_else(|e| e.into_inner());
if scripts.is_none() {
*scripts = Some(ScriptManager::new());
}
}
#[cfg(feature = "scripts")]
pub fn set_script_manager(&self, manager: ScriptManager) {
*self.scripts.lock().unwrap_or_else(|e| e.into_inner()) = Some(manager);
}
#[cfg(feature = "scripts")]
pub fn clear_scripts(&self) {
*self.scripts.lock().unwrap_or_else(|e| e.into_inner()) = None;
}
#[cfg(feature = "scripts")]
pub fn scripts_mut<F, R>(&self, f: F) -> Option<R>
where
F: FnOnce(&mut ScriptManager) -> R,
{
self.scripts
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_mut()
.map(f)
}
pub(crate) fn set_connection_state(&self, state: ConnectionState) {
*self.state.lock().unwrap_or_else(|e| e.into_inner()) = state;
}
pub(crate) fn invoke_hooks(&self, event: crate::events::Event, msg: &Message) {
let revision_before = self.hooks_revision.load(Ordering::Relaxed);
let mut local_hooks = {
let mut hooks = self.hooks.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *hooks)
};
local_hooks.fire(self, event, msg);
if self.hooks_revision.load(Ordering::Relaxed) == revision_before {
*self.hooks.lock().unwrap_or_else(|e| e.into_inner()) = local_hooks;
}
}
pub(crate) fn dispatch_bus(&self, event: crate::events::Event, msg: &Message) {
let revision_before = self.bus_revision.load(Ordering::Relaxed);
let mut local_bus = {
let mut bus = self.bus.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *bus)
};
local_bus.dispatch(self, event, msg);
if self.bus_revision.load(Ordering::Relaxed) == revision_before {
*self.bus.lock().unwrap_or_else(|e| e.into_inner()) = local_bus;
}
}
#[cfg(feature = "scripts")]
pub(crate) fn dispatch_scripts(&self, event: crate::events::Event, msg: &Message) {
if let Some(manager) = self
.scripts
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
{
let _ = manager.handle_event(event, msg);
}
}
pub(crate) fn invoke_joined_hook(&self, channel_id: crate::types::ChannelId) {
let revision_before = self.hooks_revision.load(Ordering::Relaxed);
let mut local_hooks = {
let mut hooks = self.hooks.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *hooks)
};
local_hooks.fire_joined(self, channel_id);
if self.hooks_revision.load(Ordering::Relaxed) == revision_before {
*self.hooks.lock().unwrap_or_else(|e| e.into_inner()) = local_hooks;
}
}
pub(crate) fn handle_auto_reconnect(&self) {
if self.handle_pending_phase_timeout() {
return;
}
let state = *self.state.lock().unwrap_or_else(|e| e.into_inner());
match state {
ConnectionState::Disconnected => self.handle_connect_recovery(),
ConnectionState::Connected => self.handle_login_recovery(),
ConnectionState::LoggedIn => self.handle_join_recovery(),
ConnectionState::Joined(_) => self.handle_recovery_completed(),
_ => {}
}
}
}
mod debug;
mod message;
mod reconnect_state;
mod recovery;
mod watchdog;
pub use message::{EventData, Message};
pub(crate) use reconnect_state::AutoReconnectState;
#[cfg(windows)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ClientInitMode {
Poll,
Hwnd,
}