2024-12-07 02:57:26 +00:00
|
|
|
use futures::{StreamExt,TryStreamExt};
|
2024-12-03 06:52:01 +00:00
|
|
|
use crate::nats_types::ValidateRequest;
|
|
|
|
|
2024-12-07 02:57:26 +00:00
|
|
|
const SCRIPT_CONCURRENCY:usize=16;
|
|
|
|
|
2024-12-05 00:46:20 +00:00
|
|
|
enum Policy{
|
|
|
|
Allowed,
|
|
|
|
Blocked,
|
|
|
|
Delete,
|
|
|
|
Replace(String),
|
|
|
|
}
|
|
|
|
|
2024-12-03 06:52:01 +00:00
|
|
|
#[allow(dead_code)]
|
|
|
|
#[derive(Debug)]
|
2024-12-03 06:09:01 +00:00
|
|
|
enum ValidateError{
|
2024-12-05 00:46:20 +00:00
|
|
|
Blocked,
|
|
|
|
NotAllowed,
|
2024-12-03 06:52:01 +00:00
|
|
|
Get(rbx_asset::cookie::GetError),
|
|
|
|
Json(serde_json::Error),
|
|
|
|
ReadDom(ReadDomError),
|
2024-12-07 03:36:10 +00:00
|
|
|
ApiGetScriptPolicy(api::Error),
|
|
|
|
ApiGetScript(api::Error),
|
2024-12-07 03:37:33 +00:00
|
|
|
ApiUpdateSubmissionModel(api::Error),
|
2024-12-07 03:42:44 +00:00
|
|
|
ApiActionSubmissionValidate(api::Error),
|
2024-12-04 02:54:01 +00:00
|
|
|
WriteDom(rbx_binary::EncodeError),
|
|
|
|
Upload(rbx_asset::cookie::UploadError),
|
|
|
|
Create(rbx_asset::cookie::CreateError),
|
2024-12-03 06:09:01 +00:00
|
|
|
}
|
2024-12-03 06:52:01 +00:00
|
|
|
impl std::fmt::Display for ValidateError{
|
|
|
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
|
|
|
write!(f,"{self:?}")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl std::error::Error for ValidateError{}
|
2024-12-03 06:09:01 +00:00
|
|
|
|
|
|
|
pub struct Validator{
|
|
|
|
subscriber:async_nats::Subscriber,
|
2024-12-03 06:52:01 +00:00
|
|
|
roblox_cookie:rbx_asset::cookie::CookieContext,
|
2024-12-05 00:46:20 +00:00
|
|
|
api:api::Context,
|
2024-12-03 06:09:01 +00:00
|
|
|
}
|
2024-12-03 06:52:01 +00:00
|
|
|
|
2024-12-03 06:09:01 +00:00
|
|
|
impl Validator{
|
2024-12-03 06:52:01 +00:00
|
|
|
pub async fn new(
|
|
|
|
nats:async_nats::Client,
|
|
|
|
roblox_cookie:rbx_asset::cookie::CookieContext,
|
2024-12-05 00:46:20 +00:00
|
|
|
api:api::Context,
|
2024-12-03 06:52:01 +00:00
|
|
|
)->Result<Self,async_nats::SubscribeError>{
|
2024-12-03 06:09:01 +00:00
|
|
|
Ok(Self{
|
|
|
|
subscriber:nats.subscribe("validate").await?,
|
2024-12-03 06:52:01 +00:00
|
|
|
roblox_cookie,
|
2024-12-05 00:46:20 +00:00
|
|
|
api,
|
2024-12-03 06:09:01 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
pub async fn run(mut self){
|
|
|
|
while let Some(message)=self.subscriber.next().await{
|
2024-12-03 06:52:01 +00:00
|
|
|
self.validate_supress_error(message).await
|
|
|
|
}
|
|
|
|
}
|
|
|
|
async fn validate_supress_error(&self,message:async_nats::Message){
|
|
|
|
match self.validate(message).await{
|
2024-12-07 03:37:33 +00:00
|
|
|
Ok(())=>println!("Validated, hooray!"),
|
|
|
|
Err(e)=>println!("There was an error, oopsie! {e}"),
|
2024-12-03 06:09:01 +00:00
|
|
|
}
|
|
|
|
}
|
2024-12-07 03:37:33 +00:00
|
|
|
async fn validate(&self,message:async_nats::Message)->Result<(),ValidateError>{
|
2024-12-03 06:09:01 +00:00
|
|
|
println!("validate {:?}",message);
|
2024-12-03 06:52:01 +00:00
|
|
|
// decode json
|
|
|
|
let validate_info:ValidateRequest=serde_json::from_slice(&message.payload).map_err(ValidateError::Json)?;
|
|
|
|
|
2024-12-03 06:09:01 +00:00
|
|
|
// download map
|
2024-12-03 06:52:01 +00:00
|
|
|
let data=self.roblox_cookie.get_asset(rbx_asset::cookie::GetAssetRequest{
|
|
|
|
asset_id:validate_info.model_id,
|
|
|
|
version:Some(validate_info.model_version),
|
|
|
|
}).await.map_err(ValidateError::Get)?;
|
|
|
|
|
|
|
|
// decode dom (slow!)
|
|
|
|
let mut dom=read_dom(&mut std::io::Cursor::new(data)).map_err(ValidateError::ReadDom)?;
|
|
|
|
|
2024-12-04 02:54:01 +00:00
|
|
|
/* VALIDATE MAP */
|
|
|
|
|
|
|
|
// collect unique scripts
|
|
|
|
let script_refs=get_script_refs(&dom);
|
2024-12-05 00:46:20 +00:00
|
|
|
let mut script_map=std::collections::HashMap::<String,Policy>::new();
|
2024-12-04 02:54:01 +00:00
|
|
|
for &script_ref in &script_refs{
|
|
|
|
if let Some(script)=dom.get_by_ref(script_ref){
|
|
|
|
if let Some(rbx_dom_weak::types::Variant::String(source))=script.properties.get("Source"){
|
2024-12-05 00:46:20 +00:00
|
|
|
script_map.insert(source.clone(),Policy::Blocked);
|
2024-12-04 02:54:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-12-07 02:57:26 +00:00
|
|
|
|
2024-12-07 03:37:33 +00:00
|
|
|
// send all script hashes to REST endpoint and retrieve the replacements
|
2024-12-07 02:57:26 +00:00
|
|
|
futures::stream::iter(script_map.iter_mut().map(Ok))
|
|
|
|
.try_for_each_concurrent(Some(SCRIPT_CONCURRENCY),|(source,replacement)|async{
|
|
|
|
// get the hash
|
2024-12-07 02:57:11 +00:00
|
|
|
let mut hasher=siphasher::sip::SipHasher::new();
|
|
|
|
std::hash::Hasher::write(&mut hasher,source.as_bytes());
|
2024-12-05 00:46:20 +00:00
|
|
|
let hash=std::hash::Hasher::finish(&hasher);
|
2024-12-07 02:57:26 +00:00
|
|
|
|
|
|
|
// fetch the script policy
|
2024-12-05 00:46:20 +00:00
|
|
|
let script_policy=self.api.get_script_policy_from_hash(api::ScriptPolicyHashRequest{
|
|
|
|
hash:format!("{:x}",hash),
|
|
|
|
}).await.map_err(ValidateError::ApiGetScriptPolicy)?;
|
2024-12-07 02:57:26 +00:00
|
|
|
|
|
|
|
// write the policy to the script_map, fetching the replacement code if necessary
|
2024-12-05 00:46:20 +00:00
|
|
|
*replacement=match script_policy.Policy{
|
|
|
|
api::Policy::Allowed=>Policy::Allowed,
|
|
|
|
api::Policy::Blocked=>Policy::Blocked,
|
|
|
|
api::Policy::Delete=>Policy::Delete,
|
|
|
|
api::Policy::Replace=>{
|
|
|
|
let script=self.api.get_script(api::GetScriptRequest{
|
2024-12-07 03:02:57 +00:00
|
|
|
ScriptID:script_policy.ToScriptID,
|
2024-12-05 00:46:20 +00:00
|
|
|
}).await.map_err(ValidateError::ApiGetScript)?;
|
|
|
|
Policy::Replace(script.Source)
|
|
|
|
},
|
|
|
|
};
|
2024-12-07 02:57:26 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
})
|
|
|
|
.await?;
|
2024-12-04 02:54:01 +00:00
|
|
|
|
|
|
|
// make the replacements
|
2024-12-05 00:46:20 +00:00
|
|
|
let mut modified=false;
|
2024-12-04 02:54:01 +00:00
|
|
|
for &script_ref in &script_refs{
|
|
|
|
if let Some(script)=dom.get_by_ref_mut(script_ref){
|
|
|
|
if let Some(rbx_dom_weak::types::Variant::String(source))=script.properties.get_mut("Source"){
|
2024-12-05 00:46:20 +00:00
|
|
|
match script_map.get(source.as_str()){
|
|
|
|
Some(Policy::Blocked)=>return Err(ValidateError::Blocked),
|
|
|
|
None=>return Err(ValidateError::NotAllowed),
|
|
|
|
Some(Policy::Allowed)=>(),
|
|
|
|
Some(Policy::Delete)=>{
|
|
|
|
modified=true;
|
|
|
|
// delete script
|
2024-12-07 04:07:15 +00:00
|
|
|
dom.destroy(script_ref);
|
2024-12-05 00:46:20 +00:00
|
|
|
},
|
|
|
|
Some(Policy::Replace(replacement))=>{
|
|
|
|
modified=true;
|
|
|
|
*source=replacement.clone();
|
|
|
|
},
|
2024-12-04 02:54:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-12-03 06:52:01 +00:00
|
|
|
|
2024-12-07 03:45:44 +00:00
|
|
|
// if the model was validated, the submission must be changed to use the modified model
|
|
|
|
if modified{
|
2024-12-04 02:54:01 +00:00
|
|
|
// serialize model (slow!)
|
|
|
|
let mut data=Vec::new();
|
|
|
|
rbx_binary::to_writer(&mut data,&dom,&[dom.root_ref()]).map_err(ValidateError::WriteDom)?;
|
|
|
|
|
|
|
|
// upload a model lol
|
|
|
|
let (model_id,model_version)=if let Some(model_id)=validate_info.validated_model_id{
|
|
|
|
// upload to existing id
|
|
|
|
let response=self.roblox_cookie.upload(rbx_asset::cookie::UploadRequest{
|
|
|
|
assetid:model_id,
|
|
|
|
name:None,
|
|
|
|
description:None,
|
|
|
|
ispublic:None,
|
|
|
|
allowComments:None,
|
|
|
|
groupId:None,
|
|
|
|
},data).await.map_err(ValidateError::Upload)?;
|
|
|
|
|
|
|
|
// response.AssetVersionId is not the right one
|
|
|
|
let model_version=get_model_version_number();
|
|
|
|
(response.AssetId,model_version)
|
|
|
|
}else{
|
|
|
|
// create new model
|
|
|
|
let response=self.roblox_cookie.create(rbx_asset::cookie::CreateRequest{
|
2024-12-05 00:46:20 +00:00
|
|
|
name:dom.root().name.clone(),
|
2024-12-04 02:54:01 +00:00
|
|
|
description:"".to_owned(),
|
|
|
|
ispublic:true,
|
|
|
|
allowComments:true,
|
|
|
|
groupId:None,
|
|
|
|
},data).await.map_err(ValidateError::Create)?;
|
|
|
|
|
|
|
|
// response.AssetVersionId is not the right one
|
|
|
|
let model_version=get_model_version_number();
|
|
|
|
(response.AssetId,model_version)
|
|
|
|
};
|
|
|
|
|
2024-12-07 03:45:44 +00:00
|
|
|
// update the submission to use the validated model
|
|
|
|
self.api.update_submission_model(api::UpdateSubmissionModelRequest{
|
|
|
|
ID:validate_info.submission_id,
|
|
|
|
ModelID:model_id,
|
|
|
|
ModelVersion:model_version,
|
|
|
|
}).await.map_err(ValidateError::ApiUpdateSubmissionModel)?;
|
2024-12-07 03:37:33 +00:00
|
|
|
};
|
|
|
|
|
2024-12-07 03:46:38 +00:00
|
|
|
// update the submission model status to validated
|
2024-12-07 03:42:44 +00:00
|
|
|
self.api.action_submission_validate(
|
|
|
|
api::SubmissionID(validate_info.submission_id)
|
|
|
|
).await.map_err(ValidateError::ApiActionSubmissionValidate)?;
|
|
|
|
|
2024-12-07 03:37:33 +00:00
|
|
|
Ok(())
|
2024-12-03 06:52:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-05 00:46:20 +00:00
|
|
|
fn get_model_version_number()->u64{
|
|
|
|
unimplemented!()
|
|
|
|
}
|
|
|
|
|
2024-12-03 06:52:01 +00:00
|
|
|
#[allow(dead_code)]
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum ReadDomError{
|
|
|
|
Binary(rbx_binary::DecodeError),
|
|
|
|
Xml(rbx_xml::DecodeError),
|
|
|
|
Read(std::io::Error),
|
|
|
|
Seek(std::io::Error),
|
|
|
|
UnknownFormat([u8;8]),
|
|
|
|
}
|
|
|
|
impl std::fmt::Display for ReadDomError{
|
|
|
|
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
|
|
|
write!(f,"{self:?}")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
impl std::error::Error for ReadDomError{}
|
|
|
|
|
|
|
|
fn read_dom<R:std::io::Read+std::io::Seek>(input:&mut R)->Result<rbx_dom_weak::WeakDom,ReadDomError>{
|
|
|
|
let mut first_8=[0u8;8];
|
|
|
|
std::io::Read::read_exact(input,&mut first_8).map_err(ReadDomError::Read)?;
|
|
|
|
std::io::Seek::rewind(input).map_err(ReadDomError::Seek)?;
|
|
|
|
match &first_8[0..4]{
|
|
|
|
b"<rob"=>{
|
|
|
|
match &first_8[4..8]{
|
|
|
|
b"lox!"=>rbx_binary::from_reader(input).map_err(ReadDomError::Binary),
|
|
|
|
b"lox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(ReadDomError::Xml),
|
|
|
|
_=>Err(ReadDomError::UnknownFormat(first_8)),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_=>Err(ReadDomError::UnknownFormat(first_8)),
|
2024-12-03 06:09:01 +00:00
|
|
|
}
|
|
|
|
}
|
2024-12-04 02:54:01 +00:00
|
|
|
|
|
|
|
fn class_is_a(class:&str,superclass:&str)->bool{
|
|
|
|
if class==superclass{
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
let class_descriptor=rbx_reflection_database::get().classes.get(class);
|
|
|
|
if let Some(descriptor)=&class_descriptor{
|
|
|
|
if let Some(class_super)=&descriptor.superclass{
|
|
|
|
return class_is_a(&class_super,superclass)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
|
|
|
fn recursive_collect_superclass(objects:&mut std::vec::Vec<rbx_dom_weak::types::Ref>,dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str){
|
|
|
|
for &referent in instance.children(){
|
|
|
|
if let Some(c)=dom.get_by_ref(referent){
|
|
|
|
if class_is_a(c.class.as_str(),superclass){
|
|
|
|
objects.push(c.referent());//copy ref
|
|
|
|
}
|
|
|
|
recursive_collect_superclass(objects,dom,c,superclass);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_script_refs(dom:&rbx_dom_weak::WeakDom)->Vec<rbx_dom_weak::types::Ref>{
|
|
|
|
let mut scripts=std::vec::Vec::new();
|
|
|
|
recursive_collect_superclass(&mut scripts,dom,dom.root(),"LuaSourceContainer");
|
|
|
|
scripts
|
|
|
|
}
|