submissions-api-rs: v0.2.0 split internal & external

This commit is contained in:
Quaternions 2024-12-15 00:55:11 -08:00
parent 33d272ab04
commit cf1fdb4099
13 changed files with 208 additions and 170 deletions

View File

@ -952,7 +952,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "submissions-api" name = "submissions-api"
version = "0.1.1" version = "0.2.0"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"serde", "serde",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "submissions-api" name = "submissions-api"
version = "0.1.1" version = "0.2.0"
edition = "2021" edition = "2021"
publish = ["strafesnet"] publish = ["strafesnet"]
repository = "https://git.itzana.me/StrafesNET/maps-service" repository = "https://git.itzana.me/StrafesNET/maps-service"
@ -15,3 +15,6 @@ reqwest = { version = "0", features = ["json"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
url = "2" url = "2"
[features]
internal = []

View File

@ -0,0 +1,23 @@
#[derive(Clone)]
pub struct Context{
pub base_url:String,
client:reqwest::Client,
}
impl Context{
pub fn new(mut base_url:String)->reqwest::Result<Self>{
base_url+="/v1";
Ok(Self{
base_url,
client:reqwest::Client::new(),
})
}
pub async fn get(&self,url:impl reqwest::IntoUrl)->Result<reqwest::Response,reqwest::Error>{
self.client.get(url)
.send().await
}
pub async fn post(&self,url:impl reqwest::IntoUrl)->Result<reqwest::Response,reqwest::Error>{
self.client.post(url)
.send().await
}
}

View File

@ -0,0 +1,86 @@
use crate::Error;
#[derive(serde::Deserialize)]
pub struct ScriptID(pub(crate) i64);
#[allow(nonstandard_style)]
pub struct GetScriptRequest{
pub ScriptID:ScriptID,
}
#[allow(nonstandard_style)]
#[derive(serde::Deserialize)]
pub struct ScriptResponse{
pub ID:i64,
pub Hash:String,
pub Source:String,
pub SubmissionID:i64,
}
#[derive(serde::Deserialize)]
#[repr(i32)]
pub enum Policy{
None=0, // not yet reviewed
Allowed=1,
Blocked=2,
Delete=3,
Replace=4,
}
pub struct ScriptPolicyHashRequest{
pub hash:String,
}
#[allow(nonstandard_style)]
#[derive(serde::Deserialize)]
pub struct ScriptPolicyResponse{
pub ID:i64,
pub FromScriptHash:String,
pub ToScriptID:ScriptID,
pub Policy:Policy
}
#[allow(nonstandard_style)]
pub struct UpdateSubmissionModelRequest{
pub SubmissionID:i64,
pub ModelID:u64,
pub ModelVersion:u64,
}
#[derive(Clone)]
pub struct Context(crate::context::Context);
impl Context{
pub fn new(base_url:String)->reqwest::Result<Self>{
Ok(Self(crate::context::Context::new(base_url)?))
}
pub async fn get_script(&self,config:GetScriptRequest)->Result<ScriptResponse,Error>{
let url_raw=format!("{}/scripts/{}",self.0.base_url,config.ScriptID.0);
let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
self.0.get(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?
.json().await.map_err(Error::Reqwest)
}
pub async fn get_script_policy_from_hash(&self,config:ScriptPolicyHashRequest)->Result<ScriptPolicyResponse,Error>{
let url_raw=format!("{}/script-policy/hash/{}",self.0.base_url,config.hash);
let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
self.0.get(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?
.json().await.map_err(Error::Reqwest)
}
pub async fn update_submission_model(&self,config:UpdateSubmissionModelRequest)->Result<(),Error>{
let url_raw=format!("{}/submissions/{}/model",self.0.base_url,config.SubmissionID);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
{
url.query_pairs_mut()
.append_pair("ModelID",config.ModelID.to_string().as_str())
.append_pair("ModelVersion",config.ModelVersion.to_string().as_str());
}
self.0.post(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?;
Ok(())
}
}

View File

@ -0,0 +1,48 @@
use crate::Error;
#[allow(nonstandard_style)]
pub struct ActionSubmissionUploadedRequest{
pub SubmissionID:i64,
pub TargetAssetID:Option<u64>,
}
pub struct SubmissionID(pub i64);
#[derive(Clone)]
pub struct Context(crate::context::Context);
// there are lots of action endpoints and they all follow the same pattern
macro_rules! action{
($fname:ident,$action:expr)=>{
pub async fn $fname(&self,config:SubmissionID)->Result<(),Error>{
let url_raw=format!(concat!("{}/submissions/{}/status/",$action),self.0.base_url,config.0);
let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
self.0.post(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?;
Ok(())
}
};
}
impl Context{
pub fn new(base_url:String)->reqwest::Result<Self>{
Ok(Self(crate::context::Context::new(base_url)?))
}
pub async fn action_submission_uploaded(&self,config:ActionSubmissionUploadedRequest)->Result<(),Error>{
let url_raw=format!("{}/submissions/{}/status/uploaded",self.0.base_url,config.SubmissionID);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
if let Some(target_asset_id)=config.TargetAssetID{
url.query_pairs_mut()
.append_pair("TargetAssetID",target_asset_id.to_string().as_str());
}
self.0.post(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?;
Ok(())
}
action!(action_submission_validated,"validator-validated");
action!(action_submission_released,"releaser-released");
}

View File

@ -1,3 +1,13 @@
mod context;
#[cfg(feature="internal")]
pub mod internal;
pub mod external;
//lazy reexport
pub type ReqwestError=reqwest::Error;
#[derive(Debug)] #[derive(Debug)]
pub enum Error{ pub enum Error{
ParseError(url::ParseError), ParseError(url::ParseError),
@ -9,143 +19,3 @@ impl std::fmt::Display for Error{
} }
} }
impl std::error::Error for Error{} impl std::error::Error for Error{}
#[derive(serde::Deserialize)]
pub struct ScriptID(i64);
#[allow(nonstandard_style)]
pub struct GetScriptRequest{
pub ScriptID:ScriptID,
}
#[allow(nonstandard_style)]
#[derive(serde::Deserialize)]
pub struct ScriptResponse{
pub ID:i64,
pub Hash:String,
pub Source:String,
pub SubmissionID:i64,
}
#[derive(serde::Deserialize)]
#[repr(i32)]
pub enum Policy{
None=0, // not yet reviewed
Allowed=1,
Blocked=2,
Delete=3,
Replace=4,
}
pub struct ScriptPolicyHashRequest{
pub hash:String,
}
#[allow(nonstandard_style)]
#[derive(serde::Deserialize)]
pub struct ScriptPolicyResponse{
pub ID:i64,
pub FromScriptHash:String,
pub ToScriptID:ScriptID,
pub Policy:Policy
}
#[allow(nonstandard_style)]
pub struct UpdateSubmissionModelRequest{
pub SubmissionID:i64,
pub ModelID:u64,
pub ModelVersion:u64,
}
pub struct SubmissionID(pub i64);
#[allow(nonstandard_style)]
pub struct ActionSubmissionUploadedRequest{
pub SubmissionID:i64,
pub TargetAssetID:Option<u64>,
}
#[derive(Clone)]
pub struct Context{
base_url:String,
client:reqwest::Client,
}
pub type ReqwestError=reqwest::Error;
// there are lots of action endpoints and they all follow the same pattern
macro_rules! action{
($fname:ident,$action:expr)=>{
pub async fn $fname(&self,config:SubmissionID)->Result<(),Error>{
let url_raw=format!(concat!("{}/submissions/{}/status/",$action),self.base_url,config.0);
let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
self.post(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?;
Ok(())
}
};
}
impl Context{
pub fn new(mut base_url:String)->reqwest::Result<Self>{
base_url+="/v1";
Ok(Self{
base_url,
client:reqwest::Client::new(),
})
}
async fn get(&self,url:impl reqwest::IntoUrl)->Result<reqwest::Response,reqwest::Error>{
self.client.get(url)
.send().await
}
async fn post(&self,url:impl reqwest::IntoUrl)->Result<reqwest::Response,reqwest::Error>{
self.client.post(url)
.send().await
}
pub async fn get_script(&self,config:GetScriptRequest)->Result<ScriptResponse,Error>{
let url_raw=format!("{}/scripts/{}",self.base_url,config.ScriptID.0);
let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
self.get(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?
.json().await.map_err(Error::Reqwest)
}
pub async fn get_script_policy_from_hash(&self,config:ScriptPolicyHashRequest)->Result<ScriptPolicyResponse,Error>{
let url_raw=format!("{}/script-policy/hash/{}",self.base_url,config.hash);
let url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
self.get(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?
.json().await.map_err(Error::Reqwest)
}
pub async fn update_submission_model(&self,config:UpdateSubmissionModelRequest)->Result<(),Error>{
let url_raw=format!("{}/submissions/{}/model",self.base_url,config.SubmissionID);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
{
url.query_pairs_mut()
.append_pair("ModelID",config.ModelID.to_string().as_str())
.append_pair("ModelVersion",config.ModelVersion.to_string().as_str());
}
self.post(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?;
Ok(())
}
pub async fn action_submission_uploaded(&self,config:ActionSubmissionUploadedRequest)->Result<(),Error>{
let url_raw=format!("{}/submissions/{}/status/uploaded",self.base_url,config.SubmissionID);
let mut url=reqwest::Url::parse(url_raw.as_str()).map_err(Error::ParseError)?;
if let Some(target_asset_id)=config.TargetAssetID{
url.query_pairs_mut()
.append_pair("TargetAssetID",target_asset_id.to_string().as_str());
}
self.post(url).await.map_err(Error::Reqwest)?
.error_for_status().map_err(Error::Reqwest)?;
Ok(())
}
action!(action_submission_validated,"validator-validated");
action!(action_submission_released,"releaser-released");
}

4
validation/Cargo.lock generated
View File

@ -1804,9 +1804,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "submissions-api" name = "submissions-api"
version = "0.1.1" version = "0.2.0"
source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
checksum = "7a56a1a29c7608cab695b426e286a435752321ed718c03199c858b2ab13525e3" checksum = "9d08d54792de7f1ddabbad0202c66536e14e8965fc3a6f9230179a0270440a08"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"serde", "serde",

View File

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
submissions-api = { version = "0.1.1", registry = "strafesnet" } submissions-api = { version = "0.2.0", features = ["internal"], registry = "strafesnet" }
async-nats = "0.38.0" async-nats = "0.38.0"
futures = "0.3.31" futures = "0.3.31"
rbx_asset = { version = "0.2.5", registry = "strafesnet" } rbx_asset = { version = "0.2.5", registry = "strafesnet" }

View File

@ -10,6 +10,7 @@ mod message_handler;
#[derive(Debug)] #[derive(Debug)]
pub enum StartupError{ pub enum StartupError{
API(submissions_api::ReqwestError), API(submissions_api::ReqwestError),
APIInternal(submissions_api::ReqwestError),
NatsConnect(async_nats::ConnectError), NatsConnect(async_nats::ConnectError),
NatsGetStream(async_nats::jetstream::context::GetStreamError), NatsGetStream(async_nats::jetstream::context::GetStreamError),
NatsConsumer(async_nats::jetstream::stream::ConsumerError), NatsConsumer(async_nats::jetstream::stream::ConsumerError),
@ -34,7 +35,10 @@ async fn main()->Result<(),StartupError>{
// maps-service api // maps-service api
let api_host=std::env::var("API_HOST").expect("API_HOST env required"); let api_host=std::env::var("API_HOST").expect("API_HOST env required");
let api=submissions_api::Context::new(api_host).map_err(StartupError::API)?; let api=submissions_api::external::Context::new(api_host).map_err(StartupError::API)?;
let api_host_internal=std::env::var("API_HOST_INTERNAL").expect("API_HOST_INTERNAL env required");
let api_internal=submissions_api::internal::Context::new(api_host_internal).map_err(StartupError::APIInternal)?;
// nats // nats
let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required"); let nats_host=std::env::var("NATS_HOST").expect("NATS_HOST env required");
@ -52,7 +56,7 @@ async fn main()->Result<(),StartupError>{
.messages().await.map_err(StartupError::NatsStream) .messages().await.map_err(StartupError::NatsStream)
}; };
let message_handler=message_handler::MessageHandler::new(cookie_context,api); let message_handler=message_handler::MessageHandler::new(cookie_context,api,api_internal);
// Create a signal listener for SIGTERM // Create a signal listener for SIGTERM
let mut sig_term=tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).expect("Failed to create SIGTERM signal listener"); let mut sig_term=tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).expect("Failed to create SIGTERM signal listener");

View File

@ -26,12 +26,13 @@ pub struct MessageHandler{
impl MessageHandler{ impl MessageHandler{
pub fn new( pub fn new(
cookie_context:rbx_asset::cookie::CookieContext, cookie_context:rbx_asset::cookie::CookieContext,
api:submissions_api::Context, api:submissions_api::external::Context,
api_internal:submissions_api::internal::Context,
)->Self{ )->Self{
Self{ Self{
publish_new:crate::publish_new::Publisher::new(cookie_context.clone(),api.clone()), publish_new:crate::publish_new::Publisher::new(cookie_context.clone(),api_internal.clone()),
publish_fix:crate::publish_fix::Publisher::new(cookie_context.clone(),api.clone()), publish_fix:crate::publish_fix::Publisher::new(cookie_context.clone(),api_internal.clone()),
validator:crate::validator::Validator::new(cookie_context,api), validator:crate::validator::Validator::new(cookie_context,api,api_internal),
} }
} }
pub async fn handle_message_result(&self,message_result:MessageResult)->Result<(),HandleMessageError>{ pub async fn handle_message_result(&self,message_result:MessageResult)->Result<(),HandleMessageError>{

View File

@ -17,16 +17,16 @@ impl std::error::Error for PublishError{}
pub struct Publisher{ pub struct Publisher{
roblox_cookie:rbx_asset::cookie::CookieContext, roblox_cookie:rbx_asset::cookie::CookieContext,
api:submissions_api::Context, api_internal:submissions_api::internal::Context,
} }
impl Publisher{ impl Publisher{
pub const fn new( pub const fn new(
roblox_cookie:rbx_asset::cookie::CookieContext, roblox_cookie:rbx_asset::cookie::CookieContext,
api:submissions_api::Context, api_internal:submissions_api::internal::Context,
)->Self{ )->Self{
Self{ Self{
roblox_cookie, roblox_cookie,
api, api_internal,
} }
} }
pub async fn publish(&self,message:async_nats::jetstream::Message)->Result<(),PublishError>{ pub async fn publish(&self,message:async_nats::jetstream::Message)->Result<(),PublishError>{
@ -53,7 +53,7 @@ impl Publisher{
// that's it, the database entry does not need to be changed. // that's it, the database entry does not need to be changed.
// mark submission as uploaded, TargetAssetID is unchanged // mark submission as uploaded, TargetAssetID is unchanged
self.api.action_submission_uploaded(submissions_api::ActionSubmissionUploadedRequest{ self.api_internal.action_submission_uploaded(submissions_api::internal::ActionSubmissionUploadedRequest{
SubmissionID:publish_info.SubmissionID, SubmissionID:publish_info.SubmissionID,
TargetAssetID:None, TargetAssetID:None,
}).await.map_err(PublishError::ApiActionSubmissionUploaded)?; }).await.map_err(PublishError::ApiActionSubmissionUploaded)?;

View File

@ -18,12 +18,12 @@ impl std::error::Error for PublishError{}
pub struct Publisher{ pub struct Publisher{
roblox_cookie:rbx_asset::cookie::CookieContext, roblox_cookie:rbx_asset::cookie::CookieContext,
api:submissions_api::Context, api:submissions_api::internal::Context,
} }
impl Publisher{ impl Publisher{
pub const fn new( pub const fn new(
roblox_cookie:rbx_asset::cookie::CookieContext, roblox_cookie:rbx_asset::cookie::CookieContext,
api:submissions_api::Context, api:submissions_api::internal::Context,
)->Self{ )->Self{
Self{ Self{
roblox_cookie, roblox_cookie,
@ -51,7 +51,7 @@ impl Publisher{
},model_data).await.map_err(PublishError::Create)?; },model_data).await.map_err(PublishError::Create)?;
// note the asset id of the created model for later release, and mark the submission as uploaded // note the asset id of the created model for later release, and mark the submission as uploaded
self.api.action_submission_uploaded(submissions_api::ActionSubmissionUploadedRequest{ self.api.action_submission_uploaded(submissions_api::internal::ActionSubmissionUploadedRequest{
SubmissionID:publish_info.SubmissionID, SubmissionID:publish_info.SubmissionID,
TargetAssetID:Some(upload_response.AssetId), TargetAssetID:Some(upload_response.AssetId),
}).await.map_err(PublishError::ApiActionSubmissionPublish)?; }).await.map_err(PublishError::ApiActionSubmissionPublish)?;

View File

@ -37,17 +37,20 @@ impl std::error::Error for ValidateError{}
pub struct Validator{ pub struct Validator{
roblox_cookie:rbx_asset::cookie::CookieContext, roblox_cookie:rbx_asset::cookie::CookieContext,
api:submissions_api::Context, api:submissions_api::external::Context,
api_internal:submissions_api::internal::Context,
} }
impl Validator{ impl Validator{
pub const fn new( pub const fn new(
roblox_cookie:rbx_asset::cookie::CookieContext, roblox_cookie:rbx_asset::cookie::CookieContext,
api:submissions_api::Context, api:submissions_api::external::Context,
api_internal:submissions_api::internal::Context,
)->Self{ )->Self{
Self{ Self{
roblox_cookie, roblox_cookie,
api, api,
api_internal,
} }
} }
pub async fn validate(&self,message:async_nats::jetstream::Message)->Result<(),ValidateError>{ pub async fn validate(&self,message:async_nats::jetstream::Message)->Result<(),ValidateError>{
@ -86,18 +89,18 @@ impl Validator{
let hash=std::hash::Hasher::finish(&hasher); let hash=std::hash::Hasher::finish(&hasher);
// fetch the script policy // fetch the script policy
let script_policy=self.api.get_script_policy_from_hash(submissions_api::ScriptPolicyHashRequest{ let script_policy=self.api.get_script_policy_from_hash(submissions_api::external::ScriptPolicyHashRequest{
hash:format!("{:x}",hash), hash:format!("{:x}",hash),
}).await.map_err(ValidateError::ApiGetScriptPolicy)?; }).await.map_err(ValidateError::ApiGetScriptPolicy)?;
// write the policy to the script_map, fetching the replacement code if necessary // write the policy to the script_map, fetching the replacement code if necessary
*replacement=match script_policy.Policy{ *replacement=match script_policy.Policy{
submissions_api::Policy::None=>Policy::None, submissions_api::external::Policy::None=>Policy::None,
submissions_api::Policy::Allowed=>Policy::Allowed, submissions_api::external::Policy::Allowed=>Policy::Allowed,
submissions_api::Policy::Blocked=>Policy::Blocked, submissions_api::external::Policy::Blocked=>Policy::Blocked,
submissions_api::Policy::Delete=>Policy::Delete, submissions_api::external::Policy::Delete=>Policy::Delete,
submissions_api::Policy::Replace=>{ submissions_api::external::Policy::Replace=>{
let script=self.api.get_script(submissions_api::GetScriptRequest{ let script=self.api.get_script(submissions_api::external::GetScriptRequest{
ScriptID:script_policy.ToScriptID, ScriptID:script_policy.ToScriptID,
}).await.map_err(ValidateError::ApiGetScript)?; }).await.map_err(ValidateError::ApiGetScript)?;
Policy::Replace(script.Source) Policy::Replace(script.Source)
@ -166,7 +169,7 @@ impl Validator{
}; };
// update the submission to use the validated model // update the submission to use the validated model
self.api.update_submission_model(submissions_api::UpdateSubmissionModelRequest{ self.api.update_submission_model(submissions_api::external::UpdateSubmissionModelRequest{
SubmissionID:validate_info.SubmissionID, SubmissionID:validate_info.SubmissionID,
ModelID:model_id, ModelID:model_id,
ModelVersion:1, //TODO ModelVersion:1, //TODO
@ -174,8 +177,8 @@ impl Validator{
}; };
// update the submission model status to validated // update the submission model status to validated
self.api.action_submission_validated( self.api_internal.action_submission_validated(
submissions_api::SubmissionID(validate_info.SubmissionID) submissions_api::internal::SubmissionID(validate_info.SubmissionID)
).await.map_err(ValidateError::ApiActionSubmissionValidate)?; ).await.map_err(ValidateError::ApiActionSubmissionValidate)?;
Ok(()) Ok(())