use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use chrono::Utc;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use crate::errors::ChorusResult;
use crate::gateway::{events::Events, Gateway, GatewayHandle, GatewayOptions};
use crate::ratelimiter::ChorusRequest;
use crate::types::types::subconfigs::limits::rates::RateLimits;
use crate::types::{
ClientProperties, GatewayIdentifyPayload, GeneralConfiguration, Limit, LimitType,
LimitsConfiguration, MfaToken, MfaTokenSchema, MfaVerifySchema, Shared, User, UserSettings,
};
use crate::UrlBundle;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Instance {
pub urls: UrlBundle,
pub instance_info: GeneralConfiguration,
pub(crate) software: InstanceSoftware,
pub limits_information: Option<LimitsInformation>,
#[serde(skip)]
pub client: Client,
#[serde(skip)]
pub(crate) gateway_options: GatewayOptions,
#[serde(skip)]
pub default_gateway_events: Events,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq)]
pub struct LimitsInformation {
pub ratelimits: HashMap<LimitType, Limit>,
pub configuration: RateLimits,
}
impl std::hash::Hash for LimitsInformation {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for (k, v) in self.ratelimits.iter() {
k.hash(state);
v.hash(state);
}
self.configuration.hash(state);
}
}
#[cfg(not(tarpaulin_include))]
impl PartialEq for LimitsInformation {
fn eq(&self, other: &Self) -> bool {
self.ratelimits.iter().eq(other.ratelimits.iter())
&& self.configuration == other.configuration
}
}
impl Instance {
#[allow(unused)]
pub(crate) fn clone_limits_if_some(&self) -> Option<HashMap<LimitType, Limit>> {
if self.limits_information.is_some() {
return Some(self.limits_information.as_ref().unwrap().ratelimits.clone());
}
None
}
pub async fn from_url_bundle(
urls: UrlBundle,
options: Option<GatewayOptions>,
) -> ChorusResult<Instance> {
let is_limited: Option<LimitsConfiguration> = Instance::is_limited(&urls.api).await?;
let limit_information;
if let Some(limits_configuration) = is_limited {
let limits = ChorusRequest::limits_config_to_hashmap(&limits_configuration.rate);
limit_information = Some(LimitsInformation {
ratelimits: limits,
configuration: limits_configuration.rate,
});
} else {
limit_information = None
}
let mut instance = Instance {
urls: urls.clone(),
instance_info: GeneralConfiguration::default(),
limits_information: limit_information,
client: Client::new(),
gateway_options: options.unwrap_or_default(),
software: InstanceSoftware::Other,
default_gateway_events: Events::default(),
};
instance.instance_info = match instance.general_configuration_schema().await {
Ok(schema) => schema,
Err(e) => {
log::warn!("Could not get instance configuration schema: {}", e);
GeneralConfiguration::default()
}
};
instance.software = instance.detect_software().await;
if options.is_none() {
instance.gateway_options = GatewayOptions::for_instance_software(instance.software());
}
Ok(instance)
}
pub async fn new(root_url: &str, options: Option<GatewayOptions>) -> ChorusResult<Instance> {
let urls = UrlBundle::from_root_url(root_url).await?;
Instance::from_url_bundle(urls, options).await
}
pub async fn is_limited(api_url: &str) -> ChorusResult<Option<LimitsConfiguration>> {
let api_url = UrlBundle::parse_url(api_url);
let client = Client::new();
let request = client
.get(format!("{}/policies/instance/limits", &api_url))
.header(http::header::ACCEPT, "application/json")
.build()?;
let resp = match client.execute(request).await {
Ok(response) => response,
Err(_) => return Ok(None),
};
match resp.json::<LimitsConfiguration>().await {
Ok(limits) => Ok(Some(limits)),
Err(_) => Ok(None),
}
}
pub async fn detect_software(&mut self) -> InstanceSoftware {
if let Ok(version) = self.get_version().await {
match version.server.to_lowercase().as_str() {
"symfonia" => return InstanceSoftware::Symfonia,
"spacebar" => return InstanceSoftware::SpacebarTypescript,
_ => {}
}
}
let ping = self.ping().await;
if ping.is_ok() {
return InstanceSoftware::SpacebarTypescript;
}
InstanceSoftware::Other
}
pub fn gateway_options(&self) -> GatewayOptions {
self.gateway_options
}
pub fn set_gateway_options(&mut self, options: GatewayOptions) {
self.gateway_options = options;
}
pub fn software(&self) -> InstanceSoftware {
self.software
}
pub fn set_software(&mut self, software: InstanceSoftware) {
self.software = software;
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum InstanceSoftware {
SpacebarTypescript,
Symfonia,
#[default]
Other,
}
impl InstanceSoftware {
pub fn supports_gateway_zlib(self) -> bool {
match self {
InstanceSoftware::SpacebarTypescript => true,
InstanceSoftware::Symfonia => false,
InstanceSoftware::Other => true,
}
}
pub fn supports_gateway_etf(self) -> bool {
match self {
InstanceSoftware::SpacebarTypescript => true,
InstanceSoftware::Symfonia => false,
InstanceSoftware::Other => true,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Token {
pub token: String,
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.token)
}
}
#[derive(Debug, Clone)]
pub struct ChorusUser {
pub belongs_to: Shared<Instance>,
pub token: String,
pub client_properties: ClientProperties,
pub mfa_token: Option<MfaToken>,
pub limits: Option<HashMap<LimitType, Limit>>,
pub settings: Shared<UserSettings>,
pub object: Shared<User>,
pub gateway: GatewayHandle,
}
impl ChorusUser {
pub fn token(&self) -> String {
self.token.clone()
}
pub fn set_token(&mut self, token: &str) {
self.token = token.to_string();
}
pub fn new(
belongs_to: Shared<Instance>,
token: String,
client_properties: ClientProperties,
limits: Option<HashMap<LimitType, Limit>>,
settings: Shared<UserSettings>,
object: Shared<User>,
gateway: GatewayHandle,
) -> ChorusUser {
ChorusUser {
belongs_to,
token,
client_properties,
mfa_token: None,
limits,
settings,
object,
gateway,
}
}
pub(crate) async fn update_with_login_data(
&mut self,
token: String,
received_settings: Option<Shared<UserSettings>>,
) -> ChorusResult<()> {
self.token = token.clone();
let instance_default_events = self
.belongs_to
.read()
.unwrap()
.default_gateway_events
.clone();
*self.gateway.events.lock().await = instance_default_events;
let mut identify = GatewayIdentifyPayload::default_w_client_capabilities();
identify.token = token;
identify.properties = self.client_properties.clone();
self.gateway.send_identify(identify).await;
*self.object.write().unwrap() = self.get_current_user().await?;
if let Some(passed_settings) = received_settings {
self.settings = passed_settings;
} else {
*self.settings.write().unwrap() = self.get_settings().await?;
}
Ok(())
}
pub(crate) async fn shell(instance: Shared<Instance>, token: &str) -> ChorusUser {
let settings = Arc::new(RwLock::new(UserSettings::default()));
let object = Arc::new(RwLock::new(User::default()));
let wss_url = &instance.read().unwrap().urls.wss.clone();
let gateway_options = instance.read().unwrap().gateway_options;
let gateway = Gateway::spawn(wss_url, gateway_options).await.unwrap();
ChorusUser {
token: token.to_string(),
client_properties: ClientProperties::default(),
mfa_token: None,
belongs_to: instance.clone(),
limits: instance
.read()
.unwrap()
.limits_information
.as_ref()
.map(|info| info.ratelimits.clone()),
settings,
object,
gateway,
}
}
pub async fn complete_mfa_challenge(
&mut self,
mfa_verify_schema: MfaVerifySchema,
) -> ChorusResult<()> {
let endpoint_url = self.belongs_to.read().unwrap().urls.api.clone() + "/mfa/finish";
let chorus_request = ChorusRequest {
request: Client::new().post(endpoint_url).json(&mfa_verify_schema),
limit_type: LimitType::Global,
}
.with_headers_for(self);
let mfa_token_schema = chorus_request
.send_and_deserialize_response::<MfaTokenSchema>(self)
.await?;
self.mfa_token = Some(MfaToken {
token: mfa_token_schema.token,
expires_at: Utc::now() + Duration::from_secs(60 * 5),
});
Ok(())
}
}