pub mod errors;
use crate::errors::error_to_pishock_error;
use log::{debug, info};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::time::Duration;
pub struct PiShockAccount {
app_name: String,
api_username: String,
api_key: String,
}
impl PiShockAccount {
#[must_use]
pub fn new<S: Into<String>>(api_name: S, api_username: S, api_key: S) -> PiShockAccount {
PiShockAccount {
app_name: api_name.into(),
api_username: api_username.into(),
api_key: api_key.into(),
}
}
pub async fn get_shocker<S: Into<String>>(
&self,
share_code: S,
verify_connection: bool,
) -> Result<PiShocker, errors::PiShockError> {
let pishock_instance = PiShocker::new(
share_code.into(),
self.api_key.clone(),
self.api_username.clone(),
self.app_name.clone(),
);
if verify_connection {
pishock_instance.vibrate(20, Duration::from_secs(1)).await?;
info!("Successfully connected to shocker, connection verified");
}
Ok(pishock_instance)
}
}
#[derive(Clone, Copy)]
enum PiShockOpCode {
Shock = 0,
Vibrate = 1,
Beep = 2,
}
static PUBLIC_PISHOCK_API_URL: &str = "https://do.pishock.com/api/apioperate/";
#[derive(Debug, Clone)]
pub struct PiShocker {
share_code: String,
http_client: reqwest::Client,
api_key: String,
api_username: String,
app_name: String,
api_server_url: String,
}
impl PiShocker {
#[must_use]
pub fn new<S: Into<String>>(
share_code: S,
api_key: S,
api_username: S,
app_name: S,
) -> PiShocker {
PiShocker {
share_code: share_code.into(),
http_client: reqwest::Client::new(),
api_key: api_key.into(),
api_username: api_username.into(),
app_name: app_name.into(),
api_server_url: PUBLIC_PISHOCK_API_URL.to_string(),
}
}
#[cfg(test)]
pub(crate) fn set_api_server_url<S: Into<String>>(&mut self, api_server_url: S) {
self.api_server_url = api_server_url.into();
}
#[must_use]
pub fn get_share_code(&self) -> String {
self.share_code.clone()
}
pub async fn beep(&self, duration: Duration) -> Result<(), errors::PiShockError> {
debug!("Beeping user for {} seconds", duration.as_secs());
self.api_request(PiShockOpCode::Beep, 0, duration.as_secs() as u32)
.await?;
Ok(())
}
pub async fn vibrate(
&self,
intensity: u32,
duration: Duration,
) -> Result<(), errors::PiShockError> {
if !(1..=100).contains(&intensity) {
return Err(errors::PiShockError::InvalidIntensity(100));
}
info!(
"Vibrating user with intensity {} and duration {} seconds",
intensity,
duration.as_secs()
);
self.api_request(PiShockOpCode::Vibrate, intensity, duration.as_secs() as u32)
.await?;
Ok(())
}
pub async fn shock(
&self,
intensity: u32,
duration: Duration,
) -> Result<(), errors::PiShockError> {
if duration.as_secs() < 1 || duration.as_secs() > 15 {
return Err(errors::PiShockError::InvalidDuration(
duration.as_secs() as u32
));
}
if !(1..=100).contains(&intensity) {
return Err(errors::PiShockError::InvalidIntensity(100));
}
info!(
"Shocking user with intensity {} and duration {} seconds",
intensity,
duration.as_secs()
);
self.api_request(PiShockOpCode::Shock, intensity, duration.as_secs() as u32)
.await?;
Ok(())
}
pub async fn shock_with_warning(
&self,
intensity: u32,
duration: Duration,
) -> Result<(), errors::PiShockError> {
debug!("Sending warning vibration");
self.vibrate(20, Duration::from_secs(1)).await?;
tokio::time::sleep(Duration::from_millis(200)).await; debug!("Sending shock");
self.shock(intensity, duration).await?;
Ok(())
}
async fn api_request(
&self,
op_code: PiShockOpCode,
intensity: u32,
duration: u32,
) -> Result<(), errors::PiShockError> {
#[derive(Serialize, Deserialize)]
struct PiShockAPIRequest {
#[serde(rename(serialize = "Op"))]
op: u32,
#[serde(rename(serialize = "Intensity"))]
intensity: u32,
#[serde(rename(serialize = "Duration"))]
duration: u32,
#[serde(rename(serialize = "Code"))]
sharecode: String,
#[serde(rename(serialize = "Apikey"))]
api_key: String,
#[serde(rename(serialize = "Name"))]
app_name: String,
#[serde(rename(serialize = "Username"))]
username: String,
}
debug!("Sending request to PiShock API: {{ Op: {}, Intensity: {}, Duration: {}, Code: {}, Apikey: {} }}", op_code as u32, intensity, duration, self.share_code, self.api_key);
let http_response = self
.http_client
.post(self.api_server_url.clone())
.json(&PiShockAPIRequest {
op: op_code as u32,
intensity,
duration,
sharecode: self.share_code.clone(),
api_key: self.api_key.clone(),
app_name: self.app_name.clone(),
username: self.api_username.clone(),
})
.send()
.await;
return if let Ok(response) = http_response {
debug!("Response from PiShock API: {}", response.status());
let response_text = response.text().await;
if let Ok(response_text) = response_text {
error_to_pishock_error(response_text)
} else {
Err(errors::PiShockError::ConnectionError(
response_text.unwrap_err().to_string(),
))
}
} else {
let response_code = http_response.unwrap_err().status();
if response_code.is_some() {
return Err(errors::PiShockError::ConnectionError(format!(
"Failed to connect to {}, response code: {}",
self.api_server_url,
response_code.unwrap_or(StatusCode::IM_A_TEAPOT)
)));
}
Err(errors::PiShockError::ConnectionError(format!(
"Failed to connect to {}",
self.api_server_url
)))
};
}
}
#[cfg(test)]
mod tests {
use crate::{PiShockAccount, PiShockOpCode};
use httpmock::Method::POST;
use httpmock::{Mock, MockServer};
use serde_json::json;
use test_log::test;
fn successful_server_opcode_mock(opcode: PiShockOpCode, mock_server: &MockServer) -> Mock {
mock_server.mock(|when, then| {
when.method(POST)
.path("/")
.header("Content-Type", "application/json")
.json_body(json!({
"Op": opcode as u32,
"Intensity": 50,
"Duration": 2,
"Code": "sharecode",
"Apikey": "apikey",
"Name": "pishock_rs",
"Username": "username"
}));
then.status(200).body("Operation Succeeded.");
})
}
macro_rules! successful_opcode_tests {
($($name:ident: $value:expr,)*) => {
$(
#[test(tokio::test)]
async fn $name() {
let mockserver = httpmock::MockServer::start();
let pishock_account = PiShockAccount::new("pishock_rs", "username", "apikey");
let mut pishocker_instance = pishock_account.get_shocker("sharecode".to_string(), false).await.unwrap();
pishocker_instance.set_api_server_url(mockserver.url(""));
let mock = successful_server_opcode_mock($value, &mockserver);
pishocker_instance.api_request($value, 50, 2).await.expect("Failed to send opcode");
mock.assert();
}
)*
}
}
successful_opcode_tests! {
test_vibrate: PiShockOpCode::Vibrate,
test_shock: PiShockOpCode::Shock,
test_beep: PiShockOpCode::Beep,
}
}