use serde::Deserialize;
use serde_json::{json, Value};
use crate::{
context::DiscordContext,
error::{DiscordError, Result},
proto::PreloadedUserSettings,
route::Route,
types::*,
};
#[derive(Debug, Clone)]
pub struct SettingsProtoResponse {
pub out_of_date: bool,
pub settings: PreloadedUserSettings,
pub raw_settings_b64: String,
}
#[derive(Deserialize)]
struct RawSettingsProtoResponse {
settings: String,
#[serde(default)]
out_of_date: bool,
}
#[derive(Debug, Clone)]
pub enum ActivityData {
Playing { name: String },
Streaming { name: String, url: String },
Listening { name: String },
Watching { name: String },
Competing { name: String },
}
impl ActivityData {
pub fn playing(name: impl Into<String>) -> Self {
Self::Playing { name: name.into() }
}
pub fn streaming(name: impl Into<String>, url: impl Into<String>) -> Self {
Self::Streaming { name: name.into(), url: url.into() }
}
pub fn listening(name: impl Into<String>) -> Self {
Self::Listening { name: name.into() }
}
pub fn watching(name: impl Into<String>) -> Self {
Self::Watching { name: name.into() }
}
pub fn competing(name: impl Into<String>) -> Self {
Self::Competing { name: name.into() }
}
fn to_json(&self) -> Value {
match self {
Self::Playing { name } => json!({ "name": name, "type": 0 }),
Self::Streaming { name, url } => json!({ "name": name, "type": 1, "url": url }),
Self::Listening { name } => json!({ "name": name, "type": 2 }),
Self::Watching { name } => json!({ "name": name, "type": 3 }),
Self::Competing { name } => json!({ "name": name, "type": 5 }),
}
}
}
impl<T: DiscordContext + Send + Sync> StatusOps for T {}
#[allow(async_fn_in_trait)]
pub trait StatusOps: DiscordContext {
async fn set_status(&self, status: UserStatus) -> Result<()> {
if let Some(gateway) = self.gateway() {
gateway.send_presence(status).await
} else {
Err(DiscordError::NotInitialized)
}
}
async fn set_custom_status(
&self,
status: UserStatus,
custom_status_text: Option<&str>,
expires_at_ms: Option<u64>,
) -> Result<SettingsProtoResponse> {
use crate::proto::{CustomStatus, PreloadedUserSettings, StatusSettings};
let mut activities = Vec::new();
if let Some(text) = custom_status_text {
if !text.is_empty() {
let mut activity = json!({
"name": "Custom Status",
"type": 4,
"state": text,
"emoji": null
});
if let Some(expires) = expires_at_ms {
activity["timestamps"] = json!({ "end": expires });
}
activities.push(activity);
}
}
let ws_payload = json!({
"op": 3,
"d": {
"status": status.as_str(),
"since": 0,
"activities": activities,
"afk": false
}
});
if let Some(gateway) = self.gateway() {
gateway.send_raw(ws_payload).await?;
}
let mut status_settings = StatusSettings::new(status.as_str());
if let Some(text) = custom_status_text {
if !text.is_empty() {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let mut custom = CustomStatus::new(text).with_created_at(now_ms);
if let Some(expires) = expires_at_ms {
custom = custom.with_expiry(expires);
}
status_settings = status_settings.with_custom_status(custom);
}
}
status_settings = status_settings.with_show_current_game(false);
if let Some(expires) = expires_at_ms {
status_settings = status_settings.with_status_expires_at(expires);
}
let settings = PreloadedUserSettings::with_status(status_settings);
let encoded = settings.to_base64();
let raw: RawSettingsProtoResponse = self
.http()
.patch(Route::SettingsProto { version: 1 }, json!({ "settings": encoded }))
.await?;
let settings = PreloadedUserSettings::from_base64(&raw.settings)
.map_err(|e| DiscordError::Other(format!("failed to decode settings-proto response: {e}")))?;
Ok(SettingsProtoResponse {
out_of_date: raw.out_of_date,
settings,
raw_settings_b64: raw.settings,
})
}
async fn clear_custom_status(&self) -> Result<SettingsProtoResponse> {
self.set_custom_status(UserStatus::Online, None, None).await
}
async fn set_activity(&self, activity: ActivityData, status: UserStatus) -> Result<()> {
let ws_payload = json!({
"op": 3,
"d": {
"status": status.as_str(),
"since": 0,
"activities": [activity.to_json()],
"afk": false
}
});
if let Some(gateway) = self.gateway() {
gateway.send_raw(ws_payload).await?;
} else {
return Err(DiscordError::NotInitialized);
}
Ok(())
}
async fn clear_activity(&self, status: UserStatus) -> Result<()> {
let ws_payload = json!({
"op": 3,
"d": {
"status": status.as_str(),
"since": 0,
"activities": [],
"afk": false
}
});
if let Some(gateway) = self.gateway() {
gateway.send_raw(ws_payload).await?;
} else {
return Err(DiscordError::NotInitialized);
}
Ok(())
}
async fn edit_profile(&self, req: EditProfileRequest) -> Result<User> {
self.http().patch(Route::UpdateMe, req).await
}
}