use crate::{models::StatsBlockWithFrames, client_https};
use anyhow::bail;
use hyper::{Body, Client, Method, Request};
use futures::StreamExt;
use crate::ddinfo::get_os;
use super::models::OperatingSystem;
pub struct DdclSecrets {
pub iv: String,
pub pass: String,
pub salt: String,
}
#[derive(serde::Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SubmitRunRequest {
pub survival_hash_md5: String,
pub player_id: i32,
pub player_name: String,
pub time_in_seconds: f32,
pub time_as_bytes: String,
pub gems_collected: i32,
pub enemies_killed: i32,
pub daggers_fired: i32,
pub daggers_hit: i32,
pub enemies_alive: i32,
pub homing_stored: i32,
pub homing_eaten: i32,
pub gems_despawned: i32,
pub gems_eaten: i32,
pub gems_total: i32,
pub death_type: u8,
pub level_up_time2_in_seconds: f32,
pub level_up_time3_in_seconds: f32,
pub level_up_time4_in_seconds: f32,
pub level_up_time2_as_bytes: String,
pub level_up_time3_as_bytes: String,
pub level_up_time4_as_bytes: String,
pub client_version: String,
pub operating_system: OperatingSystem,
pub build_mode: String,
pub client: String,
pub validation: String,
pub validation_version: i32,
pub is_replay: bool,
pub prohibited_mods: bool,
pub game_data: GameState,
pub status: i32,
pub replay_data: String,
pub replay_player_id: i32,
pub game_mode: u8,
pub time_attack_or_race_finished: bool,
}
#[derive(serde::Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct GameState {
pub gems_collected: Vec<i32>,
pub enemies_killed: Vec<i32>,
pub daggers_fired: Vec<i32>,
pub daggers_hit: Vec<i32>,
pub enemies_alive: Vec<i32>,
pub homing_stored: Vec<i32>,
pub homing_eaten: Vec<i32>,
pub gems_despawned: Vec<i32>,
pub gems_eaten: Vec<i32>,
pub gems_total: Vec<i32>,
pub skull1s_alive: Vec<i32>,
pub skull2s_alive: Vec<i32>,
pub skull3s_alive: Vec<i32>,
pub spiderlings_alive: Vec<i32>,
pub skull4s_alive: Vec<i32>,
pub squid1s_alive: Vec<i32>,
pub squid2s_alive: Vec<i32>,
pub squid3s_alive: Vec<i32>,
pub centipedes_alive: Vec<i32>,
pub gigapedes_alive: Vec<i32>,
pub spider1s_alive: Vec<i32>,
pub spider2s_alive: Vec<i32>,
pub leviathans_alive: Vec<i32>,
pub orbs_alive: Vec<i32>,
pub thorns_alive: Vec<i32>,
pub ghostpedes_alive: Vec<i32>,
pub skull1s_killed: Vec<i32>,
pub skull2s_killed: Vec<i32>,
pub skull3s_killed: Vec<i32>,
pub spiderlings_killed: Vec<i32>,
pub skull4s_killed: Vec<i32>,
pub squid1s_killed: Vec<i32>,
pub squid2s_killed: Vec<i32>,
pub squid3s_killed: Vec<i32>,
pub centipedes_killed: Vec<i32>,
pub gigapedes_killed: Vec<i32>,
pub spider1s_killed: Vec<i32>,
pub spider2s_killed: Vec<i32>,
pub leviathans_killed: Vec<i32>,
pub orbs_killed: Vec<i32>,
pub thorns_killed: Vec<i32>,
pub ghostpedes_killed: Vec<i32>,
pub spider_eggs_alive: Vec<i32>,
pub spider_eggs_killed: Vec<i32>,
}
impl SubmitRunRequest {
pub fn from_compiled_run<T: ToString, K: ToString>(
run: std::sync::Arc<StatsBlockWithFrames>,
secrets: Option<DdclSecrets>,
client: T,
version: K,
replay_bin: std::sync::Arc<Vec<u8>>,
) -> anyhow::Result<Self> {
if secrets.is_none() {
bail!("Missing DDCL Secrets");
}
let game_data = GameState {
gems_collected: run.frames.iter().map(|f| f.gems_collected).collect(),
enemies_killed: run.frames.iter().map(|f| f.kills).collect(),
daggers_fired: run.frames.iter().map(|f| f.daggers_fired).collect(),
daggers_hit: run.frames.iter().map(|f| f.daggers_hit).collect(),
enemies_alive: run.frames.iter().map(|f| f.enemies_alive).collect(),
homing_stored: run.frames.iter().map(|f| f.homing).collect(),
homing_eaten: run.frames.iter().map(|f| f.daggers_eaten).collect(),
gems_despawned: run.frames.iter().map(|f| f.gems_despawned).collect(),
gems_eaten: run.frames.iter().map(|f| f.gems_eaten).collect(),
gems_total: run.frames.iter().map(|f| f.gems_total).collect(),
skull1s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[0] as i32).collect(),
skull2s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[1] as i32).collect(),
skull3s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[2] as i32).collect(),
spiderlings_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[3] as i32).collect(),
skull4s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[4] as i32).collect(),
squid1s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[5] as i32).collect(),
squid2s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[6] as i32).collect(),
squid3s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[7] as i32).collect(),
centipedes_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[8] as i32).collect(),
gigapedes_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[9] as i32).collect(),
spider1s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[10] as i32).collect(),
spider2s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[11] as i32).collect(),
leviathans_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[12] as i32).collect(),
orbs_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[13] as i32).collect(),
thorns_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[14] as i32).collect(),
ghostpedes_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[15] as i32).collect(),
spider_eggs_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[16] as i32).collect(),
skull1s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[0] as i32).collect(),
skull2s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[1] as i32).collect(),
skull3s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[2] as i32).collect(),
spiderlings_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[3] as i32).collect(),
skull4s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[4] as i32).collect(),
squid1s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[5] as i32).collect(),
squid2s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[6] as i32).collect(),
squid3s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[7] as i32).collect(),
centipedes_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[8] as i32).collect(),
gigapedes_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[9] as i32).collect(),
spider1s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[10] as i32).collect(),
spider2s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[11] as i32).collect(),
leviathans_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[12] as i32).collect(),
orbs_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[13] as i32).collect(),
thorns_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[14] as i32).collect(),
ghostpedes_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[15] as i32).collect(),
spider_eggs_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[16] as i32).collect(),
};
let sec = secrets.unwrap();
let last = run.frames.last().unwrap();
let to_encrypt = vec![
run.block.player_id.to_string(),
crate::utils::md5_to_string(&run.block.time.to_le_bytes()[..]),
last.gems_collected.to_string(),
last.gems_despawned.to_string(),
last.gems_eaten.to_string(),
last.gems_total.to_string(),
last.enemies_alive.to_string(),
last.kills.to_string(),
run.block.death_type.to_string(),
last.daggers_hit.to_string(),
last.daggers_fired.to_string(),
last.homing.to_string(),
last.daggers_eaten.to_string(),
if run.block.is_replay { "True".to_owned() } else { "False".to_owned() },
run.block.status.to_string(),
crate::utils::md5_to_string(&run.block.survival_md5[..]),
crate::utils::md5_to_string(&run.block.time_lvl2.to_le_bytes()[..]),
crate::utils::md5_to_string(&run.block.time_lvl3.to_le_bytes()[..]),
crate::utils::md5_to_string(&run.block.time_lvl4.to_le_bytes()[..]),
run.block.game_mode.to_string(),
if run.block.is_time_attack_or_race_finished && !cfg!(target_os = "linux") { "True".to_owned() } else { "False".to_owned() },
if run.block.prohibited_mods { "True".to_owned() } else { "False".to_owned() },
]
.join(";");
let validation = crypto_encoder::encrypt_and_encode(to_encrypt, sec.pass, sec.salt, sec.iv)?;
let replay_bin = base64::encode(&replay_bin[..]);
Ok(Self {
survival_hash_md5: base64::encode(&run.block.survival_md5),
player_id: run.block.player_id,
player_name: run.block.player_username(),
time_in_seconds: run.block.time,
time_as_bytes: base64::encode(run.block.time.to_le_bytes()),
gems_collected: last.gems_collected,
enemies_killed: last.kills,
daggers_fired: last.daggers_fired,
daggers_hit: last.daggers_hit,
enemies_alive: last.enemies_alive,
homing_stored: last.homing,
homing_eaten: last.daggers_eaten,
gems_despawned: last.gems_despawned,
gems_eaten: last.gems_eaten,
gems_total: last.gems_total,
death_type: run.block.death_type,
level_up_time2_in_seconds: run.block.time_lvl2,
level_up_time2_as_bytes: base64::encode(run.block.time_lvl2.to_le_bytes()),
level_up_time3_in_seconds: run.block.time_lvl3,
level_up_time3_as_bytes: base64::encode(run.block.time_lvl3.to_le_bytes()),
level_up_time4_in_seconds: run.block.time_lvl4,
level_up_time4_as_bytes: base64::encode(run.block.time_lvl4.to_le_bytes()),
client_version: version.to_string(),
operating_system: get_os(),
build_mode: "Release".to_owned(),
client: client.to_string(),
validation: validation.replace('=', ""),
validation_version: 2,
is_replay: run.block.is_replay,
prohibited_mods: run.block.prohibited_mods,
game_data,
status: run.block.status,
replay_data: replay_bin,
replay_player_id: run.block.replay_player_id,
game_mode: run.block.game_mode,
time_attack_or_race_finished: if cfg!(target_os = "linux") { false } else { run.block.is_time_attack_or_race_finished },
})
}
}
pub async fn submit<T: ToString, K: ToString>(
data: std::sync::Arc<StatsBlockWithFrames>,
secrets: Option<DdclSecrets>,
client: T,
version: K,
replay_bin: std::sync::Arc<Vec<u8>>
) -> anyhow::Result<()> {
if replay_bin.is_empty() {
bail!("No bytes in replay!");
}
let req = SubmitRunRequest::from_compiled_run(data, secrets, client, version, replay_bin);
if req.is_ok() {
let client: Client<_, hyper::Body> = client_https!();
let path = "api/custom-entries/submit";
let uri = format!("https://devildaggers.info/{}", path);
let req = Request::builder()
.header("content-type", "application/json")
.header("accept", "application/json")
.method(Method::POST)
.uri(uri)
.body(Body::from(serde_json::to_string(&req.unwrap())?))
.unwrap();
let mut res = client.request(req).await?;
let mut body = Vec::new();
while let Some(chunk) = res.body_mut().next().await {
body.extend_from_slice(&chunk?);
}
if res.status() != 200 {
unsafe { bail!(String::from_utf8_unchecked(body)); }
}
}
Ok(())
}
pub mod crypto_encoder {
use std::num::NonZeroU32;
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut};
use ring::pbkdf2;
use base32::Alphabet::RFC4648;
use anyhow::Result;
use aes::cipher::KeyIvInit;
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
pub fn encrypt_and_encode(plain: String, password: String, salt: String, iv: String) -> Result<String> {
let password = &password;
let mut pbkdf2_hash = [0u8; 16]; let n_iter = NonZeroU32::new(65536).unwrap();
let salt = salt.as_bytes();
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA1,
n_iter,
salt,
password.as_bytes(),
&mut pbkdf2_hash,
);
let plain = plain.as_bytes();
let mut buffer = [0_u8; 1000]; let cipher = match Aes128CbcEnc::new_from_slices(&pbkdf2_hash, iv.as_bytes()) {
Ok(v) => Ok(v),
Err(_) => Err(anyhow::anyhow!("Cipher Error")),
}?;
let pos = plain.len();
buffer[..pos].copy_from_slice(plain);
let ciphertext = match cipher.encrypt_padded_mut::<Pkcs7>(&mut buffer, pos) {
Ok(v) => Ok(v),
Err(_e) => Err(anyhow::anyhow!("Ciphertext Err")),
}?;
Ok(base32::encode(RFC4648 { padding: true }, ciphertext))
}
}