chore: rework presence, add dynamic button functions to presence

This commit is contained in:
HorizonCode committed 2026-05-21 09:21:15 +02:00
1 parent eca08f16c5
commit 16fe91b3a5
1 file changed
+185 -117
+185 -117
View File
@@ -3,13 +3,17 @@ use discord_rich_presence::{
activity::{Activity, Assets, Button, Timestamps}, activity::{Activity, Assets, Button, Timestamps},
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex as StdMutex; use std::sync::Mutex as StdMutex;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use tokio::time::{Duration, interval}; use tokio::time::{Duration, interval};
#[derive(Clone, Debug)]
pub struct PresenceButton {
pub label: String,
pub url: String,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PresenceData { pub struct PresenceData {
pub state: String, pub state: String,
@@ -18,6 +22,21 @@ pub struct PresenceData {
pub large_image_text: String, pub large_image_text: String,
pub small_image_key: Option<String>, pub small_image_key: Option<String>,
pub small_image_text: Option<String>, pub small_image_text: Option<String>,
pub dynamic_button: Option<PresenceButton>,
}
impl Default for PresenceData {
fn default() -> Self {
Self {
state: "Idle in Launcher...".to_string(),
details: " ".to_string(),
large_image_key: "ezppfarm".to_string(),
large_image_text: "EZPPFarm".to_string(),
small_image_key: None,
small_image_text: None,
dynamic_button: None,
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -26,14 +45,21 @@ enum PresenceCommand {
Disconnect(oneshot::Sender<()>), Disconnect(oneshot::Sender<()>),
UpdateData(PresenceData), UpdateData(PresenceData),
IsConnected(oneshot::Sender<bool>), IsConnected(oneshot::Sender<bool>),
Tick, }
enum ReconnectResult {
Connected(DiscordIpcClient),
Failed,
} }
struct PresenceActor { struct PresenceActor {
receiver: mpsc::Receiver<PresenceCommand>, receiver: mpsc::Receiver<PresenceCommand>,
reconnect_rx: mpsc::Receiver<ReconnectResult>,
reconnect_tx: mpsc::Sender<ReconnectResult>,
client: Option<DiscordIpcClient>, client: Option<DiscordIpcClient>,
data: PresenceData, data: PresenceData,
start_timestamp: i64, start_timestamp: i64,
reconnecting: bool,
} }
impl PresenceActor { impl PresenceActor {
@@ -43,116 +69,96 @@ impl PresenceActor {
.unwrap() .unwrap()
.as_secs() as i64; .as_secs() as i64;
let data = PresenceData { let (reconnect_tx, reconnect_rx) = mpsc::channel(1);
state: "Idle in Launcher...".to_string(),
details: " ".to_string(),
large_image_key: "ezppfarm".to_string(),
large_image_text: "EZPPFarm".to_string(),
small_image_key: None,
small_image_text: None,
};
PresenceActor { Self {
receiver, receiver,
reconnect_rx,
reconnect_tx,
client: None, client: None,
data, data: PresenceData::default(),
start_timestamp: start, start_timestamp: start,
reconnecting: false,
} }
} }
async fn run(&mut self) { async fn run(&mut self) {
let mut update_interval = interval(Duration::from_secs(5)); let mut heartbeat = interval(Duration::from_secs(5));
heartbeat.tick().await;
loop { loop {
tokio::select! { tokio::select! {
Some(cmd) = self.receiver.recv() => { Some(cmd) = self.receiver.recv() => {
match cmd { match cmd {
PresenceCommand::Connect(responder) => self.handle_connect(responder).await, PresenceCommand::Connect(resp) => self.cmd_connect(resp).await,
PresenceCommand::Disconnect(responder) => { PresenceCommand::Disconnect(resp) => self.cmd_disconnect(resp).await,
self.handle_disconnect(responder).await; PresenceCommand::UpdateData(data) => {
}, self.data = data;
PresenceCommand::UpdateData(new_data) => { self.push_presence().await;
self.data = new_data;
},
PresenceCommand::IsConnected(responder) => {
let _ = responder.send(self.client.is_some());
} }
PresenceCommand::Tick => { PresenceCommand::IsConnected(resp) => {
if self.client.is_some() { let _ = resp.send(self.client.is_some());
self.handle_update().await;
} }
} }
} }
Some(result) = self.reconnect_rx.recv() => {
self.reconnecting = false;
match result {
ReconnectResult::Connected(client) => {
println!("Presence: reconnect succeeded.");
self.client = Some(client);
self.push_presence().await;
} }
_ = update_interval.tick() => { ReconnectResult::Failed => {
let _ = PRESENCE_TX.send(PresenceCommand::Tick).await; eprintln!("Presence: all reconnect attempts exhausted.");
}
}
}
_ = heartbeat.tick() => {
self.push_presence().await;
} }
} }
} }
} }
fn handle_connect( async fn cmd_connect(&mut self, resp: oneshot::Sender<bool>) {
&mut self,
responder: oneshot::Sender<bool>,
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
if self.client.is_some() { if self.client.is_some() {
let _ = responder.send(true); let _ = resp.send(true);
return; return;
} }
println!("Actor: Connecting to Discord..."); match try_connect_once() {
let mut dc_client = DiscordIpcClient::new("1032772293220384808"); Some(client) => {
match dc_client.connect() { self.client = Some(client);
Ok(()) => { self.reconnecting = false;
self.client = Some(dc_client); self.push_presence().await;
println!("Actor: Connected successfully."); let _ = resp.send(true);
self.handle_update().await;
let _ = responder.send(true);
} }
Err(e) => { None => {
eprintln!( eprintln!("Presence: initial connect failed; spawning reconnect task.");
"Failed to connect to Discord: {:?}. Scheduling bounded retry...", let _ = resp.send(false);
e self.spawn_reconnect_task();
);
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
for attempt in 1..=3 {
tokio::time::sleep(Duration::from_secs(5 * attempt)).await;
let (resp_tx, rx) = oneshot::channel();
if tx.send(PresenceCommand::Connect(resp_tx)).await.is_ok() {
if rx.await.unwrap_or(false) {
println!("Actor: Reconnection attempt {} succeeded", attempt);
break;
}
println!(
"Actor: Reconnection attempt {} failed, retrying...",
attempt
);
} }
} }
});
if !responder.is_closed() {
let _ = responder.send(false);
}
}
}
})
} }
async fn handle_disconnect(&mut self, responder: oneshot::Sender<()>) { async fn cmd_disconnect(&mut self, resp: oneshot::Sender<()>) {
if let Some(mut client) = self.client.take() { if let Some(mut client) = self.client.take() {
println!("Actor: Disconnecting from Discord...");
let _ = client.clear_activity(); let _ = client.clear_activity();
let _ = client.close(); let _ = client.close();
println!("Actor: Disconnected successfully."); println!("Presence: disconnected.");
} }
let _ = responder.send(()); let _ = resp.send(());
} }
async fn handle_update(&mut self) { async fn push_presence(&mut self) {
if let Some(client) = self.client.as_mut() { let client = match self.client.as_mut() {
Some(c) => c,
None => return,
};
let mut assets = Assets::new() let mut assets = Assets::new()
.large_image(&self.data.large_image_key) .large_image(&self.data.large_image_key)
.large_text(&self.data.large_image_text); .large_text(&self.data.large_image_text);
@@ -164,54 +170,82 @@ impl PresenceActor {
assets = assets.small_text(text); assets = assets.small_text(text);
} }
let buttons = if let Some(btn) = &self.data.dynamic_button {
vec![
Button::new(&btn.label, &btn.url),
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
]
} else {
vec![
Button::new("Download the Launcher", "https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest"),
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
]
};
let activity = Activity::new() let activity = Activity::new()
.state(&self.data.state) .state(&self.data.state)
.details(&self.data.details) .details(&self.data.details)
.timestamps(Timestamps::new().start(self.start_timestamp)) .timestamps(Timestamps::new().start(self.start_timestamp))
.assets(assets) .assets(assets)
.buttons(vec![ .buttons(buttons);
Button::new(
"Download the Launcher",
"https://git.ez-pp.farm/EZPPFarm/EZPPLauncher/releases/latest",
),
Button::new("Join EZPPFarm", "https://ez-pp.farm/discord"),
]);
if let Err(e) = client.set_activity(activity) { if let Err(e) = client.set_activity(activity) {
eprintln!("Failed to set activity, disconnecting: {:?}", e); eprintln!("Presence: set_activity failed ({:?}); will reconnect.", e);
self.client = None; self.client = None;
self.spawn_reconnect_task();
// Don't await reconnection here - send command to avoid blocking the actor
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
let (resp_tx, _) = oneshot::channel();
let _ = tx.send(PresenceCommand::Connect(resp_tx)).await;
});
} else {
println!("Actor: Presence updated successfully.");
} }
} }
fn spawn_reconnect_task(&mut self) {
if self.reconnecting {
return;
}
self.reconnecting = true;
let tx = self.reconnect_tx.clone();
tokio::spawn(async move {
const MAX_ATTEMPTS: u32 = 5;
for attempt in 1..=MAX_ATTEMPTS {
let delay = Duration::from_secs(5 * attempt as u64);
println!(
"Presence: reconnect attempt {}/{} in {:?}…",
attempt, MAX_ATTEMPTS, delay
);
tokio::time::sleep(delay).await;
if let Some(client) = try_connect_once() {
let _ = tx.send(ReconnectResult::Connected(client)).await;
return;
}
}
let _ = tx.send(ReconnectResult::Failed).await;
});
}
}
fn try_connect_once() -> Option<DiscordIpcClient> {
let mut client = DiscordIpcClient::new("1032772293220384808");
match client.connect() {
Ok(()) => {
println!("Presence: IPC connected.");
Some(client)
}
Err(e) => {
eprintln!("Presence: IPC connect error: {:?}", e);
None
}
} }
} }
static PRESENCE_TX: Lazy<mpsc::Sender<PresenceCommand>> = Lazy::new(|| { static PRESENCE_TX: Lazy<mpsc::Sender<PresenceCommand>> = Lazy::new(|| {
let (tx, rx) = mpsc::channel(10); let (tx, rx) = mpsc::channel(32);
let mut actor = PresenceActor::new(rx); let mut actor = PresenceActor::new(rx);
tokio::spawn(async move { actor.run().await }); tokio::spawn(async move { actor.run().await });
tx tx
}); });
pub static PRESENCE_DATA: Lazy<StdMutex<PresenceData>> = Lazy::new(|| { pub static PRESENCE_DATA: Lazy<StdMutex<PresenceData>> =
StdMutex::new(PresenceData { Lazy::new(|| StdMutex::new(PresenceData::default()));
state: "Idle in Launcher...".to_string(),
details: " ".to_string(),
large_image_key: "ezppfarm".to_string(),
large_image_text: "EZPPFarm".to_string(),
small_image_key: None,
small_image_text: None,
})
});
pub async fn connect() -> bool { pub async fn connect() -> bool {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@@ -223,8 +257,7 @@ pub async fn connect() -> bool {
pub async fn disconnect() { pub async fn disconnect() {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
// Use a timeout to ensure we don't hang the exit process if Discord is unresponsive if PRESENCE_TX.send(PresenceCommand::Disconnect(tx)).await.is_ok() {
if let Ok(_) = PRESENCE_TX.send(PresenceCommand::Disconnect(tx)).await {
let _ = tokio::time::timeout(Duration::from_secs(2), rx).await; let _ = tokio::time::timeout(Duration::from_secs(2), rx).await;
} }
} }
@@ -242,30 +275,65 @@ pub async fn has_presence() -> bool {
} }
pub fn update_status(state: Option<&str>, details: Option<&str>, large_image_key: Option<&str>) { pub fn update_status(state: Option<&str>, details: Option<&str>, large_image_key: Option<&str>) {
let mut data = PRESENCE_DATA.lock().unwrap(); let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
if let Some(s) = state { if let Some(s) = state {
data.state = s.to_string(); guard.state = s.to_string();
} }
if let Some(d) = details { if let Some(d) = details {
data.details = d.to_string(); guard.details = d.to_string();
} }
if let Some(img) = large_image_key { if let Some(img) = large_image_key {
data.large_image_key = img.to_string(); guard.large_image_key = img.to_string();
} }
let data_clone = data.clone(); guard.clone()
};
let tx = PRESENCE_TX.clone(); let tx = PRESENCE_TX.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data_clone)).await; let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn set_button(label: &str, url: &str) {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
guard.dynamic_button = Some(PresenceButton {
label: label.to_string(),
url: url.to_string(),
});
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
});
}
pub fn clear_button() {
let data = {
let mut guard = PRESENCE_DATA.lock().unwrap();
guard.dynamic_button = None;
guard.clone()
};
let tx = PRESENCE_TX.clone();
tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data)).await;
}); });
} }
pub fn update_user(username: Option<&str>, id: Option<&str>) { pub fn update_user(username: Option<&str>, id: Option<&str>) {
let mut data = PRESENCE_DATA.lock().unwrap(); let data = {
data.small_image_key = id.map(|id_str| format!("https://a.ez-pp.farm/{}", id_str)); let mut guard = PRESENCE_DATA.lock().unwrap();
data.small_image_text = username.map(|s| s.to_string()); guard.small_image_key = id.map(|s| format!("https://a.ez-pp.farm/{}", s));
let data_clone = data.clone(); guard.small_image_text = username.map(str::to_string);
guard.clone()
};
let tx = PRESENCE_TX.clone(); let tx = PRESENCE_TX.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _ = tx.send(PresenceCommand::UpdateData(data_clone)).await; let _ = tx.send(PresenceCommand::UpdateData(data)).await;
}); });
} }