use crate::model::{
fields::AnimeFields,
options::{Params, RankingType, Season, StatusUpdate},
AnimeDetails, AnimeList, ForumBoards, ForumTopics, ListStatus, TopicDetails, User,
};
use rand::random;
use reqwest::Client;
use reqwest::{Method, StatusCode};
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
use simple_log::{debug, info};
use std::{fs::File, io::Write, path::PathBuf, str, time::SystemTime};
use tiny_http::{Response, Server};
use crate::MALError;
use aes_gcm::aead::{Aead, NewAead};
use aes_gcm::{Aes256Gcm, Key, Nonce};
pub struct MALClient {
client_secret: String,
dirs: PathBuf,
access_token: String,
client: reqwest::Client,
caching: bool,
pub need_auth: bool,
}
impl MALClient {
pub fn new(
client_secret: String,
dirs: PathBuf,
access_token: String,
client: Client,
caching: bool,
need_auth: bool,
) -> Self {
MALClient {
client_secret,
dirs,
access_token,
caching,
need_auth,
client,
}
}
pub fn with_access_token(token: &str) -> Self {
MALClient {
client_secret: String::new(),
need_auth: false,
dirs: PathBuf::new(),
access_token: token.to_owned(),
client: reqwest::Client::new(),
caching: false,
}
}
pub fn set_cache_dir(&mut self, dir: PathBuf) {
self.dirs = dir;
}
pub fn set_caching(&mut self, caching: bool) {
self.caching = caching;
}
pub fn get_auth_parts(&self) -> (String, String, String) {
let verifier = pkce::code_verifier(128);
let challenge = pkce::code_challenge(&verifier);
let state = format!("bruh{}", random::<u8>());
let url = format!("https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id={}&code_challenge={}&state={}", self.client_secret, challenge, state, );
(url, challenge, state)
}
pub async fn auth(
&mut self,
callback_url: &str,
challenge: &str,
state: &str,
) -> Result<(), MALError> {
let mut code = "".to_owned();
let url = if callback_url.contains("http") {
callback_url
.trim_start_matches("http://")
.trim_start_matches("https://")
} else {
callback_url
};
let server = Server::http(url).unwrap();
for i in server.incoming_requests() {
if !i.url().contains(&format!("state={}", state)) {
continue;
}
let res_raw = i.url();
debug!("raw response: {}", res_raw);
code = res_raw
.split_once('=')
.unwrap()
.1
.split_once('&')
.unwrap()
.0
.to_owned();
let response = Response::from_string("You're logged in! You can now close this window");
i.respond(response).unwrap();
break;
}
self.get_tokens(&code, challenge).await
}
async fn get_tokens(&mut self, code: &str, verifier: &str) -> Result<(), MALError> {
let params = [
("client_id", self.client_secret.as_str()),
("grant_type", "authorization_code"),
("code_verifier", verifier),
("code", code),
];
let rec = self
.client
.request(Method::POST, "https://myanimelist.net/v1/oauth2/token")
.form(¶ms)
.build()
.unwrap();
let res = self.client.execute(rec).await.unwrap();
let text = res.text().await.unwrap();
if let Ok(tokens) = serde_json::from_str::<TokenResponse>(&text) {
self.access_token = tokens.access_token.clone();
let tjson = Tokens {
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_in: tokens.expires_in,
today: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs(),
};
if self.caching {
let mut f =
File::create(self.dirs.join("tokens")).expect("Unable to create token file");
f.write_all(&encrypt_token(tjson))
.expect("Unable to write tokens");
}
Ok(())
} else {
Err(MALError::new("Unable to get tokens", "None", text))
}
}
async fn do_request(&self, url: String) -> Result<String, MALError> {
match self
.client
.get(url)
.bearer_auth(&self.access_token)
.send()
.await
{
Ok(res) => Ok(res.text().await.unwrap()),
Err(e) => Err(MALError::new(
"Unable to send request",
&format!("{}", e),
None,
)),
}
}
async fn do_request_forms(
&self,
url: String,
params: Vec<(&str, String)>,
) -> Result<String, MALError> {
match self
.client
.put(url)
.bearer_auth(&self.access_token)
.form(¶ms)
.send()
.await
{
Ok(res) => Ok(res.text().await.unwrap()),
Err(e) => Err(MALError::new(
"Unable to send request",
&format!("{}", e),
None,
)),
}
}
fn parse_response<'a, T: Serialize + Deserialize<'a>>(
&self,
res: &'a str,
) -> Result<T, MALError> {
match serde_json::from_str::<T>(res) {
Ok(v) => Ok(v),
Err(_) => Err(match serde_json::from_str::<MALError>(res) {
Ok(o) => o,
Err(e) => MALError::new(
"unable to parse response",
&format!("{}", e),
res.to_string(),
),
}),
}
}
pub fn get_access_token(&self) -> &str {
&self.access_token
}
pub async fn get_anime_list(
&self,
query: &str,
limit: impl Into<Option<u8>>,
) -> Result<AnimeList, MALError> {
let url = format!(
"https://api.myanimelist.net/v2/anime?q={}&limit={}",
query,
limit.into().unwrap_or(100)
);
let res = self.do_request(url).await?;
self.parse_response(&res)
}
pub async fn get_anime_details(
&self,
id: u32,
fields: impl Into<Option<AnimeFields>>,
) -> Result<AnimeDetails, MALError> {
let url = if let Some(f) = fields.into() {
format!("https://api.myanimelist.net/v2/anime/{}?fields={}", id, f)
} else {
format!(
"https://api.myanimelist.net/v2/anime/{}?fields={}",
id,
AnimeFields::ALL
)
};
let res = self.do_request(url).await?;
self.parse_response(&res)
}
pub async fn get_anime_ranking(
&self,
ranking_type: RankingType,
limit: impl Into<Option<u8>>,
) -> Result<AnimeList, MALError> {
let url = format!(
"https://api.myanimelist.net/v2/anime/ranking?ranking_type={}&limit={}",
ranking_type,
limit.into().unwrap_or(100)
);
let res = self.do_request(url).await?;
Ok(serde_json::from_str(&res).unwrap())
}
pub async fn get_seasonal_anime(
&self,
season: Season,
year: u32,
limit: impl Into<Option<u8>>,
) -> Result<AnimeList, MALError> {
let url = format!(
"https://api.myanimelist.net/v2/anime/season/{}/{}?limit={}",
year,
season,
limit.into().unwrap_or(100)
);
let res = self.do_request(url).await?;
self.parse_response(&res)
}
pub async fn get_suggested_anime(
&self,
limit: impl Into<Option<u8>>,
) -> Result<AnimeList, MALError> {
let url = format!(
"https://api.myanimelist.net/v2/anime/suggestions?limit={}",
limit.into().unwrap_or(100)
);
let res = self.do_request(url).await?;
self.parse_response(&res)
}
pub async fn update_user_anime_status(
&self,
id: u32,
update: StatusUpdate,
) -> Result<ListStatus, MALError> {
let params = update.get_params();
let url = format!("https://api.myanimelist.net/v2/anime/{}/my_list_status", id);
let res = self.do_request_forms(url, params).await?;
self.parse_response(&res)
}
pub async fn get_user_anime_list(&self) -> Result<AnimeList, MALError> {
let url = "https://api.myanimelist.net/v2/users/@me/animelist?fields=list_status&limit=4";
let res = self.do_request(url.to_owned()).await?;
self.parse_response(&res)
}
pub async fn delete_anime_list_item(&self, id: u32) -> Result<(), MALError> {
let url = format!("https://api.myanimelist.net/v2/anime/{}/my_list_status", id);
let res = self
.client
.delete(url)
.bearer_auth(&self.access_token)
.send()
.await;
match res {
Ok(r) => {
if r.status() == StatusCode::NOT_FOUND {
Err(MALError::new(
&format!("Anime {} not found", id),
r.status().as_str(),
None,
))
} else {
Ok(())
}
}
Err(e) => Err(MALError::new(
"Unable to send request",
&format!("{}", e),
None,
)),
}
}
pub async fn get_forum_boards(&self) -> Result<ForumBoards, MALError> {
let res = self
.do_request("https://api.myanimelist.net/v2/forum/boards".to_owned())
.await?;
self.parse_response(&res)
}
pub async fn get_forum_topic_detail(
&self,
topic_id: u32,
limit: impl Into<Option<u8>>,
) -> Result<TopicDetails, MALError> {
let url = format!(
"https://api.myanimelist.net/v2/forum/topic/{}?limit={}",
topic_id,
limit.into().unwrap_or(100)
);
let res = self.do_request(url).await?;
self.parse_response(&res)
}
pub async fn get_forum_topics(
&self,
board_id: impl Into<Option<u32>>,
subboard_id: impl Into<Option<u32>>,
query: impl Into<Option<String>>,
topic_user_name: impl Into<Option<String>>,
user_name: impl Into<Option<String>>,
limit: impl Into<Option<u32>>,
) -> Result<ForumTopics, MALError> {
let params = {
let mut tmp = vec![];
if let Some(bid) = board_id.into() {
tmp.push(format!("board_id={}", bid));
}
if let Some(bid) = subboard_id.into() {
tmp.push(format!("subboard_id={}", bid));
}
if let Some(bid) = query.into() {
tmp.push(format!("q={}", bid));
}
if let Some(bid) = topic_user_name.into() {
tmp.push(format!("topic_user_name={}", bid));
}
if let Some(bid) = user_name.into() {
tmp.push(format!("user_name={}", bid));
}
tmp.push(format!("limit={}", limit.into().unwrap_or(100)));
tmp.join(",")
};
let url = format!("https://api.myanimelist.net/v2/forum/topics?{}", params);
let res = self.do_request(url).await?;
self.parse_response(&res)
}
pub async fn get_my_user_info(&self) -> Result<User, MALError> {
let url = "https://api.myanimelist.net/v2/users/@me?fields=anime_statistics";
let res = self.do_request(url.to_owned()).await?;
self.parse_response(&res)
}
}
#[derive(Deserialize)]
pub(crate) struct TokenResponse {
pub _token_type: String,
pub expires_in: u32,
pub access_token: String,
pub refresh_token: String,
}
#[derive(Serialize, Deserialize)]
pub(crate) struct Tokens {
pub access_token: String,
pub refresh_token: String,
pub expires_in: u32,
pub today: u64,
}
pub(crate) fn encrypt_token(toks: Tokens) -> Vec<u8> {
let key = Key::from_slice(b"one two three four five six seve");
let cypher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(b"but the eart");
let plain = serde_json::to_vec(&toks).unwrap();
let res = cypher.encrypt(nonce, plain.as_ref()).unwrap();
res
}
pub(crate) fn decrypt_tokens(raw: &[u8]) -> Result<Tokens, MALError> {
let key = Key::from_slice(b"one two three four five six seve");
let cypher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(b"but the eart");
match cypher.decrypt(nonce, raw.as_ref()) {
Ok(plain) => {
let text = String::from_utf8(plain).unwrap();
Ok(serde_json::from_str(&text).expect("couldn't parse decrypted tokens"))
}
Err(e) => Err(MALError::new(
"Unable to decrypt encrypted tokens",
&format!("{}", e),
None,
)),
}
}