use std::future::Future;
use std::pin::Pin;
use rand::distributions::Alphanumeric;
use rand::Rng;
use serde::de::DeserializeOwned;
use crate::metas::meta::*;
use crate::private_model::{GenerateNewThread, SendMessage};
use crate::types::PSNFuture;
pub trait PSNRequest: Sized + Send + Sync + EncodeUrl + 'static {
type Client: Send + Sync + Clone;
type Error;
fn auth(
mut self,
client: Self::Client,
) -> Pin<Box<dyn Future<Output = Result<Self, Self::Error>> + Send>> {
Box::pin(async move {
if self.gen_access_and_refresh(&client).await.is_err() {
self.gen_access_from_refresh(&client).await?;
}
Ok(self)
})
}
fn gen_access_and_refresh<'se>(
&'se mut self,
client: &'se Self::Client,
) -> PSNFuture<'se, Result<(), Self::Error>>;
fn gen_access_from_refresh<'se>(
&'se mut self,
client: &'se Self::Client,
) -> PSNFuture<'se, Result<(), Self::Error>>;
fn get_by_url_encode<'se, 'st: 'se, T: DeserializeOwned + 'static>(
&'se self,
client: &'se Self::Client,
url: &'st str,
) -> PSNFuture<'se, Result<T, Self::Error>>;
fn del_by_url_encode<'se, 'st: 'se>(
&'se self,
client: &'se Self::Client,
url: &'st str,
) -> PSNFuture<'se, Result<(), Self::Error>>;
fn post_by_multipart<'se, 'st: 'se>(
&'se self,
client: &'se Self::Client,
boundary: &'st str,
url: &'st str,
body: Vec<u8>,
) -> PSNFuture<'se, Result<(), Self::Error>>;
fn get_profile<'se, 'st: 'se, T: DeserializeOwned + 'static>(
&'se self,
client: &'se Self::Client,
online_id: &'st str,
) -> PSNFuture<'se, Result<T, Self::Error>> {
Box::pin(async move {
let url = self.profile_encode(online_id);
self.get_by_url_encode(client, url.as_str()).await
})
}
fn get_titles<'se, 'st: 'se, T: DeserializeOwned + 'static>(
&'se self,
client: &'se Self::Client,
online_id: &'st str,
offset: u32,
) -> PSNFuture<'se, Result<T, Self::Error>> {
Box::pin(async move {
let url = self.trophy_summary_encode(online_id, offset);
self.get_by_url_encode(client, url.as_str()).await
})
}
fn get_trophy_set<'se, 'st: 'se, T: DeserializeOwned + 'static>(
&'se self,
client: &'se Self::Client,
online_id: &'st str,
np_communication_id: &'st str,
) -> PSNFuture<'se, Result<T, Self::Error>> {
Box::pin(async move {
let url = self.trophy_set_encode(online_id, np_communication_id);
self.get_by_url_encode(client, url.as_str()).await
})
}
fn get_message_threads<'a, T: DeserializeOwned + 'static>(
&'a self,
client: &'a Self::Client,
offset: u32,
) -> PSNFuture<Result<T, Self::Error>> {
Box::pin(async move {
let url = self.message_threads_encode(offset);
self.get_by_url_encode(client, url.as_str()).await
})
}
fn get_message_thread<'se, 'st: 'se, T: DeserializeOwned + 'static>(
&'se self,
client: &'se Self::Client,
thread_id: &'st str,
) -> PSNFuture<'se, Result<T, Self::Error>> {
Box::pin(async move {
let url = self.message_thread_encode(thread_id);
self.get_by_url_encode(client, url.as_str()).await
})
}
fn generate_message_thread<'se, 'st: 'se>(
&'se self,
client: &'se Self::Client,
online_id: &'st str,
) -> PSNFuture<'se, Result<(), Self::Error>> {
Box::pin(async move {
let boundary = Self::generate_boundary();
let body = self
.multipart_body(boundary.as_str(), online_id, None, None)
.await?;
let url = self.generate_thread_encode();
self.post_by_multipart(client, boundary.as_str(), url.as_str(), body)
.await
})
}
fn leave_message_thread<'se, 'st: 'se>(
&'se self,
client: &'se Self::Client,
thread_id: &'st str,
) -> PSNFuture<'se, Result<(), Self::Error>> {
Box::pin(async move {
let url = self.leave_message_thread_encode(thread_id);
self.del_by_url_encode(client, url.as_str()).await
})
}
fn send_message<'se, 'st: 'se>(
&'se self,
client: &'se Self::Client,
online_id: &'st str,
msg: Option<&'st str>,
path: Option<&'st str>,
thread_id: &'st str,
) -> PSNFuture<'se, Result<(), Self::Error>> {
Box::pin(async move {
let boundary = Self::generate_boundary();
let url = self.send_message_encode(thread_id);
let body = self.multipart_body(&boundary, online_id, msg, path).await?;
self.post_by_multipart(client, boundary.as_str(), url.as_str(), body)
.await
})
}
fn search_store_items<'se, 'st: 'se, T: DeserializeOwned + 'static>(
&'se self,
client: &'se Self::Client,
lang: &'st str,
region: &'st str,
age: &'st str,
name: &'st str,
) -> PSNFuture<'se, Result<T, Self::Error>> {
Box::pin(async move {
let url = Self::store_search_encode(lang, region, age, name);
self.get_by_url_encode(client, url.as_str()).await
})
}
fn multipart_body<'se, 'st: 'se>(
&'se self,
boundary: &'st str,
online_id: &'st str,
msg: Option<&'st str>,
path: Option<&'st str>,
) -> PSNFuture<'se, Result<Vec<u8>, Self::Error>> {
Box::pin(async move {
let mut result: Vec<u8> = Vec::new();
if msg.is_none() && path.is_none() {
let msg = serde_json::to_string(&GenerateNewThread::new(
online_id,
self.self_online_id(),
))
.unwrap_or_else(|_| "".to_owned());
write_string(&mut result, boundary, "threadDetail", msg.as_str());
return Ok(result);
};
let event_category = if path.is_some() { 3u8 } else { 1 };
let msg = serde_json::to_string(&SendMessage::new(msg, event_category))
.unwrap_or_else(|_| "".to_owned());
write_string(&mut result, boundary, "messageEventDetail", msg.as_str());
if let Some(path) = path {
let file_data = Self::read_path(path).await?;
result.extend_from_slice(b"Content-Disposition: form-data; name=\"imageData\"\r\n");
result.extend_from_slice(b"Content-Type: image/png\r\n");
result.extend_from_slice(
format!("Content-Length: {}\r\n\r\n", file_data.len()).as_bytes(),
);
result.extend_from_slice(&file_data);
result.extend_from_slice(format!("\r\n--{}\r\n", boundary).as_bytes());
}
Ok(result)
})
}
fn read_path(path: &str) -> PSNFuture<Result<Vec<u8>, Self::Error>>;
}
fn write_string(result: &mut Vec<u8>, boundary: &str, name: &str, msg: &str) {
result.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
result.extend_from_slice(
format!("Content-Disposition: form-data; name=\"{}\"\r\n", name).as_bytes(),
);
result.extend_from_slice(b"Content-Type: application/json; charset=utf-8\r\n\r\n");
result.extend_from_slice(format!("{}\r\n", msg).as_bytes());
result.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
}
pub trait EncodeUrl {
fn npsso(&self) -> Option<&str>;
fn access_token(&self) -> Option<&str>;
fn refresh_token(&self) -> &str;
fn region(&self) -> &str;
fn self_online_id(&self) -> &str;
fn language(&self) -> &str;
fn oauth_token_encode() -> [(&'static str, &'static str); 4] {
[
("client_id", CLIENT_ID),
("client_secret", CLIENT_SECRET),
("scope", SCOPE),
("grant_type", "sso_cookie"),
]
}
fn oauth_token_refresh_encode(&self) -> [(&'static str, &str); 7] {
[
("app_context", "inapp_ios"),
("client_id", CLIENT_ID),
("client_secret", CLIENT_SECRET),
("duid", DUID),
("scope", SCOPE),
("refresh_token", self.refresh_token()),
("grant_type", "refresh_token"),
]
}
fn profile_encode(&self, online_id: &str) -> String {
format!(
"https://{}{}{}/profile?fields=%40default,relation,requestMessageFlag,presence,%40personalDetail,trophySummary",
self.region(),
USERS_ENTRY,
online_id
)
}
fn trophy_summary_encode(&self, online_id: &str, offset: u32) -> String {
format!(
"https://{}{}?fields=%40default&npLanguage={}&iconSize=m&platform=PS3,PSVITA,PS4&offset={}&limit=100&comparedUser={}",
self.region(),
USER_TROPHY_ENTRY,
self.language(),
offset,
online_id
)
}
fn trophy_set_encode(&self, online_id: &str, np_communication_id: &str) -> String {
format!(
"https://{}{}{}/trophyGroups/all/trophies?fields=%40default,trophyRare,trophyEarnedRate&npLanguage={}&comparedUser={}",
self.region(),
USER_TROPHY_ENTRY,
np_communication_id,
self.language(),
online_id
)
}
fn message_threads_encode(&self, offset: u32) -> String {
format!(
"https://{}{}?offset={}",
self.region(),
MESSAGE_THREAD_ENTRY,
offset
)
}
fn message_thread_encode(&self, thread_id: &str) -> String {
format!(
"https://{}{}/{}?fields=threadMembers,threadNameDetail,threadThumbnailDetail,threadProperty,latestTakedownEventDetail,newArrivalEventDetail,threadEvents&count=100",
self.region(),
MESSAGE_THREAD_ENTRY,
thread_id
)
}
fn generate_thread_encode(&self) -> String {
format!("https://{}{}/", self.region(), MESSAGE_THREAD_ENTRY)
}
fn leave_message_thread_encode(&self, thread_id: &str) -> String {
format!(
"https://{}{}/{}/users/me",
self.region(),
MESSAGE_THREAD_ENTRY,
thread_id
)
}
fn send_message_encode(&self, thread_id: &str) -> String {
format!(
"https://{}{}/{}/messages",
self.region(),
MESSAGE_THREAD_ENTRY,
thread_id
)
}
fn store_search_encode(lang: &str, region: &str, age: &str, name: &str) -> String {
let name = name.replace(" ", "+");
format!(
"{}{}/{}/{}/tumbler-search/{}?suggested_size=999&mode=game",
STORE_ENTRY, lang, region, age, name
)
}
fn store_item_encode(lang: &str, region: &str, age: &str, game_id: &str) -> String {
format!(
"{}{}/{}/{}/resolve/{}",
STORE_ENTRY, lang, region, age, game_id
)
}
fn generate_boundary() -> String {
let mut boundary = String::with_capacity(50);
boundary.push_str("--------------------------");
let s: String = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(24)
.collect();
boundary.push_str(s.as_str());
boundary
}
}