From 03490a147483bcfcfb769228b7b3fe245b9a4f92 Mon Sep 17 00:00:00 2001 From: Ryder Date: Mon, 14 Dec 2020 12:24:16 -0500 Subject: [PATCH 01/29] Fixed to_url function to format a proper URL in request/level, comment, and user.rs also remoced duplicated use statement in model/comment/level.rs --- src/model/comment/level.rs | 1 + src/request/comment.rs | 8 ++++---- src/request/level.rs | 8 ++++---- src/request/user.rs | 8 ++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/model/comment/level.rs b/src/model/comment/level.rs index 80d747d..273f53f 100644 --- a/src/model/comment/level.rs +++ b/src/model/comment/level.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +//use std::borrow::Cow; use serde::{Deserialize, Serialize}; diff --git a/src/request/comment.rs b/src/request/comment.rs index 4b87683..b211424 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -7,8 +7,8 @@ use crate::{ use serde::Serialize; use std::fmt::{Display, Formatter}; -pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php"; -pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php"; +pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php?"; +pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php?"; /// The different orderings that can be requested for level comments #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] @@ -85,7 +85,7 @@ impl<'a> LevelCommentsRequest<'a> { const_setter!(page: u32); pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, super::to_string(self), LEVEL_COMMENTS_ENDPOINT) + format!("{}{}{}", REQUEST_BASE_URL, LEVEL_COMMENTS_ENDPOINT, super::to_string(self)) } pub const fn new(level: u64) -> Self { @@ -165,7 +165,7 @@ impl<'a> ProfileCommentsRequest<'a> { const_setter!(account_id: u64); pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, super::to_string(self), PROFILE_COMMENT_ENDPOINT) + format!("{}{}{}", REQUEST_BASE_URL, PROFILE_COMMENT_ENDPOINT, super::to_string(self)) } pub const fn new(account: u64) -> Self { diff --git a/src/request/level.rs b/src/request/level.rs index 621cf11..84a2577 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -7,8 +7,8 @@ use crate::{ }; use serde::{Deserialize, Serialize, Serializer}; -pub const DOWNLOAD_LEVEL_ENDPOINT: &str = "downloadGJLevel22.php"; -pub const SEARCH_LEVEL_ENDPOINT: &str = "getGJLevels21.php"; +pub const DOWNLOAD_LEVEL_ENDPOINT: &str = "downloadGJLevel22.php?"; +pub const SEARCH_LEVEL_ENDPOINT: &str = "getGJLevels21.php?"; /// Struct modelled after a request to `downloadGJLevel22.php`. /// @@ -85,7 +85,7 @@ impl<'a> LevelRequest<'a> { } pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, super::to_string(self), DOWNLOAD_LEVEL_ENDPOINT) + format!("{}{}{}", REQUEST_BASE_URL, DOWNLOAD_LEVEL_ENDPOINT, super::to_string(self)) } } @@ -535,7 +535,7 @@ impl<'a> LevelsRequest<'a> { const_setter!(request_type: LevelRequestType); pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, super::to_string(self), SEARCH_LEVEL_ENDPOINT) + format!("{}{}{}", REQUEST_BASE_URL, SEARCH_LEVEL_ENDPOINT, super::to_string(self)) } pub fn with_base(base: BaseRequest<'a>) -> Self { diff --git a/src/request/user.rs b/src/request/user.rs index 7b3bd62..2ea3df7 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -7,8 +7,8 @@ use crate::{ use serde::Serialize; use std::fmt::{Display, Error, Formatter}; -pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20"; -pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20"; +pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20?"; +pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20?"; /// Struct modelled after a request to `getGJUserInfo20.php`. /// @@ -36,7 +36,7 @@ impl UserRequest<'_> { } pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, super::to_string(self), GET_USER_ENDPOINT) + format!("{}{}{}", REQUEST_BASE_URL, GET_USER_ENDPOINT, super::to_string(self)) } } @@ -99,7 +99,7 @@ impl<'a> UserSearchRequest<'a> { } pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, super::to_string(self), SEARCH_USER_ENDPOINT) + format!("{}{}{}", REQUEST_BASE_URL, SEARCH_USER_ENDPOINT, super::to_string(self)) } } From 8879608b4bf3f484c7fe7cdd3a67594674d4ffd5 Mon Sep 17 00:00:00 2001 From: Ryder Date: Wed, 16 Dec 2020 14:08:22 -0500 Subject: [PATCH 02/29] Moved '?' character into the format string rather than endpoint constants. --- src/request/comment.rs | 8 ++++---- src/request/level.rs | 8 ++++---- src/request/user.rs | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/request/comment.rs b/src/request/comment.rs index b211424..8eba25d 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -7,8 +7,8 @@ use crate::{ use serde::Serialize; use std::fmt::{Display, Formatter}; -pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php?"; -pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php?"; +pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php"; +pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php"; /// The different orderings that can be requested for level comments #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] @@ -85,7 +85,7 @@ impl<'a> LevelCommentsRequest<'a> { const_setter!(page: u32); pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, LEVEL_COMMENTS_ENDPOINT, super::to_string(self)) + format!("{}{}?{}", REQUEST_BASE_URL, LEVEL_COMMENTS_ENDPOINT, super::to_string(self)) } pub const fn new(level: u64) -> Self { @@ -165,7 +165,7 @@ impl<'a> ProfileCommentsRequest<'a> { const_setter!(account_id: u64); pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, PROFILE_COMMENT_ENDPOINT, super::to_string(self)) + format!("{}{}?{}", REQUEST_BASE_URL, PROFILE_COMMENT_ENDPOINT, super::to_string(self)) } pub const fn new(account: u64) -> Self { diff --git a/src/request/level.rs b/src/request/level.rs index 84a2577..9b92ba4 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -7,8 +7,8 @@ use crate::{ }; use serde::{Deserialize, Serialize, Serializer}; -pub const DOWNLOAD_LEVEL_ENDPOINT: &str = "downloadGJLevel22.php?"; -pub const SEARCH_LEVEL_ENDPOINT: &str = "getGJLevels21.php?"; +pub const DOWNLOAD_LEVEL_ENDPOINT: &str = "downloadGJLevel22.php"; +pub const SEARCH_LEVEL_ENDPOINT: &str = "getGJLevels21.php"; /// Struct modelled after a request to `downloadGJLevel22.php`. /// @@ -85,7 +85,7 @@ impl<'a> LevelRequest<'a> { } pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, DOWNLOAD_LEVEL_ENDPOINT, super::to_string(self)) + format!("{}{}?{}", REQUEST_BASE_URL, DOWNLOAD_LEVEL_ENDPOINT, super::to_string(self)) } } @@ -535,7 +535,7 @@ impl<'a> LevelsRequest<'a> { const_setter!(request_type: LevelRequestType); pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, SEARCH_LEVEL_ENDPOINT, super::to_string(self)) + format!("{}{}?{}", REQUEST_BASE_URL, SEARCH_LEVEL_ENDPOINT, super::to_string(self)) } pub fn with_base(base: BaseRequest<'a>) -> Self { diff --git a/src/request/user.rs b/src/request/user.rs index 2ea3df7..cfd45a1 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -7,8 +7,8 @@ use crate::{ use serde::Serialize; use std::fmt::{Display, Error, Formatter}; -pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20?"; -pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20?"; +pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20"; +pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20"; /// Struct modelled after a request to `getGJUserInfo20.php`. /// @@ -36,7 +36,7 @@ impl UserRequest<'_> { } pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, GET_USER_ENDPOINT, super::to_string(self)) + format!("{}{}?{}", REQUEST_BASE_URL, GET_USER_ENDPOINT, super::to_string(self)) } } @@ -99,7 +99,7 @@ impl<'a> UserSearchRequest<'a> { } pub fn to_url(&self) -> String { - format!("{}{}{}", REQUEST_BASE_URL, SEARCH_USER_ENDPOINT, super::to_string(self)) + format!("{}{}?{}", REQUEST_BASE_URL, SEARCH_USER_ENDPOINT, super::to_string(self)) } } From 1c8792be3827cbeab7213bd11b57d5ac48398408 Mon Sep 17 00:00:00 2001 From: Ryder Date: Mon, 15 Feb 2021 15:52:47 -0500 Subject: [PATCH 03/29] Renamed export::Formatter use clauses to be '__private::Formatter' because it's different now apparently --- src/model/user/profile.rs | 2 +- src/response.rs | 2 +- src/serde/de/error.rs | 2 +- src/serde/ser/error.rs | 2 +- src/serde/thunk.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/model/user/profile.rs b/src/model/user/profile.rs index 6115a54..dfd0954 100644 --- a/src/model/user/profile.rs +++ b/src/model/user/profile.rs @@ -1,5 +1,5 @@ use crate::model::user::{Color, ModLevel}; -use serde::{export::Formatter, Deserialize, Serialize}; +use serde::{__private::Formatter, Deserialize, Serialize}; use std::{borrow::Cow, fmt::Display}; #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] diff --git a/src/response.rs b/src/response.rs index 609e705..7b4ab9d 100644 --- a/src/response.rs +++ b/src/response.rs @@ -14,7 +14,7 @@ use crate::{ }, DeError, HasRobtopFormat, }; -use serde::export::Formatter; +use serde::__private::Formatter; use std::fmt::Display; // Since NoneError is not stabilized, we cannot do `impl From for ResponseError<'_>`, so diff --git a/src/serde/de/error.rs b/src/serde/de/error.rs index 036ce0d..0ac4725 100644 --- a/src/serde/de/error.rs +++ b/src/serde/de/error.rs @@ -1,6 +1,6 @@ //! Module containing the error type for deserialization errors -use serde::export::Formatter; +use serde::__private::Formatter; use std::fmt::Display; /// Errors that can occur during deserialization diff --git a/src/serde/ser/error.rs b/src/serde/ser/error.rs index ff37740..52fe758 100644 --- a/src/serde/ser/error.rs +++ b/src/serde/ser/error.rs @@ -1,4 +1,4 @@ -use serde::export::Formatter; +use serde::__private::Formatter; use std::{fmt::Display, io}; /// Errors that can occur during serialization diff --git a/src/serde/thunk.rs b/src/serde/thunk.rs index b3254ad..9c9011d 100644 --- a/src/serde/thunk.rs +++ b/src/serde/thunk.rs @@ -1,6 +1,6 @@ use base64::{DecodeError, URL_SAFE}; use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS}; -use serde::{export::Formatter, ser::Error as _, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{__private::Formatter, ser::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{borrow::Cow, fmt::Display, num::ParseIntError, str::Utf8Error, string::FromUtf8Error}; /// Enum modelling the different errors that can occur during processing of a [`Thunk`] From 87c8170b34032ba3e5b22cc7e8ded3fd4bfa5287 Mon Sep 17 00:00:00 2001 From: Ryder Date: Sat, 25 Dec 2021 16:07:35 -0500 Subject: [PATCH 04/29] Added request structs for getting comment history, added models for parsing comment history responses, modified build scripts to include comment history parsing --- build.rs | 2 + descriptions/comment_history.yml | 43 ++++++++++++++++ src/model/comment/level.rs | 65 +++++++++++++++++++++++- src/model/mod.rs | 5 ++ src/request/comment.rs | 85 +++++++++++++++++++++++++++++++- src/response.rs | 47 +++++++++++++++++- src/serde/mod.rs | 6 +-- 7 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 descriptions/comment_history.yml diff --git a/build.rs b/build.rs index c3cb6d6..2e719ca 100644 --- a/build.rs +++ b/build.rs @@ -18,6 +18,8 @@ fn generate_boilerplate() -> std::io::Result<()> { let file_stem = entry.path().file_stem().unwrap().to_str().unwrap().to_string(); let filename = format!("{}.boilerplate", file_stem); + println!("{}", &filename); + let dest_path = Path::new(&out_dir).join(filename); let mut f = File::create(dest_path)?; diff --git a/descriptions/comment_history.yml b/descriptions/comment_history.yml new file mode 100644 index 0000000..ed897d8 --- /dev/null +++ b/descriptions/comment_history.yml @@ -0,0 +1,43 @@ +--- +struct: 'CommentHistory<''src>' +map_like: true +separator: '~' +indices: + - value: 2 + type: 'Base64Decoded<''src>' + maps_to: content + thunk: true + optional: true + - value: 3 + type: u64 + maps_to: user_id + - value: 4 + type: i32 + maps_to: likes + - value: 6 + type: u64 + maps_to: comment_id + - value: 9 + type: '&''src str' + maps_to: time_since_post + - value: 10 + type: Option + maps_to: progress + - value: 11 + type: u8 + maps_to: mod_level + use_into: true + - value: 12 + type: Option + compute: | + self.special_color.map(|color| { + match color { + Color::Known(r, g, b) => RGBColor(r, g, b), + _ => panic!("Color::Unknown passed as color of level comment"), + } + }) +special_fields: + special_color: | + internal.index_12.map(|RGBColor(r, g, b)| Color::Known(r, g, b)) + user: | + None \ No newline at end of file diff --git a/src/model/comment/level.rs b/src/model/comment/level.rs index 811ae2d..4d9f2ed 100644 --- a/src/model/comment/level.rs +++ b/src/model/comment/level.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -// use std::borrow::Cow; use serde::{Deserialize, Serialize}; @@ -76,6 +75,67 @@ pub struct LevelComment<'a> { pub special_color: Option, } +#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] +pub struct CommentHistory<'a> { + /// Information about the user that made this [`LevelComment`]. Is generally a [`CommentUser`] + /// object + pub user: Option>, + + /// The actual content of the [`LevelComment`] made. + /// + /// ## GD Internals + /// This value is provided at index `2` and is base64 encoded + #[serde(borrow)] + pub content: Option>>, + + /// The unique user id of the player who made this [`LevelComment`] + /// + /// ## GD Internals + /// This value is provided at index `3` + pub user_id: u64, + + /// The amount of likes this [`LevelComment`] has received + /// + /// ## GD Internals + /// This value is provided at index `4` + pub likes: i32, + + /// The unique id of this [`LevelComment`]. Additionally, there is also no [`ProfileComment`] + /// with this idea + /// + /// ## GD Internals + /// This value is provided at index `6` + pub comment_id: u64, + + /// Robtop's completely braindead way of keeping track of when this [`LevelComment`] was posted + /// + /// ## GD Internals + /// This value is provided at index `9` + pub time_since_post: Cow<'a, str>, + + /// If enabled by the user making this [`LevelComment`], the progress they have done on the + /// level this comment is on. + /// + /// ## GD Internals + /// This value is provided at index `10` + pub progress: Option, + + /// The level of moderator the player that made this [`LevelComment`] is + /// + /// ## GD Internals + /// This value is provided at index `11` + pub mod_level: ModLevel, + + /// If this [`LevelComment`]'s text is displayed in a special color (blue for robtop, green for + /// elder mods), the RGB code of that color will be stored here + /// + /// Note that the yellow color of comments made by the creator is not reported here. + /// + /// ## GD Internals + /// This value is provided at index `12` + pub special_color: Option, +} + #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] pub struct CommentUser<'a> { /// This [`CommentUser`]'s name @@ -133,7 +193,7 @@ mod internal { use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::model::{ - comment::level::{CommentUser, LevelComment}, + comment::level::{CommentUser, LevelComment, CommentHistory}, user::Color, }; use std::io::Write; @@ -170,5 +230,6 @@ mod internal { } include!(concat!(env!("OUT_DIR"), "/level_comment.boilerplate")); + include!(concat!(env!("OUT_DIR"), "/comment_history.boilerplate")); include!(concat!(env!("OUT_DIR"), "/comment_user.boilerplate")); } diff --git a/src/model/mod.rs b/src/model/mod.rs index 465b871..c7ceaff 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; +use crate::DeError; pub mod comment; pub mod creator; @@ -68,3 +69,7 @@ impl Display for GameVersion { } } } + +pub trait FromRobTopStr { + fn from_robtop_str(robtop_str: &str) -> Result where Self: Sized; +} diff --git a/src/request/comment.rs b/src/request/comment.rs index 9979671..a537e29 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -8,6 +8,7 @@ use serde::Serialize; pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php"; pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php"; +pub const COMMENT_HISTORY_ENDPOINT: &str = "getGJCommentHistory.php"; /// The different orderings that can be requested for level comments #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] @@ -187,9 +188,76 @@ impl ToString for ProfileCommentsRequest<'_> { } } +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)] +pub struct CommentHistoryRequest<'a> { + /// The base request data + pub base: BaseRequest<'a>, + + /// Unknown, probably related to pagination + /// + /// ## GD Internals: + /// This field is called `total` in the boomlings API + pub total: u32, + + /// The page of comments to retrieve. The first page is page `0` and pages will contain `total` number of comments + /// + /// ## GD Internals: + /// This field is called `page` in the boomlings API + pub page: u32, + + /// What to sort by comments by + /// + /// ## GD Internals: + /// This field is called `mode` in the boomlings API. + #[serde(rename = "mode")] + pub sort_mode: SortMode, + + /// The id of the player to retrieve comments, this is `not` the account ID + /// + /// ## GD Internals: + /// This field is called `userID` in the boomlings API. + #[serde(rename = "userID")] + pub player_id: u64, + + /// The amount of comments to retrieve. Note that while in-game this can only be set to 20 or 40 however, a max of 100 comments can be returned + /// ## GD Internals: + /// This field is called `count` in the boomlings API + #[serde(rename = "count")] + pub limit: u32, +} + +impl<'a> CommentHistoryRequest<'a> { + const_setter!(limit: u32); + const_setter!(page: u32); + + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, COMMENT_HISTORY_ENDPOINT) + } + + pub const fn with_base(base: BaseRequest<'a>, player: u64) -> Self { + CommentHistoryRequest { + player_id: player, + base, + page: 0, + total: 0, + sort_mode: SortMode::Recent, + limit: 20, + } + } + + pub const fn new(player: u64) -> Self { + Self::with_base(GD_21, player) + } + + pub const fn sort_mode(mut self, sort_mode: SortMode) -> Self { + self.sort_mode = sort_mode; + self + } +} + #[cfg(test)] mod tests { - use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest}; + use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest}; #[test] fn serialize_level_comments() { @@ -220,4 +288,19 @@ mod tests { "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=2&accountID=1710032" ); } + + #[test] + fn serialize_comment_history() { + if let Err(err) = env_logger::builder().is_test(true).try_init() { + // nothing to make the tests fail over + eprintln!("Error setting up env_logger: {:?}", err) + } + + let request = CommentHistoryRequest::new(159782398) + .limit(2); + + println!("{}\n{}", request.to_url(), super::super::to_string(request)); + + assert_eq!(true, true) + } } diff --git a/src/response.rs b/src/response.rs index 7b4ab9d..0cb5a97 100644 --- a/src/response.rs +++ b/src/response.rs @@ -4,7 +4,7 @@ use crate::{ model::{ comment::{ - level::{CommentUser, LevelComment}, + level::{CommentUser, LevelComment, CommentHistory}, profile::ProfileComment, }, creator::Creator, @@ -182,6 +182,38 @@ pub fn parse_get_gj_comments_response(response: &str) -> Result Result, ResponseError> { + if response == "-1" { + return Err(ResponseError::NotFound) + } + + let mut sections = response.split('#'); + + // The format here is very weird. We have a '|' separated list of (comment, user) pairs, and said + // pair is separated by a ':' + + section!(sections) + .split('|') + .map(|fragment| { + let mut parts = fragment.split(':'); + + if let (Some(raw_comment), Some(raw_user)) = (parts.next(), parts.next()) { + let mut comment = CommentHistory::from_robtop_str(raw_comment)?; + + comment.user = if raw_user == "1~~9~~10~~11~~14~~15~~16~" { + None + } else { + Some(CommentUser::from_robtop_str(raw_user)?) + }; + + Ok(comment) + } else { + Err(ResponseError::UnexpectedFormat) + } + }) + .collect() +} + pub fn parse_get_gj_acccount_comments_response(response: &str) -> Result, ResponseError> { if response == "-1" { return Err(ResponseError::NotFound) @@ -194,3 +226,16 @@ pub fn parse_get_gj_acccount_comments_response(response: &str) -> Result Date: Sat, 25 Dec 2021 18:24:19 -0500 Subject: [PATCH 05/29] Removed unneccessary tests and properly used assert statement in serialize_comment_history test --- build.rs | 3 --- src/request/comment.rs | 4 +--- src/response.rs | 13 ------------- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/build.rs b/build.rs index 2e719ca..22651a3 100644 --- a/build.rs +++ b/build.rs @@ -17,9 +17,6 @@ fn generate_boilerplate() -> std::io::Result<()> { let file_stem = entry.path().file_stem().unwrap().to_str().unwrap().to_string(); let filename = format!("{}.boilerplate", file_stem); - - println!("{}", &filename); - let dest_path = Path::new(&out_dir).join(filename); let mut f = File::create(dest_path)?; diff --git a/src/request/comment.rs b/src/request/comment.rs index a537e29..ce3f021 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -299,8 +299,6 @@ mod tests { let request = CommentHistoryRequest::new(159782398) .limit(2); - println!("{}\n{}", request.to_url(), super::super::to_string(request)); - - assert_eq!(true, true) + assert_eq!(super::super::to_string(request), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2") } } diff --git a/src/response.rs b/src/response.rs index 64ef07e..73556ec 100644 --- a/src/response.rs +++ b/src/response.rs @@ -226,17 +226,4 @@ pub fn parse_get_gj_acccount_comments_response(response: &str) -> Result Date: Sat, 25 Dec 2021 19:00:02 -0500 Subject: [PATCH 06/29] Removed CommentHistory Struct, instead opted in to make index 7 type Option and model the comment history response into LevelComment --- descriptions/comment_history.yml | 43 --------------------- descriptions/level_comment.yml | 2 +- src/model/comment/level.rs | 66 +------------------------------- src/response.rs | 18 +++++++-- tests/comment.rs | 6 +-- 5 files changed, 21 insertions(+), 114 deletions(-) delete mode 100644 descriptions/comment_history.yml diff --git a/descriptions/comment_history.yml b/descriptions/comment_history.yml deleted file mode 100644 index ed897d8..0000000 --- a/descriptions/comment_history.yml +++ /dev/null @@ -1,43 +0,0 @@ ---- -struct: 'CommentHistory<''src>' -map_like: true -separator: '~' -indices: - - value: 2 - type: 'Base64Decoded<''src>' - maps_to: content - thunk: true - optional: true - - value: 3 - type: u64 - maps_to: user_id - - value: 4 - type: i32 - maps_to: likes - - value: 6 - type: u64 - maps_to: comment_id - - value: 9 - type: '&''src str' - maps_to: time_since_post - - value: 10 - type: Option - maps_to: progress - - value: 11 - type: u8 - maps_to: mod_level - use_into: true - - value: 12 - type: Option - compute: | - self.special_color.map(|color| { - match color { - Color::Known(r, g, b) => RGBColor(r, g, b), - _ => panic!("Color::Unknown passed as color of level comment"), - } - }) -special_fields: - special_color: | - internal.index_12.map(|RGBColor(r, g, b)| Color::Known(r, g, b)) - user: | - None \ No newline at end of file diff --git a/descriptions/level_comment.yml b/descriptions/level_comment.yml index afe012e..a0a82f8 100644 --- a/descriptions/level_comment.yml +++ b/descriptions/level_comment.yml @@ -18,7 +18,7 @@ indices: type: u64 maps_to: comment_id - value: 7 - type: bool + type: Option maps_to: is_flagged_spam - value: 9 type: '&''src str' diff --git a/src/model/comment/level.rs b/src/model/comment/level.rs index 4d9f2ed..49e799a 100644 --- a/src/model/comment/level.rs +++ b/src/model/comment/level.rs @@ -44,68 +44,7 @@ pub struct LevelComment<'a> { /// /// ## GD Internals /// This value is provided at index `7` - pub is_flagged_spam: bool, - - /// Robtop's completely braindead way of keeping track of when this [`LevelComment`] was posted - /// - /// ## GD Internals - /// This value is provided at index `9` - pub time_since_post: Cow<'a, str>, - - /// If enabled by the user making this [`LevelComment`], the progress they have done on the - /// level this comment is on. - /// - /// ## GD Internals - /// This value is provided at index `10` - pub progress: Option, - - /// The level of moderator the player that made this [`LevelComment`] is - /// - /// ## GD Internals - /// This value is provided at index `11` - pub mod_level: ModLevel, - - /// If this [`LevelComment`]'s text is displayed in a special color (blue for robtop, green for - /// elder mods), the RGB code of that color will be stored here - /// - /// Note that the yellow color of comments made by the creator is not reported here. - /// - /// ## GD Internals - /// This value is provided at index `12` - pub special_color: Option, -} - -#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] -pub struct CommentHistory<'a> { - /// Information about the user that made this [`LevelComment`]. Is generally a [`CommentUser`] - /// object - pub user: Option>, - - /// The actual content of the [`LevelComment`] made. - /// - /// ## GD Internals - /// This value is provided at index `2` and is base64 encoded - #[serde(borrow)] - pub content: Option>>, - - /// The unique user id of the player who made this [`LevelComment`] - /// - /// ## GD Internals - /// This value is provided at index `3` - pub user_id: u64, - - /// The amount of likes this [`LevelComment`] has received - /// - /// ## GD Internals - /// This value is provided at index `4` - pub likes: i32, - - /// The unique id of this [`LevelComment`]. Additionally, there is also no [`ProfileComment`] - /// with this idea - /// - /// ## GD Internals - /// This value is provided at index `6` - pub comment_id: u64, + pub is_flagged_spam: Option, /// Robtop's completely braindead way of keeping track of when this [`LevelComment`] was posted /// @@ -193,7 +132,7 @@ mod internal { use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::model::{ - comment::level::{CommentUser, LevelComment, CommentHistory}, + comment::level::{CommentUser, LevelComment}, user::Color, }; use std::io::Write; @@ -230,6 +169,5 @@ mod internal { } include!(concat!(env!("OUT_DIR"), "/level_comment.boilerplate")); - include!(concat!(env!("OUT_DIR"), "/comment_history.boilerplate")); include!(concat!(env!("OUT_DIR"), "/comment_user.boilerplate")); } diff --git a/src/response.rs b/src/response.rs index 73556ec..16302b4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -4,7 +4,7 @@ use crate::{ model::{ comment::{ - level::{CommentUser, LevelComment, CommentHistory}, + level::{CommentUser, LevelComment}, profile::ProfileComment, }, creator::Creator, @@ -183,7 +183,7 @@ pub fn parse_get_gj_comments_response(response: &str) -> Result Result, ResponseError> { +pub fn parse_get_gj_comment_history_response(response: &str) -> Result, ResponseError> { if response == "-1" { return Err(ResponseError::NotFound) } @@ -199,7 +199,7 @@ pub fn parse_get_gj_comment_history_response(response: &str) -> Result Result Date: Sun, 26 Dec 2021 14:10:06 -0500 Subject: [PATCH 07/29] Removed more unneccesary testing --- src/model/mod.rs | 7 +------ src/response.rs | 44 -------------------------------------------- src/serde/mod.rs | 6 +++--- 3 files changed, 4 insertions(+), 53 deletions(-) diff --git a/src/model/mod.rs b/src/model/mod.rs index c7ceaff..2754a5d 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -16,7 +16,6 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; -use crate::DeError; pub mod comment; pub mod creator; @@ -68,8 +67,4 @@ impl Display for GameVersion { GameVersion::Version { minor, major } => write!(f, "{}.{}", major, minor), } } -} - -pub trait FromRobTopStr { - fn from_robtop_str(robtop_str: &str) -> Result where Self: Sized; -} +} \ No newline at end of file diff --git a/src/response.rs b/src/response.rs index 16302b4..8813876 100644 --- a/src/response.rs +++ b/src/response.rs @@ -183,38 +183,6 @@ pub fn parse_get_gj_comments_response(response: &str) -> Result Result, ResponseError> { - if response == "-1" { - return Err(ResponseError::NotFound) - } - - let mut sections = response.split('#'); - - // The format here is very weird. We have a '|' separated list of (comment, user) pairs, and said - // pair is separated by a ':' - - section!(sections) - .split('|') - .map(|fragment| { - let mut parts = fragment.split(':'); - - if let (Some(raw_comment), Some(raw_user)) = (parts.next(), parts.next()) { - let mut comment = LevelComment::from_robtop_str(raw_comment)?; - - comment.user = if raw_user == "1~~9~~10~~11~~14~~15~~16~" { - None - } else { - Some(CommentUser::from_robtop_str(raw_user)?) - }; - - Ok(comment) - } else { - Err(ResponseError::UnexpectedFormat) - } - }) - .collect() -} - pub fn parse_get_gj_acccount_comments_response(response: &str) -> Result, ResponseError> { if response == "-1" { return Err(ResponseError::NotFound) @@ -226,16 +194,4 @@ pub fn parse_get_gj_acccount_comments_response(response: &str) -> Result Date: Sat, 29 Oct 2022 22:20:11 -0400 Subject: [PATCH 08/29] Account (#3) * Started account login route * Not really sure I did something, account login works * Added result return type to authenticated user * renamed the password_hash properly in serde rename Co-authored-by: Ryder Co-authored-by: Ryder --- Cargo.toml | 6 ++ src/request/account.rs | 154 +++++++++++++++++++++++++++++++++++++++++ src/request/level.rs | 57 ++++++++++++++- src/request/mod.rs | 25 +++++++ tests/account.rs | 20 ++++++ tests/response.rs | 3 +- 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 src/request/account.rs create mode 100644 tests/account.rs diff --git a/Cargo.toml b/Cargo.toml index 0647c60..f9fa2b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,16 +9,22 @@ edition = "2018" [dependencies] serde = {version = "1.0.118", features = ["derive"]} base64 = "0.11.0" +base64-url = "1.4.13" log = {version = "0.4.8", features = ["release_max_level_off"]} percent-encoding = "2.1.0" itoa = "0.4.4" dtoa = "0.4.4" flate2 = {version = "1.0.14", features = ["zlib"], default-features=false} +rand = "0.8.5" +reqwest = "0.11" +async-trait = "0.1.57" +tokio = { version = "1.21.2", features = ["macros", "rt"]} [dev-dependencies] # benchmark env_logger = "0.7.1" criterion = "0.3" +tokio-test = "*" [build-dependencies] serde = {version = "1.0.104", features = ["derive"]} diff --git a/src/request/account.rs b/src/request/account.rs new file mode 100644 index 0000000..962ea20 --- /dev/null +++ b/src/request/account.rs @@ -0,0 +1,154 @@ +use std::fmt; +use std::fmt::Formatter; +use reqwest::{Error, Response}; +use serde::{Serialize}; + +use crate::{ + request::{/*AuthenticatedUser,*/ REQUEST_BASE_URL} +}; +use crate::request::AuthenticatedUser; + +pub const ACCOUNT_LOGIN_ENPOINT: &str = "accounts/loginGJAccount.php"; +pub const ACCOUNT_SECRET: &str = "Wmfv3899gc9"; + +#[derive(Debug, Clone, Serialize, Hash)] +pub struct LoginRequest<'a> { + + /// The Unique Device IDentifier (UDID) of the user to authenticate + /// + /// ## GD Internals: + /// This field is called `udid` in the boomlings API + /// The value of this value can be randomly generated + /// The digits must be between 100,000 and 100,000,000 + /// This will succeed as long as these conditions are met + pub udid: &'a str, + + /// The username of the user to authenticate + /// + /// ## GD Internals: + /// This field is called `userName` in the boomlings API + #[serde(rename = "userName")] + pub user_name: &'a str, + + /// The unencrypted password of the user to authenticate + /// + /// ## GD Internals: + /// This field is called `password` in the boomlings API + pub password: &'a str, + + pub secret: &'a str, + + //// The base request data + // #[serde(borrow)] + // pub base: BaseRequest<'a>, +} + +impl<'a> LoginRequest<'a> { + const_setter!(user_name: &'a str); + const_setter!(password: &'a str); + + pub fn default() -> Self { + LoginRequest{ + udid: "199095", + user_name: "", + password: "", + secret: ACCOUNT_SECRET + } + } + + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, ACCOUNT_LOGIN_ENPOINT) + } + + async fn execute(&self) -> Result { + let reqwest_client = reqwest::Client::new(); + println!("{}?{}", self.to_url(), self.to_string()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header("Content-Type", "application/x-www-form-urlencoded") + .send() + .await + } + + pub async fn to_authenticated_user(&self) -> Result { + match self.execute().await { + Ok(login_result) => { + + let response_body = login_result.text().await.unwrap(); + if response_body.eq("-1") { + return Err(AuthenticationError("invalid credentials".into())) + } + + Ok(AuthenticatedUser { + account_id: response_body.splitn(2, ",").next().unwrap().parse::().unwrap(), + password_hash: base64::encode(&xor(self.password.as_bytes().to_vec(), "37526".as_bytes())) + }) + + // Box::new(AuthenticatedUser{ + // account_id: account_id, + // password_hash: password + // }) + } + Err(login_error) => { + Err(AuthenticationError(login_error.to_string())) + } + } + } +} + + +fn xor(s: Vec, key: &[u8]) -> Vec { + let mut b = key.iter().cycle(); + s.into_iter().map(|x| x ^ b.next().unwrap()).collect() +} + +// fn robtop_encode_level_password(pw: String) -> [u8; 20] { +// let mut password = [b'0'; 20]; +// password[0] = b'1'; +// +// let mut itoa_buf = [0u8; 6]; +// +// let n = itoa::write(&mut itoa_buf[..], pw).unwrap(); +// +// // ensure the password is padded with zeroes as needed +// for i in 0..n { +// password[7 - n + i] = itoa_buf[i]; +// } +// +// // We need to do the xor **before** we get the base64 encoded data +// util::cyclic_xor(&mut password[..], "37526"); +// +// password +// } + +#[derive(Debug, Clone)] +pub struct AuthenticationError(String); + +impl std::error::Error for AuthenticationError {} + +impl fmt::Display for AuthenticationError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ToString for LoginRequest<'_> { + fn to_string(&self) -> String { + super::to_string(self) + } +} + +#[cfg(test)] +mod tests { + use crate::request::account::LoginRequest; + + #[tokio::test] + async fn serialize_login_request() { + let request = LoginRequest::default() + .user_name("Ryder") + .password("PASS HERE"); + + println!("{:?}", request.to_authenticated_user().await.unwrap()); + } +} diff --git a/src/request/level.rs b/src/request/level.rs index 1364281..13da37e 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -1,3 +1,6 @@ +use reqwest::{ + Response, Error +}; use crate::{ model::{ level::{DemonRating, LevelLength, LevelRating}, @@ -6,6 +9,8 @@ use crate::{ request::{BaseRequest, GD_21, REQUEST_BASE_URL}, }; use serde::{Deserialize, Serialize, Serializer}; +use async_trait::async_trait; +use crate::request::Executable; pub const DOWNLOAD_LEVEL_ENDPOINT: &str = "downloadGJLevel22.php"; pub const SEARCH_LEVEL_ENDPOINT: &str = "getGJLevels21.php"; @@ -56,6 +61,10 @@ impl<'a> LevelRequest<'a> { base[with_base]: BaseRequest<'a> } + const_setter! { + level_id: u64 + } + const_setter! { /// Sets the value of the `inc` field /// @@ -89,6 +98,20 @@ impl<'a> LevelRequest<'a> { } } +#[async_trait] +impl Executable for LevelRequest<'_> { + async fn execute(&self) -> Result { + let reqwest_client = reqwest::Client::new(); + println!("{}?{}", self.to_url(), self.to_string()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header("Content-Type", "application/x-www-form-urlencoded") + .send() + .await + } +} + impl ToString for LevelRequest<'_> { fn to_string(&self) -> String { super::to_string(self) @@ -593,6 +616,20 @@ impl<'a> LevelsRequest<'a> { } } +#[async_trait] +impl Executable for LevelsRequest<'_>{ + async fn execute(&self) -> Result { + let reqwest_client = reqwest::Client::new(); + println!("{}?{}", self.to_url(), self.to_string()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header("Content-Type", "application/x-www-form-urlencoded") + .send() + .await + } +} + impl ToString for LevelsRequest<'_> { fn to_string(&self) -> String { super::to_string(self) @@ -675,9 +712,11 @@ mod tests { model::level::LevelLength, request::level::{CompletionFilter, LevelRequestType, LevelsRequest, SearchFilters}, }; + use crate::request::Executable; + use crate::request::level::LevelRequest; - #[test] - fn serialize_levels_request() { + #[tokio::test] + async fn serialize_levels_request() { let request = LevelsRequest::default() .request_type(LevelRequestType::MostLiked) @@ -688,6 +727,7 @@ mod tests { 18018958, 21373201, 22057275, 22488444, 22008823, 23144971, 17382902, 87600, 22031889, 22390740, 22243264, 21923305, ]), )); + println!("{:?}", request.execute().await.unwrap().text().await.unwrap()); assert_eq!( super::super::to_string(request), @@ -696,4 +736,17 @@ mod tests { 22031889,22390740,22243264,21923305)&onlyCompleted=0&uncompleted=1" ); } + + #[tokio::test] + async fn serialize_level_request() { + let request = LevelRequest::default() + .level_id(17448979); + + println!("{:?}", request.execute().await.unwrap().text().await.unwrap()); + + assert_eq!( + super::super::to_string(request), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&levelID=17448979&inc=0&extra=0" + ); + } } diff --git a/src/request/mod.rs b/src/request/mod.rs index 29dc423..070640d 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -7,6 +7,8 @@ use crate::{model::GameVersion, serde::RequestSerializer}; use serde::{Deserialize, Serialize}; +use async_trait::async_trait; +use reqwest::{Error, Response}; macro_rules! const_setter { ($name: ident, $field: ident, $t: ty) => { @@ -43,6 +45,7 @@ macro_rules! const_setter { pub mod comment; pub mod level; pub mod user; +pub mod account; pub const REQUEST_BASE_URL: &str = "http://www.boomlings.com/database/"; @@ -106,6 +109,23 @@ impl Default for BaseRequest<'static> { } } +#[derive(Debug, Serialize)] +pub struct AuthenticatedUser { + /// The account ID of the authenticated user + /// + /// ## GD Internals: + /// This field is called `accountID` in the boomlings API + #[serde(rename = "accountID")] + pub account_id: u64, + + /// The encrypted password of the authenticated user, this is sensitive data as it can be used to act as a user on endpoints requiring `gjp` + /// + /// ## GD Internals: + /// This field is called `gjp` in the boomlings API + #[serde(rename = "gjp")] + pub password_hash: String, +} + pub(crate) fn to_string(request: S) -> String { let mut output = Vec::new(); let mut serializer = RequestSerializer::new(&mut output); @@ -114,3 +134,8 @@ pub(crate) fn to_string(request: S) -> String { String::from_utf8(output).unwrap() } + +#[async_trait] +pub trait Executable { + async fn execute(&self) -> Result; +} \ No newline at end of file diff --git a/tests/account.rs b/tests/account.rs new file mode 100644 index 0000000..4fe7966 --- /dev/null +++ b/tests/account.rs @@ -0,0 +1,20 @@ +use dash_rs::request; +use dash_rs::request::{BaseRequest}; + +macro_rules! blocking_await { + ($e:expr) => { + tokio_test::block_on($e) + }; +} + +#[tokio::test] +async fn login_gj_account_test() { + let base = BaseRequest::default(); + let test = request::account::LoginRequest::with_base(base) + .user_name("Ryder") + .password("PASS HERE") + .execute() + .await.unwrap(); + + println!("{:?}", test) +} \ No newline at end of file diff --git a/tests/response.rs b/tests/response.rs index cd08170..c9c1e0e 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -1,3 +1,5 @@ +use dash_rs::request; + // This string has been manually edited to ensure that the cases of duplicate and missing // songs/creators are also covered by the unit test const GET_GJ_LEVELS_RESPONSE: &str = "1:62953227:2:Noice:5:1:6:14098234:8:10:9:30:10:329795:12:0:13:21:14:16024:17::43:0:25::18:5:19:24981:42:1:45:30320:3:Tm9pY2UgbGV2ZWwsIGhvcGUgeW91IGxpa2UgaXQ=:15:3:30:0:31:0:37:0:38:0:39:5:46:1:47:2:35:778510|1:63362544:2:Happy Day Gd:5:4:6:14098234:8:10:9:30:10:16162:12:0:13:21:14:2772:17::43:0:25::18:4:19:24979:42:0:45:38476:3:NyB5ZWFycyBvZiBqb3lzIGFuZCBzb3Jyb3dzLCB0aGUgYmVzdCBnYW1lIEkga25ldyBhbmQgSSB3aWxsIHN0YXksIGNvbW1lbnQgaG93IG1hbnkgeWVhcnMgeW91IGhhdmUgYmVlbiBhbmQgd2hhdCBtb3RpdmF0ZWQgeW91IHRvIHN0YXk=:15:3:30:0:31:0:37:3:38:1:39:4:46:1:47:2:35:936243|1:63336521:2:Sound Visualization:5:1:6:4123296:8:10:9:50:10:47521:12:0:13:21:14:2951:17::43:6:25::18:8:19:24979:42:1:45:26229:3:dmlzdWFsIGVmZmVjdHM=:15:3:30:0:31:0:37:3:38:1:39:8:46:1:47:2:35:778510|1:63335504:2:Mind Control:5:2:6:10130943:8:10:9:30:10:51434:12:0:13:21:14:4120:17::43:0:25::18:5:19:24979:42:1:45:49951:3:SXQncyBkb25lIHlhYWF5:15:3:30:0:31:0:37:2:38:1:39:5:46:1:47:2:35:763439|1:63333766:2:An Ode to Time:5:8:6:7226087:8:10:9:10:10:10795:12:0:13:21:14:716:17:1:43:3:25::18:10:19:24979:42:0:45:65535:3:QW5kIGl0IGFsbCBjb21lcyBjcmFzaGluZyBkb3duLiAwOC8xMC8yMC4=:15:4:30:0:31:0:37:3:38:1:39:10:46:1:47:2:35:896364|1:63292359:2:AnnoZone:5:2:6:5897998:8:10:9:50:10:7890:12:0:13:21:14:636:17::43:6:25::18:8:19:24979:42:1:45:51592:3:VGhlIEFubm8gU2VyaWVzIGhhcyByZXR1cm5lZCBhZnRlciAyIHllYXJzIHdpdGggYSAzcmQgbGV2ZWwhIERlZGljYXRlZCB0byB0aGUgQnJveXMuIE1vcmUgQW5ubyBTZXJpZXMgbGV2ZWxzIHRvIGNvbWUuLi4_:15:3:30:0:31:0:37:0:38:1:39:7:46:1:47:2:35:638150|1:63260507:2:Trouble:5:3:6:14221993:8:10:9:50:10:4512:12:0:13:21:14:323:17::43:6:25::18:8:19:24979:42:1:45:58854:3:SSBzaG91bGQgb2Yga25vd24geW91IHdlcmUgdHJvdWJsZS4uLi4uLiAgICAgICAgICAgICBoaQ==:15:3:30:0:31:0:37:1:38:1:39:8:46:1:47:2:35:939885|1:63254272:2:AdrenaLines:5:2:6:116033399:8:10:9:40:10:34848:12:0:13:21:14:2419:17::43:5:25::18:6:19:24979:42:0:45:32956:3:ZGVjbyBsdmw_ISBlbmpveSB1d3U=:15:3:30:0:31:0:37:2:38:1:39:6:46:1:47:2:35:887253|1:63232525:2:Metropolis:5:3:6:1647052:8:10:9:10:10:99385:12:0:13:21:14:5506:17:1:43:3:25::18:10:19:24979:42:1:45:65535:3:V2VsY29tZS4uLg==:15:3:30:0:31:0:37:1:38:1:39:10:46:1:47:2:35:674039|1:61865319:2:Utopia:5:4:6:5570844:8:10:9:50:10:148912:12:0:13:21:14:6961:17::43:6:25::18:8:19:24979:42:0:45:13533:3:dXBkYXRlZCwgc2Vjb25kIHBhcnQgZG9lc250IGxvb2sgc28gdWdseSBub3c=:15:3:30:0:31:0:37:3:38:1:39:8:46:1:47:2:35:761926#1647052:DesTicY:95952|4123296:Cdpre:1478680|5570844:Axils:1341135|7226087:Pauze:1705254|8908442:Nikce:2517174|10130943:FaekI:1727914|14098234:AleXins:4322668|14221993:IFuse:5633975|116033399:KumoriGD:11439344#1~|~638150~|~2~|~-ThunderZone v2-~|~3~|~30~|~4~|~Waterflame~|~5~|~8.78~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F638000%2F638150_-ThunderZone-v2-.mp3~|~7~|~UCVuv5iaVR55QXIc_BHQLakA~|~8~|~1~:~1~|~674039~|~2~|~Crystal Tokyo~|~3~|~746~|~4~|~Fantomenk~|~5~|~10.54~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F674000%2F674039_Crystal-Tokyo.mp3~|~7~|~UCMSBjXolfz29kxnXpBa7LJA~|~8~|~1~:~1~|~761926~|~2~|~mistmurk + 3MBER - Utopia~|~3~|~49123~|~4~|~mistmurk~|~5~|~7.76~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F761000%2F761926_mistmurk--3mber---Utopia.mp3~|~7~|~~|~8~|~1~:~1~|~763439~|~2~|~ColBreakz - Mind Control~|~3~|~47795~|~4~|~ColBreakz~|~5~|~10~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F763000%2F763439_ColBreakz---Mind-Control.mp3~|~7~|~~|~8~|~1~:~1~|~778510~|~2~|~Hazmat~|~3~|~23384~|~4~|~CricketSaysChill~|~5~|~1.8~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F778000%2F778510_Hazmat.mp3%3Ff1512785304~|~7~|~~|~8~|~1~:~1~|~852209~|~2~|~Fried Sushi~|~3~|~28916~|~4~|~lchavasse~|~5~|~5.88~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F852000%2F852209_Fried-Sushi.mp3%3Ff1552100587~|~7~|~~|~8~|~1~:~1~|~887253~|~2~|~Adrenaline~|~3~|~51089~|~4~|~PsoGnar~|~5~|~10.35~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F887000%2F887253_Adrenaline.mp3%3Ff1570984144~|~7~|~~|~8~|~1~:~1~|~896364~|~2~|~Beethoven - Moonlight Sonata 3rd Movement (meganeko remix)~|~3~|~48917~|~4~|~meganeko~|~5~|~9.45~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F896000%2F896364_Beethoven---Moonlight-Sona.mp3%3Ff1575491260~|~7~|~UCP3M2myndqXuAEKKnqm_7SQ~|~8~|~1~:~1~|~936243~|~2~|~Phaera - Ignition~|~3~|~50872~|~4~|~TheArcadium~|~5~|~5.76~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F936000%2F936243_Phaera---Ignition.mp3%3Ff1590147327~|~7~|~~|~8~|~1#11389:0:10#f687963dcfd37f857633563ee28b0cfadc727c97"; @@ -5,7 +7,6 @@ const GET_GJ_LEVELS_RESPONSE: &str = "1:62953227:2:Noice:5:1:6:14098234:8:10:9:3 #[test] fn process_get_gj_levels_response() { let levels = dash_rs::response::parse_get_gj_levels_response(GET_GJ_LEVELS_RESPONSE); - assert!(levels.is_ok(), "{}", levels.unwrap_err()); let levels = levels.unwrap(); From 833c5d3d51e2c7f95e556987ad75c2e6afd05b18 Mon Sep 17 00:00:00 2001 From: Ryder <36455274+zRyder@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:52:58 -0400 Subject: [PATCH 09/29] Post delete comment (#5) * Added delete request struct and made changes to authenticated user * Added models for requesting comment deletion and upload, minor changes to base requests for account auth endpoints * Removed deprecated test code * Used Cow type instead of String for encoded data * I don't know I cleaned up something probably Co-authored-by: Ryder --- Cargo.toml | 2 +- src/request/account.rs | 46 ++------ src/request/comment.rs | 252 ++++++++++++++++++++++++++++++++++++++++- src/request/mod.rs | 18 ++- src/util.rs | 13 +++ tests/account.rs | 18 +-- 6 files changed, 290 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 923e345..73cb55b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] serde = {version = "1.0.118", features = ["derive"]} base64 = "0.11.0" -base64-url = "1.4.13" +sha1_smol = "1.0.0" log = {version = "0.4.8" } percent-encoding = "2.1.0" itoa = "0.4.4" diff --git a/src/request/account.rs b/src/request/account.rs index 962ea20..b24ea3d 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -4,12 +4,13 @@ use reqwest::{Error, Response}; use serde::{Serialize}; use crate::{ - request::{/*AuthenticatedUser,*/ REQUEST_BASE_URL} + util, + request::{REQUEST_BASE_URL} }; use crate::request::AuthenticatedUser; pub const ACCOUNT_LOGIN_ENPOINT: &str = "accounts/loginGJAccount.php"; -pub const ACCOUNT_SECRET: &str = "Wmfv3899gc9"; +pub const XOR_KEY: &str = "37526"; #[derive(Debug, Clone, Serialize, Hash)] pub struct LoginRequest<'a> { @@ -36,6 +37,7 @@ pub struct LoginRequest<'a> { /// This field is called `password` in the boomlings API pub password: &'a str, + /// The secret token to call /database/accounts routes pub secret: &'a str, //// The base request data @@ -52,7 +54,7 @@ impl<'a> LoginRequest<'a> { udid: "199095", user_name: "", password: "", - secret: ACCOUNT_SECRET + secret: super::ACCOUNT_SECRET } } @@ -66,7 +68,7 @@ impl<'a> LoginRequest<'a> { reqwest_client .post(self.to_url()) .body(self.to_string()) - .header("Content-Type", "application/x-www-form-urlencoded") + .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) .send() .await } @@ -74,21 +76,16 @@ impl<'a> LoginRequest<'a> { pub async fn to_authenticated_user(&self) -> Result { match self.execute().await { Ok(login_result) => { - let response_body = login_result.text().await.unwrap(); if response_body.eq("-1") { return Err(AuthenticationError("invalid credentials".into())) } Ok(AuthenticatedUser { + user_name: self.user_name, account_id: response_body.splitn(2, ",").next().unwrap().parse::().unwrap(), - password_hash: base64::encode(&xor(self.password.as_bytes().to_vec(), "37526".as_bytes())) + password_hash: base64::encode_config(&util::xor(self.password.as_bytes().to_vec(), XOR_KEY.as_bytes()), base64::URL_SAFE).into() }) - - // Box::new(AuthenticatedUser{ - // account_id: account_id, - // password_hash: password - // }) } Err(login_error) => { Err(AuthenticationError(login_error.to_string())) @@ -97,31 +94,6 @@ impl<'a> LoginRequest<'a> { } } - -fn xor(s: Vec, key: &[u8]) -> Vec { - let mut b = key.iter().cycle(); - s.into_iter().map(|x| x ^ b.next().unwrap()).collect() -} - -// fn robtop_encode_level_password(pw: String) -> [u8; 20] { -// let mut password = [b'0'; 20]; -// password[0] = b'1'; -// -// let mut itoa_buf = [0u8; 6]; -// -// let n = itoa::write(&mut itoa_buf[..], pw).unwrap(); -// -// // ensure the password is padded with zeroes as needed -// for i in 0..n { -// password[7 - n + i] = itoa_buf[i]; -// } -// -// // We need to do the xor **before** we get the base64 encoded data -// util::cyclic_xor(&mut password[..], "37526"); -// -// password -// } - #[derive(Debug, Clone)] pub struct AuthenticationError(String); @@ -147,7 +119,7 @@ mod tests { async fn serialize_login_request() { let request = LoginRequest::default() .user_name("Ryder") - .password("PASS HERE"); + .password("PASSHERE"); println!("{:?}", request.to_authenticated_user().await.unwrap()); } diff --git a/src/request/comment.rs b/src/request/comment.rs index ce3f021..4ceaae2 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -1,14 +1,20 @@ //! Module containing request structs for retrieving profile/level comments -use crate::{ - model::level::Level, - request::{BaseRequest, GD_21, REQUEST_BASE_URL}, -}; +use std::borrow::Cow; +use reqwest::{Error, Response}; +use crate::{model::level::Level, request::{BaseRequest, GD_21, REQUEST_BASE_URL, AuthenticatedUser}, util}; use serde::Serialize; +use crate::request::Executable; +use async_trait::async_trait; pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php"; pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php"; pub const COMMENT_HISTORY_ENDPOINT: &str = "getGJCommentHistory.php"; +pub const UPLOAD_COMMENT_ENDPOINT: &str = "uploadGJComment21.php"; +pub const DELETE_COMMENT_ENDPOINT: &str = "deleteGJComment20.php"; + +pub const COMMENT_CHK_SALT: &str = "xPT6iUrtws0J"; +pub const COMMENT_XOR_CHK_KEY: &str = "29481"; /// The different orderings that can be requested for level comments #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] @@ -255,9 +261,180 @@ impl<'a> CommentHistoryRequest<'a> { } } +impl<'a> ToString for CommentHistoryRequest<'a> { + fn to_string(&self) -> String { + super::to_string(self) + } +} + +#[async_trait] +impl<'a> Executable for CommentHistoryRequest<'a> { + async fn execute(&self) -> Result { + let reqwest_client = reqwest::Client::new(); + println!("{:?}", self.to_string()); + println!("{:?}", self.to_url()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) + .send() + .await + } +} + +#[derive(Debug, Clone, Serialize, Hash)] +pub struct UploadCommentRequest<'a> { + /// The base request data + pub base: BaseRequest<'a>, + + /// The authenticated user data + pub authenticated_user: AuthenticatedUser<'a>, + + /// The content of the comment, this value will be base64 url encoded + pub comment: Cow<'a, str>, + + /// The id of the level the comment to upload is posted to + /// ## GD Internals: + /// This field is called `levelID` in the boomlings API + #[serde(rename = "levelID")] + pub level_id: u64, + + /// The percent completed to display on the comment, this should be a number between 0 or 100 if present + pub percent: u8, + + /// The CHK for /uploadGJComment21.php + pub chk: Cow<'a, str> +} + +impl<'a> UploadCommentRequest<'a> { + const_setter!(level_id: u64); + const_setter!(percent: u8); + + pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + Self::with_base(GD_21, authenticated_user, level_id) + } + + pub const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + UploadCommentRequest{ + base, + authenticated_user, + comment: Cow::Borrowed(""), + level_id, + percent: 0, + chk: Cow::Borrowed("") + } + } + + pub fn comment(mut self, comment_content: String) -> Self { + self.comment = base64::encode_config(comment_content.as_bytes(), base64::URL_SAFE).into(); + self + } + + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, UPLOAD_COMMENT_ENDPOINT) + } + + fn generate_chk(mut self) -> Self { + self.chk = format!("{}{}{}{}{}{}", self.authenticated_user.user_name, self.comment, self.level_id, self.percent, 0, COMMENT_CHK_SALT) + .into(); + + let xor_chk = util::xor(util::sha_encrypt(self.chk).as_bytes().to_vec(), COMMENT_XOR_CHK_KEY.as_bytes()); + self.chk = base64::encode_config(xor_chk.as_slice(), base64::URL_SAFE).into(); + self + } +} + +impl ToString for UploadCommentRequest<'_> { + fn to_string(&self) -> String { + super::to_string(self) + } +} + +#[async_trait] +impl<'a> Executable for UploadCommentRequest<'a> { + async fn execute(&self) -> Result { + + let reqwest_client = reqwest::Client::new(); + println!("{:?}", self.to_string()); + println!("{:?}", self.to_url()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) + .send() + .await + } +} + +#[derive(Debug, Clone, Serialize, Hash)] +pub struct DeleteCommentRequest<'a> { + /// The base request data + pub base: BaseRequest<'a>, + + /// The authenticated user data + pub authenticated_user: AuthenticatedUser<'a>, + + /// The id of the level comment to delete + /// ## GD Internals: + /// This field is called `commentID` in the boomlings API + #[serde(rename = "commentID")] + pub comment_id: u64, + + /// The id of the level the comment to delete is posted to + /// ## GD Internals: + /// This field is called `levelID` in the boomlings API + #[serde(rename = "levelID")] + pub level_id: u64, +} + +impl<'a> DeleteCommentRequest<'a> { + const_setter!(comment_id: u64); + const_setter!(level_id: u64); + + pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + Self::with_base(GD_21, authenticated_user, level_id) + } + + pub const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + DeleteCommentRequest { + base, + authenticated_user, + comment_id: 0, + level_id + } + } + + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, DELETE_COMMENT_ENDPOINT) + } +} + +#[async_trait] +impl<'a> Executable for DeleteCommentRequest<'a> { + async fn execute(&self) -> Result { + let reqwest_client = reqwest::Client::new(); + println!("{:?}", self.to_string()); + println!("{:?}", self.to_url()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) + .send() + .await + } +} + +impl ToString for DeleteCommentRequest<'_> { + fn to_string(&self) -> String { + super::to_string(self) + } +} + #[cfg(test)] mod tests { - use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest}; + use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest, SortMode}; + use crate::request::Executable; + use crate::response::parse_get_gj_comments_response; #[test] fn serialize_level_comments() { @@ -301,4 +478,69 @@ mod tests { assert_eq!(super::super::to_string(request), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2") } + + #[tokio::test] + async fn upload_comment() { + let request = crate::request::account::LoginRequest::default() + .user_name("Ryder") + .password("PASSHERE"); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + let comment_upload_request = crate::request::comment::UploadCommentRequest::new(login_response, 85179632) + .comment(String::from("More tests still ignore me")) + .percent(69) + .generate_chk() + .execute() + .await + .unwrap() + .text().await.unwrap(); + + println!("{:?}", comment_upload_request) + } + + #[tokio::test] + async fn delete_comment() { + let request = crate::request::account::LoginRequest::default() + .user_name("Ryder") + .password("PASSHERE"); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + + let comment_history_request = CommentHistoryRequest::new(3713125) + .sort_mode(SortMode::Recent) + .limit(1) + .page(0); + + let comment_history_response = comment_history_request.execute() + .await + .unwrap() + .text() + .await + .unwrap(); + + let comment = parse_get_gj_comments_response(comment_history_response.as_str()) + .unwrap(); + + let comment_id = comment.get(0).unwrap().comment_id; + + println!("{}", &comment_id); + + let comment_delete_request = crate::request::comment::DeleteCommentRequest::new(login_response, 85179632) + .comment_id(comment_id); + + let comment_delete_response = comment_delete_request.execute() + .await + .unwrap() + .text() + .await + .unwrap(); + + println!("{}", comment_delete_response) + } } diff --git a/src/request/mod.rs b/src/request/mod.rs index 070640d..e5e9875 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -5,6 +5,7 @@ //! making/proxying requests for the boomlings servers seems rather useless to me, as they already //! contain a lot of boomlings-specific fields. +use std::borrow::Cow; use crate::{model::GameVersion, serde::RequestSerializer}; use serde::{Deserialize, Serialize}; use async_trait::async_trait; @@ -48,6 +49,10 @@ pub mod user; pub mod account; pub const REQUEST_BASE_URL: &str = "http://www.boomlings.com/database/"; +pub const ACCOUNT_SECRET: &str = "Wmfv3899gc9"; + +pub const CONTENT_TYPE: &str = "Content-Type"; +pub const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; /// A `BaseRequest` instance that has all its fields set to the /// same values a Geometry Dash 2.1 client would use @@ -109,8 +114,15 @@ impl Default for BaseRequest<'static> { } } -#[derive(Debug, Serialize)] -pub struct AuthenticatedUser { +#[derive(Debug, Serialize, Default, Clone, Hash)] +pub struct AuthenticatedUser<'a> { + /// The user name of the authenticated user + /// + /// ## GD Internals: + /// This field is called `userName` in the boomlings API + #[serde(rename = "userName")] + pub user_name: &'a str, + /// The account ID of the authenticated user /// /// ## GD Internals: @@ -123,7 +135,7 @@ pub struct AuthenticatedUser { /// ## GD Internals: /// This field is called `gjp` in the boomlings API #[serde(rename = "gjp")] - pub password_hash: String, + pub password_hash: Cow<'a, str> } pub(crate) fn to_string(request: S) -> String { diff --git a/src/util.rs b/src/util.rs index dd790de..025b44a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,8 @@ //! Module containing various utility functions related to processing Geometry Dash data +use std::borrow::Cow; use serde::Serializer; +use sha1_smol; /// Performs RobTop's XOR en-/decoding routine on `encoded` using `key` /// @@ -47,6 +49,17 @@ pub(crate) mod default_to_none { } } +pub(crate) fn sha_encrypt(content: Cow) -> String { + let mut m = sha1_smol::Sha1::new(); + m.update(content.as_bytes()); + m.digest().to_string() +} + +pub fn xor(s: Vec, key: &[u8]) -> Vec { + let mut b = key.iter().cycle(); + s.into_iter().map(|x| x ^ b.next().unwrap()).collect() +} + pub(crate) fn false_to_empty_string(b: &bool, serializer: S) -> Result { match *b { true => serializer.serialize_str("1"), diff --git a/tests/account.rs b/tests/account.rs index 4fe7966..e57c232 100644 --- a/tests/account.rs +++ b/tests/account.rs @@ -1,20 +1,12 @@ use dash_rs::request; -use dash_rs::request::{BaseRequest}; - -macro_rules! blocking_await { - ($e:expr) => { - tokio_test::block_on($e) - }; -} #[tokio::test] async fn login_gj_account_test() { - let base = BaseRequest::default(); - let test = request::account::LoginRequest::with_base(base) + let request = request::account::LoginRequest::default() .user_name("Ryder") - .password("PASS HERE") - .execute() - .await.unwrap(); + .password("PASS_HERE"); + + let result = request.to_authenticated_user().await.unwrap(); - println!("{:?}", test) + println!("{:?}", result); } \ No newline at end of file From da92fbe53226d4b5b0562ae5cc3a00f6e8b51ce8 Mon Sep 17 00:00:00 2001 From: Ryder Date: Mon, 31 Oct 2022 16:34:07 -0400 Subject: [PATCH 10/29] Added more verbose unit test cases --- src/request/account.rs | 11 +++-- src/request/comment.rs | 104 ++++++++++------------------------------- src/request/level.rs | 6 +-- src/request/user.rs | 19 ++++++++ tests/comment.rs | 69 +++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 87 deletions(-) diff --git a/src/request/account.rs b/src/request/account.rs index b24ea3d..7666421 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -51,7 +51,7 @@ impl<'a> LoginRequest<'a> { pub fn default() -> Self { LoginRequest{ - udid: "199095", + udid: "100000", user_name: "", password: "", secret: super::ACCOUNT_SECRET @@ -118,9 +118,12 @@ mod tests { #[tokio::test] async fn serialize_login_request() { let request = LoginRequest::default() - .user_name("Ryder") - .password("PASSHERE"); + .user_name("TestUser") + .password("PLAIN_TEXT_PASS_HERE"); - println!("{:?}", request.to_authenticated_user().await.unwrap()); + assert_eq!( + request.to_string(), + "udid=100000&userName=TestUser&password=PLAIN_TEXT_PASS_HERE&secret=Wmfv3899gc9" + ); } } diff --git a/src/request/comment.rs b/src/request/comment.rs index 4ceaae2..9956ab4 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -325,7 +325,7 @@ impl<'a> UploadCommentRequest<'a> { } } - pub fn comment(mut self, comment_content: String) -> Self { + pub fn comment(mut self, comment_content: &str) -> Self { self.comment = base64::encode_config(comment_content.as_bytes(), base64::URL_SAFE).into(); self } @@ -432,17 +432,18 @@ impl ToString for DeleteCommentRequest<'_> { #[cfg(test)] mod tests { - use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest, SortMode}; - use crate::request::Executable; - use crate::response::parse_get_gj_comments_response; + use std::borrow::Cow; + use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest, UploadCommentRequest, DeleteCommentRequest, SortMode}; + use crate::request::{AuthenticatedUser}; + + const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser { + user_name: "TestUser", + account_id: 472634, + password_hash: Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") +}; #[test] fn serialize_level_comments() { - if let Err(err) = env_logger::builder().is_test(true).try_init() { - // nothing to make the tests fail over - eprintln!("Error setting up env_logger: {:?}", err) - } - let request = LevelCommentsRequest::new(1234).most_liked().page(2).limit(15); assert_eq!( @@ -453,11 +454,6 @@ mod tests { #[test] fn serialize_profile_comments() { - if let Err(err) = env_logger::builder().is_test(true).try_init() { - // nothing to make the tests fail over - eprintln!("Error setting up env_logger: {:?}", err) - } - let request = ProfileCommentsRequest::new(1710032).page(2); assert_eq!( @@ -468,79 +464,29 @@ mod tests { #[test] fn serialize_comment_history() { - if let Err(err) = env_logger::builder().is_test(true).try_init() { - // nothing to make the tests fail over - eprintln!("Error setting up env_logger: {:?}", err) - } - let request = CommentHistoryRequest::new(159782398) + .sort_mode(SortMode::Recent) + .page(0) .limit(2); - assert_eq!(super::super::to_string(request), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2") + assert_eq!(request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2") } - #[tokio::test] - async fn upload_comment() { - let request = crate::request::account::LoginRequest::default() - .user_name("Ryder") - .password("PASSHERE"); - - let login_response = request.to_authenticated_user() - .await - .unwrap(); - - let comment_upload_request = crate::request::comment::UploadCommentRequest::new(login_response, 85179632) - .comment(String::from("More tests still ignore me")) - .percent(69) - .generate_chk() - .execute() - .await - .unwrap() - .text().await.unwrap(); + #[test] + fn serialize_upload_comment() { + let request = UploadCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) + .comment("This is a test comment") + .percent(56) + .generate_chk(); - println!("{:?}", comment_upload_request) + assert_eq!(request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&comment=VGhpcyBpcyBhIHRlc3QgY29tbWVudA==&levelID=85179632&percent=56&chk=UQsGAAEACgQBVQBaAwoGVwtSDQIEWAYOUFEAVQoIBVtWDwEHDQEJVA==") } - #[tokio::test] - async fn delete_comment() { - let request = crate::request::account::LoginRequest::default() - .user_name("Ryder") - .password("PASSHERE"); - - let login_response = request.to_authenticated_user() - .await - .unwrap(); - - - let comment_history_request = CommentHistoryRequest::new(3713125) - .sort_mode(SortMode::Recent) - .limit(1) - .page(0); - - let comment_history_response = comment_history_request.execute() - .await - .unwrap() - .text() - .await - .unwrap(); - - let comment = parse_get_gj_comments_response(comment_history_response.as_str()) - .unwrap(); - - let comment_id = comment.get(0).unwrap().comment_id; - - println!("{}", &comment_id); - - let comment_delete_request = crate::request::comment::DeleteCommentRequest::new(login_response, 85179632) - .comment_id(comment_id); - - let comment_delete_response = comment_delete_request.execute() - .await - .unwrap() - .text() - .await - .unwrap(); + #[test] + fn serialize_delete_comment() { + let request = DeleteCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) + .comment_id(7000000); - println!("{}", comment_delete_response) + assert_eq!(request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&commentID=7000000&levelID=85179632") } } diff --git a/src/request/level.rs b/src/request/level.rs index 13da37e..731fa35 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -730,7 +730,7 @@ mod tests { println!("{:?}", request.execute().await.unwrap().text().await.unwrap()); assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&type=2&str=&len=2,3&diff=-&page=0&total=0&featured=1&original=0&\ twoPlayer=1&coins=0&epic=1&star=1&completedLevels=(18018958,21373201,22057275,22488444,22008823,23144971,17382902,87600,\ 22031889,22390740,22243264,21923305)&onlyCompleted=0&uncompleted=1" @@ -742,10 +742,8 @@ mod tests { let request = LevelRequest::default() .level_id(17448979); - println!("{:?}", request.execute().await.unwrap().text().await.unwrap()); - assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&levelID=17448979&inc=0&extra=0" ); } diff --git a/src/request/user.rs b/src/request/user.rs index 8a24547..997e94d 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -119,3 +119,22 @@ impl ToString for UserSearchRequest<'_> { super::to_string(self) } } + +#[cfg(test)] +mod tests { + use crate::request::user::{UserRequest, UserSearchRequest}; + + #[test] + fn serialize_user_request() { + let request = UserRequest::new(57903); + + assert_eq!(request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&targetAccountID=57903"); + } + + #[test] + fn test_user_searc_request() { + let request = UserSearchRequest::new("Ryder"); + + assert_eq!(request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&str=Ryder"); + } +} diff --git a/tests/comment.rs b/tests/comment.rs index 0e4fb69..6ec49b9 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -1,4 +1,8 @@ use dash_rs::{ + request::{ + AuthenticatedUser, + comment::{UploadCommentRequest, DeleteCommentRequest}, + }, model::{ comment::{ level::{CommentUser, LevelComment}, @@ -118,3 +122,68 @@ const PROFILE_COMMENT: ProfileComment = ProfileComment { load_save_roundtrip!(ProfileComment, PROFILE_COMMENT_DATA, PROFILE_COMMENT, "~", true); save_load_roundtrip!(ProfileComment, ProfileComment, PROFILE_COMMENT); + +#[tokio::test] +async fn upload_comment() { + let request = crate::request::account::LoginRequest::default() + .user_name("Ryder") + .password("PASSHERE"); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + let comment_upload_request = UploadCommentRequest::new(login_response, 85179632) + .comment("More tests still ignore me") + .percent(69) + .generate_chk() + .execute() + .await + .unwrap() + .text().await.unwrap(); + + println!("{:?}", comment_upload_request) +} + +#[tokio::test] +async fn delete_comment() { + let request = crate::request::account::LoginRequest::default() + .user_name("Ryder") + .password("PASSHERE"); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + + let comment_history_request = CommentHistoryRequest::new(3713125) + .sort_mode(SortMode::Recent) + .limit(1) + .page(0); + + let comment_history_response = comment_history_request.execute() + .await + .unwrap() + .text() + .await + .unwrap(); + + let comment = parse_get_gj_comments_response(comment_history_response.as_str()) + .unwrap(); + + let comment_id = comment.get(0).unwrap().comment_id; + + println!("{}", &comment_id); + + let comment_delete_request = crate::request::comment::DeleteCommentRequest::new(login_response, 85179632) + .comment_id(comment_id); + + let comment_delete_response = comment_delete_request.execute() + .await + .unwrap() + .text() + .await + .unwrap(); + + println!("{}", comment_delete_response) +} \ No newline at end of file From 0bf2a4ffdbd78afe76188038145d8086de4a51b4 Mon Sep 17 00:00:00 2001 From: Ryder Date: Tue, 15 Nov 2022 19:01:54 -0800 Subject: [PATCH 11/29] Integration testing --- tests/comment.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/comment.rs b/tests/comment.rs index 6ec49b9..e0dcb6a 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -13,6 +13,9 @@ use dash_rs::{ Base64Decoded, Thunk, }; use std::borrow::Cow; +use dash_rs::request::comment::{CommentHistoryRequest, SortMode}; +use dash_rs::request::Executable; +use dash_rs::response::parse_get_gj_comments_response; #[macro_use] mod helper; @@ -136,7 +139,6 @@ async fn upload_comment() { let comment_upload_request = UploadCommentRequest::new(login_response, 85179632) .comment("More tests still ignore me") .percent(69) - .generate_chk() .execute() .await .unwrap() From b685231b0157b36f59af9e31c45fa18edfeb3898 Mon Sep 17 00:00:00 2001 From: Ryder <36455274+zRyder@users.noreply.github.com> Date: Thu, 24 Nov 2022 16:12:59 -0800 Subject: [PATCH 12/29] Trait implementations (#7) * Added more verbose unit test cases * Integration testing * Everything but trait implementations haha * Testing fixes Co-authored-by: Ryder Co-authored-by: Ryder --- .gitignore | 3 +- Cargo.toml | 1 + descriptions/level_comment.yml | 2 +- descriptions/profile.yml | 4 +- src/model/comment/level.rs | 93 ++++++++++- src/model/mod.rs | 4 +- src/model/user/mod.rs | 170 +++++++++++++++++++- src/model/user/profile.rs | 286 ++++++++++++++++++++++++++++++--- src/request/account.rs | 35 ++-- src/request/comment.rs | 263 ++++++++++-------------------- src/request/level.rs | 54 +------ src/request/mod.rs | 59 ++++++- src/request/user.rs | 93 +++++++---- src/response.rs | 6 +- src/util.rs | 2 +- tests/account.rs | 10 +- tests/comment.rs | 199 +++++++++++------------ tests/profile.rs | 175 ++++++++++++++------ 18 files changed, 982 insertions(+), 477 deletions(-) diff --git a/.gitignore b/.gitignore index 222a392..d4943f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target .idea Cargo.lock -*.iml \ No newline at end of file +*.iml +*.env \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 73cb55b..ec56017 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ tokio = { version = "1.21.2", features = ["macros", "rt"]} env_logger = "0.7.1" criterion = "0.3" tokio-test = "*" +dotenv = "0.15.0" [build-dependencies] serde = {version = "1.0.104", features = ["derive"]} diff --git a/descriptions/level_comment.yml b/descriptions/level_comment.yml index a0a82f8..b8f01a9 100644 --- a/descriptions/level_comment.yml +++ b/descriptions/level_comment.yml @@ -27,7 +27,7 @@ indices: type: Option maps_to: progress - value: 11 - type: u8 + type: Option maps_to: mod_level use_into: true - value: 12 diff --git a/descriptions/profile.yml b/descriptions/profile.yml index 526fb3c..02527cc 100644 --- a/descriptions/profile.yml +++ b/descriptions/profile.yml @@ -36,8 +36,8 @@ indices: type: u16 maps_to: user_coins - value: 18 - type: '&''src str' - maps_to: index_18 + type: u8 + maps_to: message_state - value: 19 type: '&''src str' maps_to: index_19 diff --git a/src/model/comment/level.rs b/src/model/comment/level.rs index 49e799a..31288b1 100644 --- a/src/model/comment/level.rs +++ b/src/model/comment/level.rs @@ -6,6 +6,7 @@ use crate::{ model::user::{Color, IconType, ModLevel}, Base64Decoded, Thunk, }; +use crate::model::RobtopForm; #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] pub struct LevelComment<'a> { @@ -63,7 +64,7 @@ pub struct LevelComment<'a> { /// /// ## GD Internals /// This value is provided at index `11` - pub mod_level: ModLevel, + pub mod_level: Option, /// If this [`LevelComment`]'s text is displayed in a special color (blue for robtop, green for /// elder mods), the RGB code of that color will be stored here @@ -75,6 +76,8 @@ pub struct LevelComment<'a> { pub special_color: Option, } +impl<'a> RobtopForm for LevelComment<'a> {} + #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] pub struct CommentUser<'a> { /// This [`CommentUser`]'s name @@ -136,6 +139,7 @@ mod internal { user::Color, }; use std::io::Write; + use crate::model::user::ModLevel; struct RGBColor(u8, u8, u8); @@ -168,6 +172,91 @@ mod internal { } } - include!(concat!(env!("OUT_DIR"), "/level_comment.boilerplate")); + #[allow(non_upper_case_globals, unused_imports)] + const _level_comment: () = { + use crate::{ + serde::{DeError, HasRobtopFormat, IndexedDeserializer, IndexedSerializer, PercentDecoded, SerError, Thunk, RefThunk, Base64Decoded}, + }; + use serde::{Deserialize, Serialize}; + use std::{borrow::{Cow, Borrow}, io::Write}; + #[derive(Serialize, Deserialize)] + struct InternalLevelComment<'src, 'bor> { + #[serde(rename = "2")] + index_2: Option>>, + #[serde(rename = "3")] + index_3: u64, + #[serde(rename = "4")] + index_4: i32, + #[serde(rename = "6")] + index_6: u64, + #[serde(rename = "7")] + index_7: Option, + #[serde(rename = "9")] + index_9: &'src str, + #[serde(rename = "10")] + index_10: Option, + #[serde(rename = "11")] + index_11: Option, + #[serde(rename = "12")] + index_12: Option, + } + + impl<'src> HasRobtopFormat<'src> for LevelComment<'src> { + fn from_robtop_str(input: &'src str) -> Result { + let internal = InternalLevelComment::deserialize(&mut IndexedDeserializer::new(input, "~", true))?; + Ok(Self { + content: match internal.index_2 { + None => None, + Some (RefThunk::Unprocessed(unproc)) => + Some(Thunk::Unprocessed(unproc)), + _ => unreachable!()}, + user_id: internal.index_3, + likes: internal.index_4, + comment_id: internal.index_6, + is_flagged_spam: internal.index_7, + time_since_post: Cow::Borrowed(internal.index_9), + progress: internal.index_10, + mod_level: match internal.index_11 { + None => + None, + Some(mod_level) => { + Some(mod_level.into()) + } + }, + special_color: internal.index_12.map(|RGBColor(r, g, b)| Color::Known(r, g, b)), + user: None, + }) + } + + fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { + let internal = InternalLevelComment { + index_2: self.content.as_ref().map(|t| t.as_ref_thunk()), + index_3: self.user_id, + index_4: self.likes, + index_6: self.comment_id, + index_7: self.is_flagged_spam, + index_9: self.time_since_post.as_ref(), + index_10: self.progress, + index_11: match self.mod_level { + None => None, + Some(ModLevel::Normal) => Some(1), + Some(ModLevel::Elder) => Some(2), + Some(ModLevel::Unknown(value)) => Some(value), + _ => unreachable!() + }, + index_12: self.special_color.map(|color| { + match color { + Color::Known(r, g, b) => RGBColor(r, g, b), + _ => panic!("Color::Unknown passed as color of level comment"), + } + }) + , + }; + internal.serialize(&mut IndexedSerializer::new("~", writer, true)) + } + } + }; + +// include!(concat!(env!("OUT_DIR"), "/level_comment.boilerplate")); include!(concat!(env!("OUT_DIR"), "/comment_user.boilerplate")); } diff --git a/src/model/mod.rs b/src/model/mod.rs index 2754a5d..5e59e88 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -67,4 +67,6 @@ impl Display for GameVersion { GameVersion::Version { minor, major } => write!(f, "{}.{}", major, minor), } } -} \ No newline at end of file +} + +pub trait RobtopForm {} \ No newline at end of file diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index e7c8843..24bd9ad 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -38,12 +38,12 @@ impl From for ModLevel { 0 => ModLevel::None, 1 => ModLevel::Normal, 2 => ModLevel::Elder, - i => ModLevel::Unknown(i), + _ => ModLevel::Unknown(i), } } } -/// The type of icon displayed next a user's comment of next to their search result +/// The type of icon displayed next a user's comment or next to their search result #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum IconType { Cube, @@ -86,6 +86,172 @@ impl From for u8 { } } +/// The user's in game message privacy +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum MessageState { + + /// Messages are open to all users + /// This is provided by value 0 by boomlings response for messageState + Anyone, + + /// Messages are only open to friends + /// This is provided by value 1 by boomlings response for messageState + FriendsOnly, + + /// Messages cannot be sent to this user + /// This is provided by value 2 by boomlings response for messageState + NoOne, + + /// Unknown or invalid value + Unknown(u8) +} + +impl From for u8 { + fn from(state: MessageState) -> u8 { + match state { + MessageState::Anyone => 0, + MessageState::FriendsOnly => 1, + MessageState::NoOne => 2, + MessageState::Unknown(inner) => inner, + } + } +} + +impl From for MessageState { + fn from(i: u8) -> Self { + match i { + 0 => MessageState::Anyone, + 1 => MessageState::FriendsOnly, + 2 => MessageState::NoOne, + _ => MessageState::Unknown(i) + } + } +} + +/// The user's in game message privacy +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum FriendsState { + + /// Friend requests are open to all users, this is provided by value 0 by boomlings response for friendsState + Anyone, + + /// Friend requests cannot be sent to this user, this is provided by value 1 by boomlings response for friendsState + NoOne, + + /// Unknown or invalid value + Unknown(u8) +} + +impl From for u8 { + fn from(state: FriendsState) -> u8 { + match state { + FriendsState::Anyone => 0, + FriendsState::NoOne => 1, + FriendsState::Unknown(inner) => inner, + } + } +} + +impl From for FriendsState { + fn from(i: u8) -> Self { + match i { + 0 => FriendsState::Anyone, + 1 => FriendsState::NoOne, + _ => FriendsState::Unknown(i) + } + } +} + +/// The user's status of a friend request for a given [`AuthenticatedUser`] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum FriendState { + + /// The [`AuthenticatedUser`] has not sent a friend request to this profile + /// This is provided by value 0 by boomlings response for friendsState + NoRequest, + + /// The [`AuthenticatedUser`] is already friends with this profile + /// This is provided by value 1 by boomlings response for friendsState + AlreadyFriends, + + /// The [`AuthenticatedUser`] has sent a request to this profile but the profile has not responded + /// This is provided by value 3 by boomlings response for friendsState + RequestPendingByProfile, + + /// The profile has sent a request to the [`AuthenticatedUser`] but the [`AuthenticatedUser`] has not responded + /// This is provided by value 4 by boomlings response for friendsState + RequestPendingByUser, + + /// Unknown or invalid value + Unknown(u8) +} + +impl From for u8 { + fn from(state: FriendState) -> u8 { + match state { + FriendState::NoRequest => 0, + FriendState::AlreadyFriends => 1, + FriendState::RequestPendingByProfile => 3, + FriendState::RequestPendingByUser => 4, + FriendState::Unknown(inner) => inner, + } + } +} + +impl From for FriendState { + fn from(i: u8) -> Self { + match i { + 0 => FriendState::NoRequest, + 1 => FriendState::AlreadyFriends, + 3 => FriendState::RequestPendingByProfile, + 4 => FriendState::RequestPendingByUser, + _ => FriendState::Unknown(i), + } + } +} + +/// The user's comment history privacy +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CommentHistoryState { + + /// Messages are open to all users + /// This is provided by value 0 by boomlings response for messageState + Anyone, + + /// Messages are only open to friends + /// This is provided by value 1 by boomlings response for messageState + FriendsOnly, + + /// Messages cannot be sent to this user + /// This is provided by value 2 by boomlings response for messageState + NoOne, + + /// Unknown or invalid value + Unknown(u8) +} + +impl From for u8 { + fn from(state: CommentHistoryState) -> u8 { + match state { + CommentHistoryState::Anyone => 0, + CommentHistoryState::FriendsOnly => 1, + CommentHistoryState::NoOne => 2, + CommentHistoryState::Unknown(inner) => inner, + } + } +} + +impl From for CommentHistoryState { + fn from(i: u8) -> Self { + match i { + 0 => CommentHistoryState::Anyone, + 1 => CommentHistoryState::FriendsOnly, + 2 => CommentHistoryState::NoOne, + _ => CommentHistoryState::Unknown(i), + } + } +} + // Enum representing an in-game icon color #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] pub enum Color { diff --git a/src/model/user/profile.rs b/src/model/user/profile.rs index dfd0954..3edbeec 100644 --- a/src/model/user/profile.rs +++ b/src/model/user/profile.rs @@ -1,4 +1,4 @@ -use crate::model::user::{Color, ModLevel}; +use crate::model::user::{Color, CommentHistoryState, FriendsState, FriendState, IconType, MessageState, ModLevel}; use serde::{__private::Formatter, Deserialize, Serialize}; use std::{borrow::Cow, fmt::Display}; @@ -67,12 +67,34 @@ pub struct Profile<'a> { /// This value is provided at index `4` pub demons: u16, + /// The global leaderboard position for this [`Profile`]. + /// This data is returned when requesting leaderboard data, otherwise `None`. + /// + /// ## GD Internals: + /// This value is provided at index `6` + pub leaderboard_position: Option, + + /// The account ID for this [`Profile`], just used to highlight leaderboard ranking. + /// This data is returned when requesting leaderboard data, otherwise `None`. + /// + /// + /// ## GD Internals: + /// This value is provided at index `7` + pub account_highlight: Option, + /// The amount of creator points this [`Profile`] was awarded. /// /// ## GD Internals: /// This value is provided at index `8` pub creator_points: u16, + /// The icon this [`Profile`] has displayed for leaderboards and comments. + /// This data is returned when requesting leaderboard or comment data, otherwise `None`. + /// + /// ## GD Internals: + /// This value is provided at index `9` + pub icon_id: Option, + /// This [`Profile`]'s primary color /// /// ## GD Internals: @@ -94,6 +116,13 @@ pub struct Profile<'a> { /// This value is provided at index `13` pub secret_coins: u8, + /// The icon type this [`Profile`] has displayed for leaderboards and comments. + /// This data is returned when requesting leaderboard or comment data, otherwise `None`. + /// + /// ## GD Internals: + /// This value is provided at index `14` + pub icon_type: Option, + /// The [`Profile`]'s unique account ID /// /// ## GD Internals: @@ -106,15 +135,15 @@ pub struct Profile<'a> { /// This value is provided at index `17` pub user_coins: u16, - // TODO: figure this value out + /// The privacy option for messages that this ['Profile'] has set /// ## GD Internals: /// This value is provided at index `18` - pub index_18: Cow<'a, str>, + pub message_state: MessageState, - // TODO: figure this value out + /// The privacy option for friends that this ['Profile'] has set /// ## GD Internals: /// This value is provided at index `19` - pub index_19: Cow<'a, str>, + pub friends_state: FriendsState, /// The link to the [`Profile`]'s [YouTube](https://youtube.com) channel, if provided /// @@ -170,10 +199,11 @@ pub struct Profile<'a> { /// This value is provided at index `28`, as an integer pub has_glow: bool, - // TODO: figure this value out + /// Flag for if this [`Profile`] is registered or not. + /// /// ## GD Internals: - /// This value is provided at index `29` - pub index_29: Cow<'a, str>, + /// This value is provided at index `29`, 0 if not registered, 1 if registed + pub is_registered: bool, /// This [`Profile`]'s global rank. [`None`] if he is banned or not ranked. /// @@ -181,25 +211,46 @@ pub struct Profile<'a> { /// This value is provided at index `30`. For unranked/banned users it's `0` pub global_rank: Option, - // TODO: figure this value out + /// Holds the status of the friend request sent to this [`Profile`] by the [`AuthenticatedUser`]. + /// /// ## GD Internals: /// This value is provided at index `31` - pub index_31: Cow<'a, str>, + pub friend_state: FriendState, - // TODO: figure this value out + /// Number of unread messages this [`Profile`] has. + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. + /// /// ## GD Internals: /// This value is provided at index `38` - pub index_38: Cow<'a, str>, + pub unread_messages_count: Option, - // TODO: figure this value out + /// Number of unread friend requests this [`Profile`] has. + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. + /// /// ## GD Internals: /// This value is provided at index `39` - pub index_39: Cow<'a, str>, + pub unread_friend_request_count: Option, - // TODO: figure this value out + /// Number of new friend this [`Profile`] has. + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. + /// /// ## GD Internals: /// This value is provided at index `40` - pub index_40: Cow<'a, str>, + pub new_friends_count: Option, + + /// Flag for if a friend request is new. + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. + /// + /// ## GD Internals: + /// This value is provided at index `41` + pub new_friend_request: Option, + + /// The time since this [`Profile`] has submitted a new level score. + /// This data is only returned when requesting leaderboard data + /// + /// ## GD Internals: + /// This value is provided at index `42` + pub time_since_score_update: Option>, /// The 1-based index of the spider this [`Profile`] currently uses. Indexing of icons starts at /// the top left corner and then goes left-to-right and top-to-bottom @@ -239,14 +290,211 @@ pub struct Profile<'a> { /// This value is provided at index `49` pub mod_level: ModLevel, - // TODO: figure this value out + /// The privacy option this for viewing comment history [`Profile`] has set. /// ## GD Internals: /// This value is provided at index `50` - pub index_50: Cow<'a, str>, + pub comment_history_state: CommentHistoryState, } mod internal { use crate::model::user::profile::{Profile, Twitch, Twitter, Youtube}; - include!(concat!(env!("OUT_DIR"), "/profile.boilerplate")); + #[allow(non_upper_case_globals, unused_imports)] + const _profile: () = { + use crate::{ + serde::{DeError, HasRobtopFormat, IndexedDeserializer, IndexedSerializer, PercentDecoded, SerError, Thunk, RefThunk, Base64Decoded}, + }; + use serde::{Deserialize, Serialize}; + use std::{borrow::{Cow, Borrow}, io::Write}; + #[derive(Serialize, Deserialize)] + struct InternalProfile<'src> { + #[serde(rename = "1")] + index_1: &'src str, + #[serde(rename = "2")] + index_2: u64, + #[serde(rename = "3")] + index_3: u32, + #[serde(rename = "4")] + index_4: u16, + #[serde(rename = "6")] + index_6: Option, + #[serde(rename = "7")] + index_7: Option, + #[serde(rename = "8")] + index_8: u16, + #[serde(rename = "9")] + index_9: Option, + #[serde(rename = "10")] + index_10: u8, + #[serde(rename = "11")] + index_11: u8, + #[serde(rename = "13")] + index_13: u8, + #[serde(rename = "14")] + index_14: Option, + #[serde(rename = "16")] + index_16: u64, + #[serde(rename = "17")] + index_17: u16, + #[serde(rename = "18")] + index_18: u8, + #[serde(rename = "19")] + index_19: u8, + #[serde(rename = "20")] + index_20: Option<&'src str>, + #[serde(rename = "21")] + index_21: u16, + #[serde(rename = "22")] + index_22: u8, + #[serde(rename = "23")] + index_23: u8, + #[serde(rename = "24")] + index_24: u8, + #[serde(rename = "25")] + index_25: u8, + #[serde(rename = "26")] + index_26: u8, + #[serde(rename = "28")] + index_28: bool, + #[serde(rename = "29")] + index_29: bool, + #[serde(rename = "30")] + index_30: Option, + #[serde(rename = "31")] + index_31: u8, + #[serde(rename = "38")] + index_38: Option, + #[serde(rename = "39")] + index_39: Option, + #[serde(rename = "40")] + index_40: Option, + #[serde(rename = "41")] + index_41: Option, + #[serde(rename = "42")] + index_42: Option<&'src str>, + #[serde(rename = "43")] + index_43: u8, + #[serde(rename = "44")] + index_44: Option<&'src str>, + #[serde(rename = "45")] + index_45: Option<&'src str>, + #[serde(rename = "46")] + index_46: u16, + #[serde(rename = "48")] + index_48: u8, + #[serde(rename = "49")] + index_49: u8, + #[serde(rename = "50")] + index_50: u8, + } + impl<'src> HasRobtopFormat<'src> for Profile<'src> { + fn from_robtop_str(input: &'src str) -> Result { + let internal = InternalProfile::deserialize(&mut IndexedDeserializer::new(input, ":", true))?; + Ok(Self { + name: Cow::Borrowed(internal.index_1), + user_id: internal.index_2, + stars: internal.index_3, + demons: internal.index_4, + leaderboard_position: internal.index_6, + account_highlight: internal.index_7, + creator_points: internal.index_8, + icon_id: internal.index_9, + primary_color: internal.index_10.into(), + secondary_color: internal.index_11.into(), + secret_coins: internal.index_13, + icon_type: match internal.index_14 { + None => None, + Some(icon_type_value) => Some(icon_type_value.into()) + }, + account_id: internal.index_16, + user_coins: internal.index_17, + message_state: internal.index_18.into(), + friends_state: internal.index_19.into(), + cube_index: internal.index_21, + ship_index: internal.index_22, + ball_index: internal.index_23, + ufo_index: internal.index_24, + wave_index: internal.index_25, + robot_index: internal.index_26, + has_glow: internal.index_28, + is_registered: internal.index_29, + global_rank: internal.index_30, + friend_state: internal.index_31.into(), + unread_messages_count: internal.index_38.into(), + unread_friend_request_count: internal.index_39, + new_friends_count: internal.index_40, + new_friend_request: internal.index_41, + time_since_score_update: match internal.index_42 { + None => None, + Some(age_value) => Some(Cow::Borrowed(age_value)) + }, + spider_index: internal.index_43, + diamonds: internal.index_46, + death_effect_index: internal.index_48, + mod_level: internal.index_49.into(), + comment_history_state: internal.index_50.into(), + youtube_url: internal.index_20.map(Cow::Borrowed).map(Youtube) + , + twitch_url: internal.index_45.map(Cow::Borrowed).map(Twitch), + twitter_url: internal.index_44.map(Cow::Borrowed).map(Twitter) + , + }) + } + fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { + let internal = InternalProfile { + index_1: self.name.as_ref(), + index_2: self.user_id, + index_3: self.stars, + index_4: self.demons, + index_6: self.leaderboard_position, + index_7: self.account_highlight, + index_8: self.creator_points, + index_9: self.icon_id, + index_10: self.primary_color.into(), + index_11: self.secondary_color.into(), + index_13: self.secret_coins, + index_14: match self.icon_type { + None => None, + Some(icon_type_value) => Some(icon_type_value.into()) + }, + index_16: self.account_id, + index_17: self.user_coins, + index_18: self.message_state.into(), + index_19: self.friends_state.into(), + index_20: self.youtube_url.as_ref().map(|y| y.0.borrow()), + index_21: self.cube_index, + index_22: self.ship_index, + index_23: self.ball_index, + index_24: self.ufo_index, + index_25: self.wave_index, + index_26: self.robot_index, + index_28: self.has_glow, + index_29: self.is_registered.into(), + index_30: self.global_rank, + index_31: self.friend_state.into(), + index_38: self.unread_messages_count.into(), + index_39: self.unread_friend_request_count, + index_40: self.new_friends_count, + index_41: self.new_friend_request, + index_42: match &self.time_since_score_update { + None => None, + Some(age_value) => Some(age_value.as_ref()) + }, + index_43: self.spider_index, + index_44: self.twitter_url.as_ref().map(|y| y.0.borrow()) + , + index_45: self.twitch_url.as_ref().map(|y| y.0.borrow()) + , + index_46: self.diamonds, + index_48: self.death_effect_index, + index_49: self.mod_level.into(), + index_50: self.comment_history_state.into(), + }; + internal.serialize(&mut IndexedSerializer::new(":", writer, true)) + } + } + }; + + + // include!(concat!(env!("OUT_DIR"), "/profile.boilerplate")); } diff --git a/src/request/account.rs b/src/request/account.rs index b24ea3d..f4c24e6 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -1,13 +1,12 @@ use std::fmt; use std::fmt::Formatter; -use reqwest::{Error, Response}; use serde::{Serialize}; use crate::{ util, request::{REQUEST_BASE_URL} }; -use crate::request::AuthenticatedUser; +use crate::request::{AuthenticatedUser, Executable}; pub const ACCOUNT_LOGIN_ENPOINT: &str = "accounts/loginGJAccount.php"; pub const XOR_KEY: &str = "37526"; @@ -51,28 +50,13 @@ impl<'a> LoginRequest<'a> { pub fn default() -> Self { LoginRequest{ - udid: "199095", + udid: "100000", user_name: "", password: "", secret: super::ACCOUNT_SECRET } } - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, ACCOUNT_LOGIN_ENPOINT) - } - - async fn execute(&self) -> Result { - let reqwest_client = reqwest::Client::new(); - println!("{}?{}", self.to_url(), self.to_string()); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) - .send() - .await - } - pub async fn to_authenticated_user(&self) -> Result { match self.execute().await { Ok(login_result) => { @@ -94,6 +78,12 @@ impl<'a> LoginRequest<'a> { } } +impl<'a> Executable for LoginRequest<'a> { + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, ACCOUNT_LOGIN_ENPOINT) + } +} + #[derive(Debug, Clone)] pub struct AuthenticationError(String); @@ -118,9 +108,12 @@ mod tests { #[tokio::test] async fn serialize_login_request() { let request = LoginRequest::default() - .user_name("Ryder") - .password("PASSHERE"); + .user_name("TestUser") + .password("PLAIN_TEXT_PASS_HERE"); - println!("{:?}", request.to_authenticated_user().await.unwrap()); + assert_eq!( + request.to_string(), + "udid=100000&userName=TestUser&password=PLAIN_TEXT_PASS_HERE&secret=Wmfv3899gc9" + ); } } diff --git a/src/request/comment.rs b/src/request/comment.rs index 4ceaae2..f1db5de 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -1,11 +1,17 @@ //! Module containing request structs for retrieving profile/level comments use std::borrow::Cow; -use reqwest::{Error, Response}; -use crate::{model::level::Level, request::{BaseRequest, GD_21, REQUEST_BASE_URL, AuthenticatedUser}, util}; +use crate::{ + model::comment::{ + profile::ProfileComment, + level::LevelComment + }, + request::{BaseRequest, GD_21, REQUEST_BASE_URL, AuthenticatedUser}, + response::{ResponseError, parse_get_gj_comments_response, parse_get_gj_acccount_comments_response}, + util +}; use serde::Serialize; -use crate::request::Executable; -use async_trait::async_trait; +use reqwest::Error; pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php"; pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php"; @@ -74,8 +80,7 @@ pub struct LevelCommentsRequest<'a> { pub level_id: u64, /// The amount of comments to retrieve. Note that while in-game this can only be set to 20 or 40 - /// (via the "load more comments option), the API accepts any value. So you can set it to - /// something ridiculously high (like u32::MAX_VALUE) and retrieve all comments at once. + /// (via the "load more comments option), the API accepts any value. The max value for this is 100 /// /// ## GD Internals: /// This field is called `count` in the boomlings API @@ -90,7 +95,7 @@ impl<'a> LevelCommentsRequest<'a> { const_setter!(page: u32); - pub fn to_url(&self) -> String { + fn to_url(&self) -> String { format!("{}{}", REQUEST_BASE_URL, LEVEL_COMMENTS_ENDPOINT) } @@ -98,7 +103,7 @@ impl<'a> LevelCommentsRequest<'a> { Self::with_base(GD_21, level) } - pub const fn with_base(base: BaseRequest<'a>, level: u64) -> Self { + const fn with_base(base: BaseRequest<'a>, level: u64) -> Self { LevelCommentsRequest { level_id: level, base, @@ -118,23 +123,13 @@ impl<'a> LevelCommentsRequest<'a> { self.sort_mode = SortMode::Recent; self } -} - -impl ToString for LevelCommentsRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) - } -} -impl From for LevelCommentsRequest<'_> { - fn from(level_id: u64) -> Self { - LevelCommentsRequest::new(level_id) + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await } -} -impl From> for LevelCommentsRequest<'_> { - fn from(level: Level<'_, S, U>) -> Self { - LevelCommentsRequest::from(level.level_id) + pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { + parse_get_gj_comments_response(response_body) } } @@ -186,12 +181,15 @@ impl<'a> ProfileCommentsRequest<'a> { total: 0, } } -} -impl ToString for ProfileCommentsRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await } + + pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { + parse_get_gj_acccount_comments_response(response_body) + } + } #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)] @@ -236,10 +234,6 @@ impl<'a> CommentHistoryRequest<'a> { const_setter!(limit: u32); const_setter!(page: u32); - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, COMMENT_HISTORY_ENDPOINT) - } - pub const fn with_base(base: BaseRequest<'a>, player: u64) -> Self { CommentHistoryRequest { player_id: player, @@ -251,6 +245,10 @@ impl<'a> CommentHistoryRequest<'a> { } } + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, COMMENT_HISTORY_ENDPOINT) + } + pub const fn new(player: u64) -> Self { Self::with_base(GD_21, player) } @@ -259,26 +257,13 @@ impl<'a> CommentHistoryRequest<'a> { self.sort_mode = sort_mode; self } -} -impl<'a> ToString for CommentHistoryRequest<'a> { - fn to_string(&self) -> String { - super::to_string(self) + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await } -} -#[async_trait] -impl<'a> Executable for CommentHistoryRequest<'a> { - async fn execute(&self) -> Result { - let reqwest_client = reqwest::Client::new(); - println!("{:?}", self.to_string()); - println!("{:?}", self.to_url()); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) - .send() - .await + pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { + parse_get_gj_comments_response(response_body) } } @@ -310,6 +295,10 @@ impl<'a> UploadCommentRequest<'a> { const_setter!(level_id: u64); const_setter!(percent: u8); + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, UPLOAD_COMMENT_ENDPOINT) + } + pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { Self::with_base(GD_21, authenticated_user, level_id) } @@ -325,44 +314,23 @@ impl<'a> UploadCommentRequest<'a> { } } - pub fn comment(mut self, comment_content: String) -> Self { + pub fn comment(mut self, comment_content: &str) -> Self { self.comment = base64::encode_config(comment_content.as_bytes(), base64::URL_SAFE).into(); self } - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, UPLOAD_COMMENT_ENDPOINT) - } - - fn generate_chk(mut self) -> Self { + fn generate_chk(&mut self) -> &mut Self { self.chk = format!("{}{}{}{}{}{}", self.authenticated_user.user_name, self.comment, self.level_id, self.percent, 0, COMMENT_CHK_SALT) .into(); - let xor_chk = util::xor(util::sha_encrypt(self.chk).as_bytes().to_vec(), COMMENT_XOR_CHK_KEY.as_bytes()); + let xor_chk = util::xor(util::sha_encrypt(&self.chk).as_bytes().to_vec(), COMMENT_XOR_CHK_KEY.as_bytes()); self.chk = base64::encode_config(xor_chk.as_slice(), base64::URL_SAFE).into(); self } -} -impl ToString for UploadCommentRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) - } -} - -#[async_trait] -impl<'a> Executable for UploadCommentRequest<'a> { - async fn execute(&self) -> Result { - - let reqwest_client = reqwest::Client::new(); - println!("{:?}", self.to_string()); - println!("{:?}", self.to_url()); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) - .send() - .await + pub async fn get_response_body(&mut self) -> Result { + self.generate_chk(); + super::execute(&self, &self.to_url()).await } } @@ -391,58 +359,42 @@ impl<'a> DeleteCommentRequest<'a> { const_setter!(comment_id: u64); const_setter!(level_id: u64); - pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { - Self::with_base(GD_21, authenticated_user, level_id) + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, DELETE_COMMENT_ENDPOINT) } - pub const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64, comment_id: u64) -> Self { + Self::with_base(GD_21, authenticated_user, level_id, comment_id) + } + + const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64, comment_id: u64) -> Self { DeleteCommentRequest { base, authenticated_user, - comment_id: 0, + comment_id, level_id } } - - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, DELETE_COMMENT_ENDPOINT) - } -} - -#[async_trait] -impl<'a> Executable for DeleteCommentRequest<'a> { - async fn execute(&self) -> Result { - let reqwest_client = reqwest::Client::new(); - println!("{:?}", self.to_string()); - println!("{:?}", self.to_url()); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header(super::CONTENT_TYPE, super::URL_FORM_ENCODED) - .send() - .await - } -} -impl ToString for DeleteCommentRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await } } #[cfg(test)] mod tests { - use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest, SortMode}; - use crate::request::Executable; - use crate::response::parse_get_gj_comments_response; + use std::borrow::Cow; + use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest, UploadCommentRequest, DeleteCommentRequest, SortMode}; + use crate::request::{AuthenticatedUser}; + + const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser { + user_name: "TestUser", + account_id: 472634, + password_hash: Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") + }; #[test] fn serialize_level_comments() { - if let Err(err) = env_logger::builder().is_test(true).try_init() { - // nothing to make the tests fail over - eprintln!("Error setting up env_logger: {:?}", err) - } - let request = LevelCommentsRequest::new(1234).most_liked().page(2).limit(15); assert_eq!( @@ -453,11 +405,6 @@ mod tests { #[test] fn serialize_profile_comments() { - if let Err(err) = env_logger::builder().is_test(true).try_init() { - // nothing to make the tests fail over - eprintln!("Error setting up env_logger: {:?}", err) - } - let request = ProfileCommentsRequest::new(1710032).page(2); assert_eq!( @@ -468,79 +415,37 @@ mod tests { #[test] fn serialize_comment_history() { - if let Err(err) = env_logger::builder().is_test(true).try_init() { - // nothing to make the tests fail over - eprintln!("Error setting up env_logger: {:?}", err) - } - let request = CommentHistoryRequest::new(159782398) + .sort_mode(SortMode::Recent) + .page(0) .limit(2); - assert_eq!(super::super::to_string(request), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2") + assert_eq!( + super::super::to_string(request), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2" + ); } - #[tokio::test] - async fn upload_comment() { - let request = crate::request::account::LoginRequest::default() - .user_name("Ryder") - .password("PASSHERE"); - - let login_response = request.to_authenticated_user() - .await - .unwrap(); - - let comment_upload_request = crate::request::comment::UploadCommentRequest::new(login_response, 85179632) - .comment(String::from("More tests still ignore me")) - .percent(69) - .generate_chk() - .execute() - .await - .unwrap() - .text().await.unwrap(); + #[test] + fn serialize_upload_comment() { + let request = UploadCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) + .comment("This is a test comment") + .percent(56) + .generate_chk(); - println!("{:?}", comment_upload_request) + assert_eq!( + super::super::to_string(request), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&comment=VGhpcyBpcyBhIHRlc3QgY29tbWVudA==&levelID=85179632&percent=56&chk=UQsGAAEACgQBVQBaAwoGVwtSDQIEWAYOUFEAVQoIBVtWDwEHDQEJVA==" + ); } - #[tokio::test] - async fn delete_comment() { - let request = crate::request::account::LoginRequest::default() - .user_name("Ryder") - .password("PASSHERE"); - - let login_response = request.to_authenticated_user() - .await - .unwrap(); - - - let comment_history_request = CommentHistoryRequest::new(3713125) - .sort_mode(SortMode::Recent) - .limit(1) - .page(0); - - let comment_history_response = comment_history_request.execute() - .await - .unwrap() - .text() - .await - .unwrap(); - - let comment = parse_get_gj_comments_response(comment_history_response.as_str()) - .unwrap(); - - let comment_id = comment.get(0).unwrap().comment_id; - - println!("{}", &comment_id); - - let comment_delete_request = crate::request::comment::DeleteCommentRequest::new(login_response, 85179632) - .comment_id(comment_id); - - let comment_delete_response = comment_delete_request.execute() - .await - .unwrap() - .text() - .await - .unwrap(); + #[test] + fn serialize_delete_comment() { + let request = DeleteCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632, 7000000); - println!("{}", comment_delete_response) + assert_eq!( + super::super::to_string(request), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&commentID=7000000&levelID=85179632" + ); } } diff --git a/src/request/level.rs b/src/request/level.rs index 13da37e..59c4503 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -1,6 +1,3 @@ -use reqwest::{ - Response, Error -}; use crate::{ model::{ level::{DemonRating, LevelLength, LevelRating}, @@ -84,7 +81,7 @@ impl<'a> LevelRequest<'a> { /// Uses a default [`BaseRequest`], and sets the /// `inc` field to `true` and `extra` to `false`, as are the default /// values set the by the Geometry Dash Client - pub const fn new(level_id: u64) -> LevelRequest<'static> { + pub const fn new(level_id: u64) -> LevelRequest<'a> { LevelRequest { base: GD_21, level_id, @@ -92,29 +89,11 @@ impl<'a> LevelRequest<'a> { extra: false, } } - - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, DOWNLOAD_LEVEL_ENDPOINT) - } } -#[async_trait] impl Executable for LevelRequest<'_> { - async fn execute(&self) -> Result { - let reqwest_client = reqwest::Client::new(); - println!("{}?{}", self.to_url(), self.to_string()); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header("Content-Type", "application/x-www-form-urlencoded") - .send() - .await - } -} - -impl ToString for LevelRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, DOWNLOAD_LEVEL_ENDPOINT) } } @@ -563,10 +542,6 @@ impl<'a> LevelsRequest<'a> { const_setter!(request_type: LevelRequestType); - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, SEARCH_LEVEL_ENDPOINT) - } - pub fn with_base(base: BaseRequest<'a>) -> Self { LevelsRequest { base, @@ -618,21 +593,8 @@ impl<'a> LevelsRequest<'a> { #[async_trait] impl Executable for LevelsRequest<'_>{ - async fn execute(&self) -> Result { - let reqwest_client = reqwest::Client::new(); - println!("{}?{}", self.to_url(), self.to_string()); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header("Content-Type", "application/x-www-form-urlencoded") - .send() - .await - } -} - -impl ToString for LevelsRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, SEARCH_LEVEL_ENDPOINT) } } @@ -730,7 +692,7 @@ mod tests { println!("{:?}", request.execute().await.unwrap().text().await.unwrap()); assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&type=2&str=&len=2,3&diff=-&page=0&total=0&featured=1&original=0&\ twoPlayer=1&coins=0&epic=1&star=1&completedLevels=(18018958,21373201,22057275,22488444,22008823,23144971,17382902,87600,\ 22031889,22390740,22243264,21923305)&onlyCompleted=0&uncompleted=1" @@ -742,10 +704,8 @@ mod tests { let request = LevelRequest::default() .level_id(17448979); - println!("{:?}", request.execute().await.unwrap().text().await.unwrap()); - assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&levelID=17448979&inc=0&extra=0" ); } diff --git a/src/request/mod.rs b/src/request/mod.rs index e5e9875..d28a818 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -6,10 +6,16 @@ //! contain a lot of boomlings-specific fields. use std::borrow::Cow; -use crate::{model::GameVersion, serde::RequestSerializer}; +use crate::{ + model::{ + GameVersion + }, + serde::RequestSerializer, +}; use serde::{Deserialize, Serialize}; use async_trait::async_trait; use reqwest::{Error, Response}; +use crate::response::ResponseError; macro_rules! const_setter { ($name: ident, $field: ident, $t: ty) => { @@ -147,7 +153,54 @@ pub(crate) fn to_string(request: S) -> String { String::from_utf8(output).unwrap() } +async fn execute(request: S, url: &str) -> Result { + let reqwest_client = reqwest::Client::new(); + println!("{:?}", to_string(&request)); + println!("{:?}", url); + reqwest_client + .post(url) + .body(to_string(request)) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await? + .text() + .await +} + #[async_trait] -pub trait Executable { - async fn execute(&self) -> Result; +trait Executable { + fn to_url(&self) -> String; + + fn to_string(&self) -> String + where + Self: Serialize + { + to_string(&self) + } + + async fn execute(&self) -> Result + where + Self: Serialize + { + let reqwest_client = reqwest::Client::new(); + println!("{:?}", self.to_string()); + println!("{:?}", self.to_url()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + } + + async fn get_response_body(&self, response: Response) -> Result { + response.text().await + } +} + +#[async_trait] +pub trait ToRobtop { + type RobtopModel; + + async fn into_robtop(&self, response_body: &str) -> Result, ResponseError>; } \ No newline at end of file diff --git a/src/request/user.rs b/src/request/user.rs index 8a24547..45a4334 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -1,23 +1,30 @@ //! Module containing request definitions for retrieving users +use reqwest::Error; use crate::{ - model::creator::Creator, request::{BaseRequest, GD_21, REQUEST_BASE_URL}, }; use serde::Serialize; +use crate::model::user::profile::Profile; +use crate::model::user::searched::SearchedUser; +use crate::request::AuthenticatedUser; +use crate::response::{parse_get_gj_user_info_response, parse_get_gj_users_response, ResponseError}; -pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20"; -pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20"; +pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20.php"; +pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20.php"; /// Struct modelled after a request to `getGJUserInfo20.php`. /// /// In the geometry Dash API, this endpoint is used to download player profiles from the servers by /// their account IDs -#[derive(Debug, Default, Clone, Copy, Serialize, Hash)] +#[derive(Debug, Default, Clone, Serialize, Hash)] pub struct UserRequest<'a> { /// The base request data pub base: BaseRequest<'a>, + /// The authenticated user data + pub authenticated_user: Option>, + /// The **account ID** (_not_ user ID) of the users whose data to retrieve. /// /// ## GD Internals: @@ -26,34 +33,34 @@ pub struct UserRequest<'a> { pub user: u64, } -impl UserRequest<'_> { - pub const fn new(user_id: u64) -> UserRequest<'static> { +impl<'a> UserRequest<'a> { + + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, GET_USER_ENDPOINT) + } + + pub const fn new(user_id: u64) -> UserRequest<'a> { UserRequest { base: GD_21, + authenticated_user: None, user: user_id, } } - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, GET_USER_ENDPOINT) + pub const fn with_authenticated_user(authenticated_user: AuthenticatedUser<'a>, user_id: u64) -> UserRequest<'a> { + UserRequest { + base: GD_21, + authenticated_user: Some(authenticated_user), + user: user_id, + } } -} -impl From for UserRequest<'_> { - fn from(user_id: u64) -> Self { - UserRequest::new(user_id) + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await } -} -impl From> for UserRequest<'_> { - fn from(creator: Creator<'_>) -> Self { - UserRequest::from(creator.user_id) - } -} - -impl ToString for UserRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) + pub async fn into_robtop(self, response_body: &str) -> Result { + parse_get_gj_user_info_response(response_body) } } @@ -88,6 +95,11 @@ pub struct UserSearchRequest<'a> { } impl<'a> UserSearchRequest<'a> { + + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, SEARCH_USER_ENDPOINT) + } + pub const fn new(search_string: &'a str) -> Self { UserSearchRequest { base: GD_21, @@ -97,25 +109,36 @@ impl<'a> UserSearchRequest<'a> { } } - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, SEARCH_USER_ENDPOINT) + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await } -} -impl<'a> From<&'a str> for UserSearchRequest<'a> { - fn from(search_string: &'a str) -> Self { - UserSearchRequest::new(search_string) + pub async fn into_robtop(self, response_body: &str) -> Result { + parse_get_gj_users_response(response_body) } } -impl<'a: 'b, 'b> From<&'b Creator<'a>> for UserSearchRequest<'b> { - fn from(creator: &'b Creator<'a>) -> Self { - UserSearchRequest::from(&*creator.name) +#[cfg(test)] +mod tests { + use crate::request::user::{UserRequest, UserSearchRequest}; + + #[test] + fn serialize_user_request() { + let request = UserRequest::new(57903); + + assert_eq!( + super::super::to_string(request), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&targetAccountID=57903" + ); } -} -impl ToString for UserSearchRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) + #[test] + fn serialize_user_search_request() { + let request = UserSearchRequest::new("Ryder"); + + assert_eq!( + super::super::to_string(request), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&str=Ryder" + ); } } diff --git a/src/response.rs b/src/response.rs index 8813876..e3eb5ce 100644 --- a/src/response.rs +++ b/src/response.rs @@ -39,6 +39,9 @@ pub enum ResponseError<'a> { /// The response was not worked in the expected way (too few sections, etc.) UnexpectedFormat, + + /// There was an error making the request to http://www.boomlings.com + RequestError(reqwest::Error) } impl Display for ResponseError<'_> { @@ -47,6 +50,7 @@ impl Display for ResponseError<'_> { ResponseError::De(err) => err.fmt(f), ResponseError::NotFound => write!(f, "not found"), ResponseError::UnexpectedFormat => write!(f, "unexpected format"), + ResponseError::RequestError(err) => err.fmt(f) } } } @@ -151,7 +155,7 @@ pub fn parse_get_gj_users_response(response: &str) -> Result Result, ResponseError> { +pub fn parse_get_gj_comments_response(response: &str) -> Result, ResponseError> { if response == "-1" { return Err(ResponseError::NotFound) } diff --git a/src/util.rs b/src/util.rs index 025b44a..493af55 100644 --- a/src/util.rs +++ b/src/util.rs @@ -49,7 +49,7 @@ pub(crate) mod default_to_none { } } -pub(crate) fn sha_encrypt(content: Cow) -> String { +pub(crate) fn sha_encrypt(content: &Cow) -> String { let mut m = sha1_smol::Sha1::new(); m.update(content.as_bytes()); m.digest().to_string() diff --git a/tests/account.rs b/tests/account.rs index e57c232..c02a848 100644 --- a/tests/account.rs +++ b/tests/account.rs @@ -2,11 +2,15 @@ use dash_rs::request; #[tokio::test] async fn login_gj_account_test() { + dotenv::from_filename("test_env.env").expect("test_env.env file not found"); + + let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); + let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + let request = request::account::LoginRequest::default() - .user_name("Ryder") - .password("PASS_HERE"); + .user_name(&user_name) + .password(&password); let result = request.to_authenticated_user().await.unwrap(); - println!("{:?}", result); } \ No newline at end of file diff --git a/tests/comment.rs b/tests/comment.rs index 0e4fb69..629cc14 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -1,120 +1,107 @@ use dash_rs::{ - model::{ - comment::{ - level::{CommentUser, LevelComment}, - profile::ProfileComment, - }, - user::{Color, IconType, ModLevel}, + request::{ + account::LoginRequest, + comment::{UploadCommentRequest, DeleteCommentRequest, CommentHistoryRequest, LevelCommentsRequest, ProfileCommentsRequest, SortMode}, }, - Base64Decoded, Thunk, -}; -use std::borrow::Cow; - -#[macro_use] -mod helper; - -// A few of the most liked comments from "Ode to Time" by Pauze, 2020/08/15 -const LEVEL_COMMENT1_DATA :&str = "2~U3BlY2lhbCB0aGFua3MgdG8gSGFkbywgQ2luY2ksIFN5bmFjdGl2ZSwgQ29vbCwgUHJpc20sIFN1Yndvb2ZlciwgYW5kIEhhZG8gZm9yIHBsYXl0ZXN0aW5nLg==~3~7226087~4~104~7~0~10~0~9~5 days~6~258976~11~2~12~75,255,75"; -const LEVEL_COMMENT2_DATA: &str = "2~R3VydS4=~3~2723387~4~63~7~0~10~0~9~5 days~6~260007~11~2~12~75,255,75"; -const LEVEL_COMMENT3_DATA: &str = - "2~TGV0cyBtYWtlIGF1Z3VzdCAxMHRoIFBhdXplJ3MgaW50ZXJuYXRpb25hbCBkYXk=~3~7178197~4~58~7~0~10~0~9~5 days~6~259333~11~1~12~255,255,255"; - -const COMMENT_USER_DATA: &str = "1~Pauze~9~58~10~18~11~16~14~0~15~2~16~1705254"; - -const LEVEL_COMMENT1: LevelComment = LevelComment { - user: None, - content: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed( - "Special thanks to Hado, Cinci, Synactive, Cool, Prism, Subwoofer, and Hado for playtesting.", - )))), - user_id: 7226087, - likes: 104, - comment_id: 258976, - is_flagged_spam: Some(false), - time_since_post: Cow::Borrowed("5 days"), - progress: Some(0), - mod_level: ModLevel::Elder, - special_color: Some(Color::Known(75, 255, 75)), }; -const LEVEL_COMMENT2: LevelComment = LevelComment { - user: None, - content: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed("Guru.")))), - user_id: 2723387, - likes: 63, - comment_id: 260007, - is_flagged_spam: Some(false), - time_since_post: Cow::Borrowed("5 days"), - progress: Some(0), - mod_level: ModLevel::Elder, - special_color: Some(Color::Known(75, 255, 75)), -}; +#[tokio::test] +async fn get_level_comments() { + let request = LevelCommentsRequest::new(76298358) + .page(0) + .limit(5) + .most_recent(); -const LEVEL_COMMENT3: LevelComment = LevelComment { - user: None, - content: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed( - "Lets make august 10th Pauze's international day", - )))), - user_id: 7178197, - likes: 58, - comment_id: 259333, - is_flagged_spam: Some(false), - time_since_post: Cow::Borrowed("5 days"), - progress: Some(0), - mod_level: ModLevel::Normal, - special_color: Some(Color::Known(255, 255, 255)), -}; + let request_body = request.get_response_body() + .await + .unwrap(); -const COMMENT_USER: CommentUser = CommentUser { - name: Cow::Borrowed("Pauze"), - icon_index: 58, - primary_color: Color::Known(80, 80, 80), - secondary_color: Color::Known(0, 200, 255), - icon_type: IconType::Cube, - has_glow: true, - account_id: Some(1705254), -}; + let level_comments = request.into_robtop(&request_body) + .await + .unwrap(); -impl helper::ThunkProcessor for LevelComment<'_> { - fn process_all_thunks(&mut self) { - if let Some(ref mut cnt) = self.content { - assert!(cnt.process().is_ok()); - } - } + assert_eq!(level_comments.len(), 5); } -impl helper::ThunkProcessor for CommentUser<'_> { - fn process_all_thunks(&mut self) {} + +#[tokio::test] +async fn get_profile_comments() { + let request = ProfileCommentsRequest::new(57903) + .page(0); + + let response_body = request.get_response_body() + .await + .unwrap(); + + let profile_comments = request.into_robtop(&response_body) + .await + .unwrap(); + + assert_eq!(profile_comments.len(), 2); } -impl helper::ThunkProcessor for ProfileComment<'_> { - fn process_all_thunks(&mut self) { - if let Some(ref mut cnt) = self.content { - assert!(cnt.process().is_ok()) - } - } +#[tokio::test] +async fn upload_comment() { + dotenv::from_filename("test_env.env").expect("test_env.env file not found"); + + let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); + let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + + let request = LoginRequest::default() + .user_name(&user_name) + .password(&password); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + let mut comment_upload_request = UploadCommentRequest::new(login_response, 76298358) + .comment("More tests still ignore me") + .percent(69); + let comment_upload_response = comment_upload_request + .get_response_body() + .await + .unwrap(); + + assert!(!comment_upload_response.eq("-1")) } -load_save_roundtrip!(load_save_roundtrip1, LevelComment, LEVEL_COMMENT1_DATA, LEVEL_COMMENT1, "~", true); -load_save_roundtrip!(load_save_roundtrip2, LevelComment, LEVEL_COMMENT2_DATA, LEVEL_COMMENT2, "~", true); -load_save_roundtrip!(load_save_roundtrip3, LevelComment, LEVEL_COMMENT3_DATA, LEVEL_COMMENT3, "~", true); -load_save_roundtrip!(load_save_roundtrip_user, CommentUser, COMMENT_USER_DATA, COMMENT_USER, "~", true); - -save_load_roundtrip!(save_load_roundtrip1, LevelComment, LEVEL_COMMENT1); -save_load_roundtrip!(save_load_roundtrip2, LevelComment, LEVEL_COMMENT2); -save_load_roundtrip!(save_load_roundtrip3, LevelComment, LEVEL_COMMENT3); -save_load_roundtrip!(save_load_roundtrip_user, CommentUser, COMMENT_USER); - -const PROFILE_COMMENT_DATA: &str = - "2~QSB3aW5kb3cgdG8gdGhlIHBhc3QsIGEgZ2xpbXBzZSBvZiB0aGUgZnV0dXJlLCBBbiBPZGUgdG8gVGltZS4=~4~432~9~6 days~6~1922667"; - -const PROFILE_COMMENT: ProfileComment = ProfileComment { - content: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed( - "A window to the past, a glimpse of the future, An Ode to Time.", - )))), - likes: 432, - comment_id: 1922667, - time_since_post: Cow::Borrowed("6 days"), -}; +#[tokio::test] +async fn delete_comment() { + dotenv::from_filename("test_env.env").expect("test_env.env file not found"); + + let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); + let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + + let request = LoginRequest::default() + .user_name(&user_name) + .password(&password); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + + let comment_history_request = CommentHistoryRequest::new(3713125) + .sort_mode(SortMode::Recent) + .limit(1) + .page(0); + + let comment_history_response = comment_history_request.get_response_body() + .await + .unwrap(); + + let comment = comment_history_request.into_robtop(&comment_history_response) + .await + .unwrap(); + + let comment_id = comment.get(0).unwrap().comment_id; + + let comment_delete_request = DeleteCommentRequest::new(login_response, 76298358, comment_id); + + let comment_delete_response = comment_delete_request.get_response_body() + .await + .unwrap(); -load_save_roundtrip!(ProfileComment, PROFILE_COMMENT_DATA, PROFILE_COMMENT, "~", true); -save_load_roundtrip!(ProfileComment, ProfileComment, PROFILE_COMMENT); + assert!(!comment_delete_response.eq("-1")) +} \ No newline at end of file diff --git a/tests/profile.rs b/tests/profile.rs index 67bd0d6..00db2a9 100644 --- a/tests/profile.rs +++ b/tests/profile.rs @@ -1,55 +1,124 @@ -use dash_rs::model::user::{ - profile::{Profile, Twitter, Youtube}, - Color, ModLevel, -}; -use std::borrow::Cow; - -#[macro_use] -mod helper; - -const PROFILE_STARDUST1971_DATA: &str = "1:stardust1971:2:2073761:13:149:17:498:10:9:11:10:3:13723:46:2312:4:484:8:19:18:0:19:0:50:0:20:\ - stardust19710:21:95:22:48:23:33:24:18:25:11:26:10:28:1:43:2:48:13:30:0:16:8451:31:0:44:\ - stadust1971:45::49:0:38:0:39:579:40:0:29:1"; - -const PROFILE_STARDUST1971: Profile = Profile { - name: Cow::Borrowed("stardust1971"), - user_id: 2073761, - stars: 13723, - demons: 484, - creator_points: 19, - primary_color: Color::Known(255, 0, 0), - secondary_color: Color::Known(255, 125, 0), - secret_coins: 149, - account_id: 8451, - user_coins: 498, - index_18: Cow::Borrowed("0"), - index_19: Cow::Borrowed("0"), - youtube_url: Some(Youtube(Cow::Borrowed("stardust19710"))), - cube_index: 95, - ship_index: 48, - ball_index: 33, - ufo_index: 18, - wave_index: 11, - robot_index: 10, - has_glow: true, - index_29: Cow::Borrowed("1"), - global_rank: Some(0), - index_31: Cow::Borrowed("0"), - index_38: Cow::Borrowed("0"), - index_39: Cow::Borrowed("579"), - index_40: Cow::Borrowed("0"), - spider_index: 2, - twitter_url: Some(Twitter(Cow::Borrowed("stadust1971"))), - twitch_url: None, - diamonds: 2312, - death_effect_index: 13, - mod_level: ModLevel::None, - index_50: Cow::Borrowed("0"), -}; - -impl helper::ThunkProcessor for Profile<'_> { - fn process_all_thunks(&mut self) {} +// use dash_rs::model::user::{ +// profile::{Profile, Twitter, Youtube}, +// Color, ModLevel, +// }; +// use std::borrow::Cow; +// +// #[macro_use] +// mod helper; +// +// const PROFILE_STARDUST1971_DATA: &str = "1:stardust1971:2:2073761:13:149:17:498:10:9:11:10:3:13723:46:2312:4:484:8:19:18:0:19:0:50:0:20:\ +// stardust19710:21:95:22:48:23:33:24:18:25:11:26:10:28:1:43:2:48:13:30:0:16:8451:31:0:44:\ +// stadust1971:45::49:0:38:0:39:579:40:0:29:1"; +// +// const PROFILE_STARDUST1971: Profile = Profile { +// name: Cow::Borrowed("stardust1971"), +// user_id: 2073761, +// stars: 13723, +// demons: 484, +// creator_points: 19, +// primary_color: Color::Known(255, 0, 0), +// secondary_color: Color::Known(255, 125, 0), +// secret_coins: 149, +// account_id: 8451, +// user_coins: 498, +// index_18: Cow::Borrowed("0"), +// index_19: Cow::Borrowed("0"), +// youtube_url: Some(Youtube(Cow::Borrowed("stardust19710"))), +// cube_index: 95, +// ship_index: 48, +// ball_index: 33, +// ufo_index: 18, +// wave_index: 11, +// robot_index: 10, +// has_glow: true, +// index_29: Cow::Borrowed("1"), +// global_rank: Some(0), +// index_31: Cow::Borrowed("0"), +// index_38: Cow::Borrowed("0"), +// index_39: Cow::Borrowed("579"), +// index_40: Cow::Borrowed("0"), +// spider_index: 2, +// twitter_url: Some(Twitter(Cow::Borrowed("stadust1971"))), +// twitch_url: None, +// diamonds: 2312, +// death_effect_index: 13, +// mod_level: ModLevel::None, +// index_50: Cow::Borrowed("0"), +// }; +// +// impl helper::ThunkProcessor for Profile<'_> { +// fn process_all_thunks(&mut self) {} +// } +// +// save_load_roundtrip!(Profile, PROFILE_STARDUST1971); +// load_save_roundtrip!(Profile, PROFILE_STARDUST1971_DATA, PROFILE_STARDUST1971, ":", true); + +use dash_rs::model::user::ModLevel; +use dash_rs::request::account::LoginRequest; +use dash_rs::request::user::UserRequest; + +#[tokio::test] +async fn test_get_user_profile_with_auth() { + dotenv::from_filename("test_env.env").expect("test_env.env file not found"); + + let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); + let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + + let request = LoginRequest::default() + .user_name(&user_name) + .password(&password); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + let request = UserRequest::with_authenticated_user(login_response, 57903); + + let response_body = request.get_response_body() + .await + .unwrap(); + + println!("{}", response_body); + + let user = request.into_robtop(&response_body) + .await.unwrap(); + + println!("{:?}", user); + + assert_eq!( + user.name, + "Ryder" + ); + assert_eq!( + user.mod_level, + ModLevel::Elder + ); + assert!(!user.unread_friend_request_count.is_none()) } -save_load_roundtrip!(Profile, PROFILE_STARDUST1971); -load_save_roundtrip!(Profile, PROFILE_STARDUST1971_DATA, PROFILE_STARDUST1971, ":", true); +#[tokio::test] +async fn test_get_user_profile_without_auth() { + let request = UserRequest::new(57903); + + let response_body = request.get_response_body() + .await + .unwrap(); + + println!("{}", response_body); + + let user = request.into_robtop(&response_body) + .await.unwrap(); + + println!("{:?}", user); + + assert_eq!( + user.name, + "Ryder" + ); + assert_eq!( + user.mod_level, + ModLevel::Elder + ); + assert!(user.unread_friend_request_count.is_none()) +} \ No newline at end of file From 3c69f8a2258c10521f0307e4da74879f26b94410 Mon Sep 17 00:00:00 2001 From: Ryder <36455274+zRyder@users.noreply.github.com> Date: Sun, 18 Dec 2022 15:33:47 -0800 Subject: [PATCH 13/29] Level (#8) * Added more verbose unit test cases * Integration testing * level parsing changes * level parsing testing Co-authored-by: Ryder Co-authored-by: Ryder --- src/model/level/internal.rs | 327 +++++++++++++++----- src/model/level/local_level.rs | 368 ++++++++++++++++++++++ src/model/level/metadata.rs | 113 ------- src/model/level/mod.rs | 533 +++++++------------------------- src/model/level/online_level.rs | 415 +++++++++++++++++++++++++ src/model/song.rs | 61 +++- src/request/comment.rs | 7 +- src/request/level.rs | 33 +- src/response.rs | 24 +- tests/comment.rs | 1 - tests/level.rs | 90 ++++++ tests/level_meta.rs | 179 ----------- 12 files changed, 1333 insertions(+), 818 deletions(-) create mode 100644 src/model/level/local_level.rs delete mode 100644 src/model/level/metadata.rs create mode 100644 src/model/level/online_level.rs create mode 100644 tests/level.rs delete mode 100644 tests/level_meta.rs diff --git a/src/model/level/internal.rs b/src/model/level/internal.rs index 77d1cc2..58e0a18 100644 --- a/src/model/level/internal.rs +++ b/src/model/level/internal.rs @@ -1,7 +1,11 @@ #![allow(unused_imports)] use crate::{ model::{ - level::{DemonRating, Featured, Level, LevelData, LevelLength, LevelRating, Objects, Password}, + level::{ + DemonRating, Objects, Password, Featured, LevelLength, LevelRating, + local_level::LevelData, + online_level::Level, + }, song::MainSong, GameVersion, }, @@ -18,86 +22,247 @@ mod level_length { use crate::model::level::LevelLength; use serde::{de::Error, Deserialize, Deserializer, Serializer}; - pub fn serialize(to_serialize: &LevelLength, serializer: S) -> Result - where - S: Serializer, - { - match to_serialize { - LevelLength::Unknown(unknown) => serializer.serialize_i32(*unknown), - LevelLength::Tiny => serializer.serialize_str("0"), - LevelLength::Short => serializer.serialize_str("1"), - LevelLength::Medium => serializer.serialize_str("2"), - LevelLength::Long => serializer.serialize_str("3"), - LevelLength::ExtraLong => serializer.serialize_str("4"), - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - Ok(match <&str>::deserialize(deserializer)? { - "0" => LevelLength::Tiny, - "1" => LevelLength::Short, - "2" => LevelLength::Medium, - "3" => LevelLength::Long, - "4" => LevelLength::ExtraLong, - int => LevelLength::Unknown(int.parse().map_err(D::Error::custom)?), - }) - } -} - -impl LevelRating { - fn from_response_value(value: i32) -> LevelRating { - match value { - 0 => LevelRating::NotAvailable, - 10 => LevelRating::Easy, - 20 => LevelRating::Normal, - 30 => LevelRating::Hard, - 40 => LevelRating::Harder, - 50 => LevelRating::Insane, - _ => LevelRating::Unknown(value), - } - } - - fn into_response_value(self) -> i32 { - match self { - LevelRating::Unknown(value) => value, - LevelRating::NotAvailable => 0, - LevelRating::Easy => 10, - LevelRating::Normal => 20, - LevelRating::Hard => 30, - LevelRating::Harder => 40, - LevelRating::Insane => 50, - LevelRating::Demon(demon_rating) => demon_rating.into_response_value(), - _ => panic!("got {:?}, please handle before calling this function", self), - } - } + // pub fn serialize(to_serialize: &LevelLength, serializer: S) -> Result + // where + // S: Serializer, + // { + // match to_serialize { + // LevelLength::Unknown(unknown) => serializer.serialize_i32(*unknown), + // LevelLength::Tiny => serializer.serialize_str("0"), + // LevelLength::Short => serializer.serialize_str("1"), + // LevelLength::Medium => serializer.serialize_str("2"), + // LevelLength::Long => serializer.serialize_str("3"), + // LevelLength::ExtraLong => serializer.serialize_str("4"), + // } + // } + // + // pub fn deserialize<'de, D>(deserializer: D) -> Result + // where + // D: Deserializer<'de>, + // { + // Ok(match <&str>::deserialize(deserializer)? { + // "0" => LevelLength::Tiny, + // "1" => LevelLength::Short, + // "2" => LevelLength::Medium, + // "3" => LevelLength::Long, + // "4" => LevelLength::ExtraLong, + // int => LevelLength::Unknown(int.parse().map_err(D::Error::custom)?), + // }) + // } } -impl DemonRating { - fn from_response_value(value: i32) -> DemonRating { - match value { - 10 => DemonRating::Easy, - 20 => DemonRating::Medium, - 30 => DemonRating::Hard, - 40 => DemonRating::Insane, - 50 => DemonRating::Extreme, - _ => DemonRating::Unknown(value), - } - } - - fn into_response_value(self) -> i32 { - match self { - DemonRating::Unknown(value) => value, - DemonRating::Easy => 10, - DemonRating::Medium => 20, - DemonRating::Hard => 30, - DemonRating::Insane => 40, - DemonRating::Extreme => 50, - } - } -} +// impl LevelRating { +// fn from_response_value(value: i8) -> LevelRating { +// match value { +// 0 => LevelRating::NotAvailable, +// 10 => LevelRating::Easy, +// 20 => LevelRating::Normal, +// 30 => LevelRating::Hard, +// 40 => LevelRating::Harder, +// 50 => LevelRating::Insane, +// _ => LevelRating::Unknown(value), +// } +// } +// +// fn into_response_value(self) -> i8 { +// match self { +// LevelRating::Unknown(value) => value, +// LevelRating::NotAvailable => 0, +// LevelRating::Easy => 10, +// LevelRating::Normal => 20, +// LevelRating::Hard => 30, +// LevelRating::Harder => 40, +// LevelRating::Insane => 50, +// LevelRating::Demon(demon_rating) => demon_rating.into_response_value(), +// _ => panic!("got {:?}, please handle before calling this function", self), +// } +// } +// } +// +// impl DemonRating { +// fn from_response_value(value: i32) -> DemonRating { +// match value { +// 10 => DemonRating::Easy, +// 20 => DemonRating::Medium, +// 30 => DemonRating::Hard, +// 40 => DemonRating::Insane, +// 50 => DemonRating::Extreme, +// _ => DemonRating::Unknown(value), +// } +// } +// +// fn into_response_value(self) -> i32 { +// match self { +// DemonRating::Unknown(value) => value, +// DemonRating::Easy => 10, +// DemonRating::Medium => 20, +// DemonRating::Hard => 30, +// DemonRating::Insane => 40, +// DemonRating::Extreme => 50, +// } +// } +// } -include!(concat!(env!("OUT_DIR"), "/partial_level.boilerplate")); -include!(concat!(env!("OUT_DIR"), "/level.boilerplate")); +// #[allow(non_upper_case_globals, unused_imports)] +// const _partial_level: () = { +// use crate::{ +// serde::{DeError, HasRobtopFormat, IndexedDeserializer, IndexedSerializer, PercentDecoded, SerError, Thunk, RefThunk, Base64Decoded}, +// }; +// use serde::{Deserialize, Serialize}; +// use std::{borrow::{Cow, Borrow}, io::Write}; +// #[derive(Serialize, Deserialize)] +// struct InternalLevel<'src, 'bor> { +// #[serde(rename = "1")] +// index_1: u64, +// #[serde(rename = "2")] +// index_2: &'src str, +// #[serde(rename = "3")] +// index_3: Option>>, +// #[serde(rename = "5")] +// index_5: u32, +// #[serde(rename = "6")] +// index_6: u64, +// #[serde(serialize_with = "crate::util::false_to_empty_string")] +// #[serde(rename = "25")] +// index_25: bool, +// #[serde(serialize_with = "crate::util::true_to_ten")] +// #[serde(rename = "8")] +// index_8: bool, +// #[serde(rename = "9")] +// index_9: i32, +// #[serde(serialize_with = "crate::util::false_to_empty_string")] +// #[serde(rename = "17")] +// index_17: bool, +// #[serde(rename = "10")] +// index_10: u32, +// #[serde(rename = "12")] +// index_12: u8, +// #[serde(rename = "13")] +// index_13: u8, +// #[serde(rename = "14")] +// index_14: i32, +// #[serde(with = "level_length")] +// #[serde(rename = "15")] +// index_15: LevelLength, +// #[serde(rename = "18")] +// index_18: u8, +// #[serde(rename = "19")] +// index_19: Featured, +// #[serde(with = "crate::util::default_to_none")] +// #[serde(rename = "30")] +// index_30: Option, +// #[serde(rename = "31")] +// index_31: bool, +// #[serde(with = "crate::util::default_to_none")] +// #[serde(rename = "35")] +// index_35: Option, +// #[serde(rename = "37")] +// index_37: u8, +// #[serde(rename = "38")] +// index_38: bool, +// #[serde(with = "crate::util::default_to_none")] +// #[serde(rename = "39")] +// index_39: Option, +// #[serde(rename = "42")] +// index_42: bool, +// #[serde(rename = "43")] +// index_43: u8, +// #[serde(with = "crate::util::default_to_none")] +// #[serde(rename = "45")] +// index_45: Option, +// #[serde(rename = "46")] +// index_46: Option<&'src str>, +// #[serde(rename = "47")] +// index_47: Option<&'src str>, +// } +// impl<'src> HasRobtopFormat<'src> for Level<'src, ()> { +// fn from_robtop_str(input: &'src str) -> Result { +// let internal = InternalLevel::deserialize(&mut IndexedDeserializer::new(input, ":", true))?; +// Ok(Self { +// level_id: internal.index_1, +// name: Cow::Borrowed(internal.index_2), +// description: match internal.index_3 {None => None, Some(RefThunk::Unprocessed(unproc)) => Some(Thunk::Unprocessed(unproc)), _ => unreachable!()}, +// version: internal.index_5, +// creator: internal.index_6, +// downloads: internal.index_10, +// gd_version: internal.index_13.into(), +// likes: internal.index_14, +// length: internal.index_15, +// stars: internal.index_18, +// featured: internal.index_19, +// copy_of: internal.index_30, +// two_player: internal.index_31, +// custom_song: internal.index_35, +// coin_amount: internal.index_37, +// coins_verified: internal.index_38, +// stars_requested: internal.index_39, +// is_epic: internal.index_42, +// object_amount: internal.index_45, +// index_46: internal.index_46.map(Cow::Borrowed), +// index_47: internal.index_47.map(Cow::Borrowed), +// main_song: if internal.index_35.is_some() { // custom_song +// None +// } else { +// Some(MainSong::from(internal.index_12)) // main_song +// } +// , +// difficulty: if !internal.index_8 { // has_difficulty_rating +// LevelRating::NotAvailable +// } else if internal.index_25 { // is_auto +// LevelRating::Auto +// } else if internal.index_17 { // is_demon +// LevelRating::Demon(DemonRating::from_response_value(internal.index_9)) +// } else { +// LevelRating::from_response_value(internal.index_9) // rating +// } +// , +// level_data: (), +// }) +// } +// fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { +// let internal = InternalLevel { +// index_1: self.level_id, +// index_2: self.name.as_ref(), +// index_3: self.description.as_ref().map(|t| t.as_ref_thunk()), +// index_5: self.version, +// index_6: self.creator, +// index_25: self.difficulty == LevelRating::Auto +// , +// index_8: self.difficulty != LevelRating::NotAvailable +// , +// index_9: self.difficulty.into_response_value() +// , +// index_17: self.difficulty.is_demon() +// , +// index_10: self.downloads, +// index_12: self.main_song.map(|song| song.main_song_id).unwrap_or(0) +// , +// index_13: self.gd_version.into(), +// index_14: self.likes, +// index_15: self.length, +// index_18: self.stars, +// index_19: self.featured, +// index_30: self.copy_of, +// index_31: self.two_player, +// index_35: self.custom_song, +// index_37: self.coin_amount, +// index_38: self.coins_verified, +// index_39: self.stars_requested, +// index_42: self.is_epic, +// index_43: match self.difficulty { +// LevelRating::Demon(DemonRating::Easy) => 3, +// LevelRating::Demon(DemonRating::Medium) => 4, +// LevelRating::Demon(DemonRating::Hard) => 0, +// LevelRating::Demon(DemonRating::Insane) => 5, +// LevelRating::Demon(DemonRating::Extreme) => 6, +// _ => 5, // this seems to be the default for non-demons +// } +// , +// index_45: self.object_amount, +// index_46: self.index_46.as_deref(), +// index_47: self.index_47.as_deref(), +// }; +// internal.serialize(&mut IndexedSerializer::new(":", writer, true)) +// } +// } +//}; \ No newline at end of file diff --git a/src/model/level/local_level.rs b/src/model/level/local_level.rs new file mode 100644 index 0000000..4082251 --- /dev/null +++ b/src/model/level/local_level.rs @@ -0,0 +1,368 @@ +use crate::model::level::object::speed::Speed; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use flate2::read::{GzDecoder, GzEncoder, ZlibDecoder}; +use flate2::Compression; +use std::io::Read; +use crate::model::level::{LevelProcessError, Password}; +use crate::{HasRobtopFormat, Thunk, ThunkContent}; +use crate::model::level::object::{LevelObject, ObjectData}; + +#[derive(Debug, PartialEq, Clone, Default, Copy, Serialize, Deserialize)] +pub struct LevelMetadata { + pub starting_speed: Speed, + pub song_offset: f64, + pub song_fade_in: bool, + pub song_fade_out: bool, + pub dual_start: bool, + pub two_player_controls: bool, + pub start_gravity_inverted: bool, + // ... other fields in the metadata section ... +} + +/// Struct encapsulating the additional level data returned when actually downloading a level +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct LevelData<'a> { + /// The level's password + /// + /// ## GD Internals: + /// This value is provided at index `27`. For encoding details, see the documentation on the + /// [`Password`] variants + pub password: Password, + + /// The time passed since the `Level` was uploaded, as a string. Note that these strings are + /// very imprecise, as they are only of the form "x months ago", or similar. + /// + /// TODO: Parse these into an enum + /// + /// ## GD Internals: + /// This value is provided at index `28` + pub time_since_upload: Cow<'a, str>, + + /// The time passed since the `Level` was last updated, as a string. Note that these strings are + /// very imprecise, as they are only of the form "x months ago", or similar. + /// + /// ## GD Internals: + /// This value is provided at index `29` + pub time_since_update: Cow<'a, str>, + + /// According to the GDPS source, this is a value called `extraString` + /// + /// ## GD Internals: + /// This value is provided at index `36` + pub extra_string: Option>, + + /// If this [`Level`] has a low detail option. + /// + /// ## GD Internals + /// This value is provided at index `40` + pub has_low_detail_mode: bool, + + /// The number of seconds this [`Level`] has been opened in the level editor. + /// + /// ## GD Internals + /// This value is provided at index `46` + pub editor_time: Option, + + /// The number of seconds this [`Level`] has been opened in the level editor include the amount from copies. + /// + /// ## GD Internals + /// This value is provided at index `47` + pub copy_editor_time: Option, + + /// The level's actual data. + /// + /// ## GD Internals: + /// This value is provided at index `4`, and is urlsafe base64 encoded and `DEFLATE` compressed + #[serde(borrow)] + pub level_data: Thunk<'a, Objects>, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Objects { + pub meta: LevelMetadata, + pub objects: Vec, +} + +impl<'a> ThunkContent<'a> for Objects { + type Error = LevelProcessError; + + fn from_unprocessed(unprocessed: &'a str) -> Result { + // Doing the entire base64 in one go is actually faster than using base64::read::DecoderReader and + // having the two readers go back and forth. + let decoded = base64::decode_config(unprocessed, base64::URL_SAFE).map_err(LevelProcessError::Base64)?; + + // Here's the deal: Robtop decompresses all levels by calling the zlib function 'inflateInit2_' with + // the second argument set to 47. This basically tells zlib "this data might be compressed using + // zlib or gzip format, with window size at most 15, but you gotta figure it out yourself". + // However, flate2 doesnt expose this option, so we have to manually determine whether we + // have gzip or zlib compression. + + let mut decompressed = String::new(); + + match &decoded[..2] { + // gz magic bytes + [0x1f, 0x8b] => { + let mut decoder = GzDecoder::new(&decoded[..]); + + decoder.read_to_string(&mut decompressed).map_err(LevelProcessError::Compression)?; + }, + // There's no such thing as "zlib magic bytes", but the first byte stores some information about how the data is compressed. + // '0x78' is the first byte for the compression method robtop used (note: this is only used for very old levels, as he switched + // to gz for newer levels) + [0x78, _] => { + let mut decoder = ZlibDecoder::new(&decoded[..]); + + decoder.read_to_string(&mut decompressed).map_err(LevelProcessError::Compression)?; + }, + _ => return Err(LevelProcessError::UnknownCompression), + } + + let mut iter = decompressed.split_terminator(';'); + + let metadata_string = match iter.next() { + Some(meta) => meta, + None => return Err(LevelProcessError::MissingMetadata), + }; + + let meta = LevelMetadata::from_robtop_str(metadata_string).map_err(|err| LevelProcessError::Deserialize(err.to_string()))?; + + iter.map(|object_string| LevelObject::from_robtop_str(object_string)) + .collect::>() + .map(|objects| Objects { meta, objects }) + .map_err(|err| LevelProcessError::Deserialize(err.to_string())) + } + + fn as_unprocessed(&self) -> Result, LevelProcessError> { + let mut bytes = Vec::new(); + + self.meta.write_robtop_data(&mut bytes).map_err(LevelProcessError::Serialize)?; + + bytes.push(b';'); + + for object in &self.objects { + object.write_robtop_data(&mut bytes).map_err(LevelProcessError::Serialize)?; + bytes.push(b';'); + } + + // FIXME(game specific): Should we remember the compression scheme (zlib or gz) from above, or just + // always re-compress using gz? Since the game dyncamially detects the compression method, we're + // compatible either way. + + let mut encoder = GzEncoder::new(&bytes[..], Compression::new(9)); // TODO: idk what these values mean + let mut compressed = Vec::new(); + + encoder.read_to_end(&mut compressed).map_err(LevelProcessError::Compression)?; + + Ok(Cow::Owned(base64::encode_config(&compressed, base64::URL_SAFE))) + } +} + +impl Objects { + pub fn length_in_seconds(&self) -> f32 { + let mut portals = Vec::new(); + let mut furthest_x = 0.0; + + for object in &self.objects { + if let ObjectData::SpeedPortal { checked: true, speed } = object.metadata { + portals.push((object.x, speed)) + } + + furthest_x = f32::max(furthest_x, object.x); + } + + portals.sort_by(|(x1, _), (x2, _)| x1.partial_cmp(x2).unwrap()); + + get_seconds_from_x_pos(furthest_x, self.meta.starting_speed, &portals) + } +} + +fn get_seconds_from_x_pos(pos: f32, start_speed: Speed, portals: &[(f32, Speed)]) -> f32 { + let mut speed: f32 = start_speed.into(); + + if portals.is_empty() { + return pos / speed + } + + let mut last_obj_pos = 0.0; + let mut total_time = 0.0; + + for (x, portal_speed) in portals { + // distance between last portal and this one + let current_segment = x - last_obj_pos; + + // break if we're past the position we want to calculate the position to + if pos <= current_segment { + break + } + + // Calculate time spent in this segment and add to total time + total_time += current_segment / speed; + + speed = (*portal_speed).into(); + + last_obj_pos = *x; + } + + // add the time spent between end and last portal to total time and return + (pos - last_obj_pos) / speed + total_time +} + +mod internal { + use std::borrow::Cow; + use crate::{DeError, model::level::{ + object::speed::Speed, + local_level::{LevelMetadata, LevelData}, + }, serde::{HasRobtopFormat, IndexedDeserializer, IndexedSerializer, SerError}, Thunk}; + use serde::{Deserialize, Serialize}; + use std::io::Write; + use crate::model::level::local_level::Objects; + use crate::model::level::Password; + use crate::serde::{Internal, RefThunk}; + + impl <'a> HasRobtopFormat<'a> for LevelData<'a> { + fn from_robtop_str(input: &'a str) -> Result { + let internal = InternalLevelData::deserialize(&mut IndexedDeserializer::new(input, ":", true))?; + + Ok(Self { + level_data: match internal.index_4 { + RefThunk::Unprocessed(unproc) => Thunk::Unprocessed(unproc), + _ => unreachable!() + }, + password: internal.index_27.0, + time_since_upload: Cow::Borrowed(internal.index_28), + time_since_update: Cow::Borrowed(internal.index_29), + extra_string: internal.index_36.map(Cow::Borrowed), + has_low_detail_mode: internal.index_40, + editor_time: internal.index_46, + copy_editor_time: internal.index_47 + }) + } + + fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { + let internal = InternalLevelData { + index_4: self.level_data.as_ref_thunk(), + index_27: Internal(self.password), + index_28: self.time_since_upload.as_ref(), + index_29: self.time_since_update.as_ref(), + index_36: self.extra_string.as_deref(), + index_40: self.has_low_detail_mode, + index_46: self.editor_time, + index_47: self.copy_editor_time + }; + + internal.serialize(&mut IndexedSerializer::new(":", writer, true)) + } + } + + impl<'a> HasRobtopFormat<'a> for LevelMetadata { + fn from_robtop_str(input: &'a str) -> Result { + let int = InternalLevelMetadata::deserialize(&mut IndexedDeserializer::new(input, ",", true))?; + + Ok(LevelMetadata { + starting_speed: match int.starting_speed { + 0 => Speed::Slow, + 1 => Speed::Normal, + 2 => Speed::Medium, + 3 => Speed::Fast, + 4 => Speed::VeryFast, + unknown => Speed::Unknown(unknown), + }, + song_offset: int.song_offset, + song_fade_in: int.song_fade_in, + song_fade_out: int.song_fade_out, + dual_start: int.dual_start, + two_player_controls: int.two_player_controls, + start_gravity_inverted: int.start_gravity_inverted, + }) + } + + fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { + let internal = InternalLevelMetadata { + starting_speed: match self.starting_speed { + Speed::Slow => 0, + Speed::Normal => 1, + Speed::Medium => 2, + Speed::Fast => 3, + Speed::VeryFast => 4, + Speed::Unknown(unknown) => unknown, + }, + song_offset: self.song_offset, + song_fade_in: self.song_fade_in, + song_fade_out: self.song_fade_out, + dual_start: self.dual_start, + two_player_controls: self.two_player_controls, + start_gravity_inverted: self.start_gravity_inverted, + }; + + internal.serialize(&mut IndexedSerializer::new(",", writer, true)) + } + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct InternalLevelData<'src, 'bor> { + #[serde(rename = "4")] + index_4: RefThunk<'src, 'bor, Objects>, + #[serde(rename = "27")] + index_27: Internal, + #[serde(rename = "28")] + index_28: &'src str, + #[serde(rename = "29")] + index_29: &'src str, + #[serde(rename = "36")] + index_36: Option<&'src str>, + #[serde(rename = "40")] + index_40: bool, + #[serde(rename = "46")] + index_46: Option, + #[serde(rename = "47")] + index_47: Option, + } + + #[derive(Debug, Serialize, Deserialize, Clone, Copy)] + pub struct InternalLevelMetadata { + #[serde(rename = "kA4", default = "one")] + starting_speed: u8, + + #[serde(rename = "kA13", default)] + song_offset: f64, + + #[serde(rename = "kA15", default)] + song_fade_in: bool, + + #[serde(rename = "kA16", default)] + song_fade_out: bool, + + #[serde(rename = "kA8", default)] + dual_start: bool, + + #[serde(rename = "kA10", default)] + two_player_controls: bool, + + #[serde(rename = "kA11", default)] + start_gravity_inverted: bool, //.. other fields + } + + fn one() -> u8 { + 1 + } +} + +// starting_speed(index = kA4), +// song_offset(index = kA13), +// fade_in(index = kA15), +// fade_out(index = kA16), +// song guidelines: kA16 +// background texture index: kA6 +// ground texture index: kA7 +// ground line index: kA17 +// font: kA18 +// color page (???): kS39 +// starting game mode: kA2 +// starting size: kA3 +// dual_start(index = kA8), +// level/start pos (???): kA9 +// two_player_controls(index = kA10), +// start_gravity_inverted(index = kA11, optional), + + diff --git a/src/model/level/metadata.rs b/src/model/level/metadata.rs deleted file mode 100644 index 8dbad30..0000000 --- a/src/model/level/metadata.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::model::level::object::speed::Speed; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Clone, Default, Copy, Serialize, Deserialize)] -pub struct LevelMetadata { - pub starting_speed: Speed, - pub song_offset: f64, - pub song_fade_in: bool, - pub song_fade_out: bool, - pub dual_start: bool, - pub two_player_controls: bool, - pub start_gravity_inverted: bool, - // ... other fields in the metadata section ... -} - -mod internal { - use crate::{ - model::level::{metadata::LevelMetadata, object::speed::Speed}, - serde::{HasRobtopFormat, IndexedDeserializer, IndexedSerializer}, - DeError, SerError, - }; - use serde::{Deserialize, Serialize}; - use std::io::Write; - - impl<'a> HasRobtopFormat<'a> for LevelMetadata { - fn from_robtop_str(input: &'a str) -> Result { - let int = InternalLevelMetadata::deserialize(&mut IndexedDeserializer::new(input, ",", true))?; - - Ok(LevelMetadata { - starting_speed: match int.starting_speed { - 0 => Speed::Slow, - 1 => Speed::Normal, - 2 => Speed::Medium, - 3 => Speed::Fast, - 4 => Speed::VeryFast, - unknown => Speed::Unknown(unknown), - }, - song_offset: int.song_offset, - song_fade_in: int.song_fade_in, - song_fade_out: int.song_fade_out, - dual_start: int.dual_start, - two_player_controls: int.two_player_controls, - start_gravity_inverted: int.start_gravity_inverted, - }) - } - - fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { - let internal = InternalLevelMetadata { - starting_speed: match self.starting_speed { - Speed::Slow => 0, - Speed::Normal => 1, - Speed::Medium => 2, - Speed::Fast => 3, - Speed::VeryFast => 4, - Speed::Unknown(unknown) => unknown, - }, - song_offset: self.song_offset, - song_fade_in: self.song_fade_in, - song_fade_out: self.song_fade_out, - dual_start: self.dual_start, - two_player_controls: self.two_player_controls, - start_gravity_inverted: self.start_gravity_inverted, - }; - - internal.serialize(&mut IndexedSerializer::new(",", writer, true)) - } - } - - #[derive(Debug, Serialize, Deserialize, Clone, Copy)] - pub struct InternalLevelMetadata { - #[serde(rename = "kA4", default = "one")] - starting_speed: u8, - - #[serde(rename = "kA13", default)] - song_offset: f64, - - #[serde(rename = "kA15", default)] - song_fade_in: bool, - - #[serde(rename = "kA16", default)] - song_fade_out: bool, - - #[serde(rename = "kA8", default)] - dual_start: bool, - - #[serde(rename = "kA10", default)] - two_player_controls: bool, - - #[serde(rename = "kA11", default)] - start_gravity_inverted: bool, //.. other fields - } - - fn one() -> u8 { - 1 - } -} - -// starting_speed(index = kA4), -// song_offset(index = kA13), -// fade_in(index = kA15), -// fade_out(index = kA16), -// song guidelines: kA16 -// background texture index: kA6 -// ground texture index: kA7 -// ground line index: kA17 -// font: kA18 -// color page (???): kS39 -// starting game mode: kA2 -// starting size: kA3 -// dual_start(index = kA8), -// level/start pos (???): kA9 -// two_player_controls(index = kA10), -// start_gravity_inverted(index = kA11, optional), diff --git a/src/model/level/mod.rs b/src/model/level/mod.rs index fc7e27e..f47fbb7 100644 --- a/src/model/level/mod.rs +++ b/src/model/level/mod.rs @@ -1,45 +1,31 @@ //! Module containing structs modelling Geometry Dash levels as they are returned from the boomlings //! servers +use base64::URL_SAFE; +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::Error +}; use std::{ - borrow::Cow, fmt::{Display, Formatter}, - io::Read, }; - -use base64::URL_SAFE; -use flate2::read::{GzDecoder, GzEncoder, ZlibDecoder}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use crate::{ - model::{ - creator::Creator, - level::{ - metadata::LevelMetadata, - object::{speed::Speed, LevelObject, ObjectData}, - }, - song::{MainSong, NewgroundsSong}, - GameVersion, - }, - serde::{Base64Decoded, HasRobtopFormat, Internal, ProcessError, ThunkContent}, - util, SerError, Thunk, + util, SerError, + model::level::local_level::Objects, + serde::{Internal, ProcessError}, }; -use flate2::Compression; -use serde::de::Error; - -// use flate2::read::GzDecoder; -// use std::io::Read; mod internal; -pub mod metadata; +pub mod local_level; pub mod object; +pub mod online_level; /// Enum representing the possible level lengths known to dash-rs #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum LevelLength { /// Enum variant that's used by the [`From`](From) impl for when an /// unrecognized value is passed - Unknown(i32), + Unknown(u8), /// Tiny /// @@ -77,12 +63,38 @@ pub enum LevelLength { ExtraLong, } +impl From for LevelLength { + fn from(i: u8) -> Self { + match i { + 0 => LevelLength::Tiny, + 1 => LevelLength::Short, + 2 => LevelLength::Medium, + 3 => LevelLength::Long, + 4 => LevelLength::ExtraLong, + _ => LevelLength::Unknown(i) + } + } +} + +impl From for u8 { + fn from(length: LevelLength) -> Self { + match length { + LevelLength::Tiny => 0, + LevelLength::Short => 1, + LevelLength::Medium => 2, + LevelLength::Long => 3, + LevelLength::ExtraLong => 4, + LevelLength::Unknown(inner) => inner + } + } +} + /// Enum representing the possible level ratings #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum LevelRating { - /// Enum variant that's used by the [`From`](From) impl for when an + /// Enum variant that's used by the [`From`](From) impl for when an /// unrecognized value is passed - Unknown(i32), + Unknown(i8), /// Not Available, sometimes referred to as `N/A` or `NA` /// @@ -150,12 +162,44 @@ impl LevelRating { } } +impl From for LevelRating { + fn from(i: i8) -> Self { + match i { + -3 => LevelRating::Auto, + -2 => LevelRating::Demon(DemonRating::Hard), + -1 => LevelRating::NotAvailable, + 10 => LevelRating::Easy, + 20 => LevelRating::Normal, + 30 => LevelRating::Hard, + 40 => LevelRating::Harder, + 50 => LevelRating::Insane, + _ => LevelRating::Unknown(i) + } + } +} + +impl From for i8 { + fn from(rating: LevelRating) -> Self { + match rating { + LevelRating::Auto => -3, + LevelRating::Demon(_) => -2, + LevelRating::NotAvailable => -1, + LevelRating::Easy => 10, + LevelRating::Normal => 20, + LevelRating::Hard => 30, + LevelRating::Harder => 40, + LevelRating::Insane => 50, + LevelRating::Unknown(inner) => inner + } + } +} + /// Enum representing the possible demon difficulties #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum DemonRating { /// Enum variant that's used by the [`From`](From) impl for when an /// unrecognized value is passed - Unknown(i32), + Unknown(i8), /// Easy demon /// @@ -193,6 +237,32 @@ pub enum DemonRating { Extreme, } +impl From for DemonRating{ + fn from(i: i8) -> Self { + match i { + 10 => DemonRating::Easy, + 20 => DemonRating::Medium, + 30 => DemonRating::Hard, + 40 => DemonRating::Insane, + 50 => DemonRating::Extreme, + _ => DemonRating::Unknown(i) + } + } +} + +impl From for i8{ + fn from(rating: DemonRating) -> Self { + match rating { + DemonRating::Easy => 10, + DemonRating::Medium => 20, + DemonRating::Hard => 30, + DemonRating::Insane => 40, + DemonRating::Extreme => 50, + DemonRating::Unknown(inner) => inner + } + } +} + /// Enum representing a levels featured state #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(from = "i32", into = "i32")] @@ -222,11 +292,11 @@ pub enum Featured { } impl From for Featured { - fn from(int: i32) -> Self { - match int { + fn from(i: i32) -> Self { + match i { -1 => Featured::Unfeatured, 0 => Featured::NotFeatured, - _ => Featured::Featured(int as u32), + _ => Featured::Featured(i as u32), } } } @@ -281,8 +351,8 @@ pub enum Password { impl Serialize for Password { fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, + where + S: Serializer, { match self { Password::NoCopy => serializer.serialize_none(), @@ -294,8 +364,8 @@ impl Serialize for Password { impl<'de> Deserialize<'de> for Password { fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, + where + D: Deserializer<'de>, { let level_password = >::deserialize(deserializer)?; @@ -366,8 +436,8 @@ impl Password { impl Serialize for Internal { fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, + where + S: Serializer, { match self.0 { Password::FreeCopy => serializer.serialize_str("Aw=="), @@ -382,8 +452,8 @@ impl Serialize for Internal { impl<'de> Deserialize<'de> for Internal { fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, + where + D: Deserializer<'de>, { let raw_password_data = <&str>::deserialize(deserializer)?; @@ -401,265 +471,6 @@ impl Display for Password { } } -pub type ListedLevel<'a> = Level<'a, (), Option>, Option>>; - -/// Struct representing levels as returned by the boomlings API. -/// -/// These can be retrieved using [`LevelRequest`](crate::request::level::LevelRequest)s or -/// [`LevelsRequest`](crate::request::level::LevelsRequest). The `level_data` field is only set if -/// the level was retrieved using a request of the former kind. For requests of the latter type, it -/// will be set to [`None`] -/// -/// ## GD Internals: -/// The Geometry Dash servers provide lists of partial levels via the -/// `getGJLevels` endpoint. Complete levels can be downloaded via `downloadGJLevel` -/// -/// ### Unmapped values: -/// + Index `8`: Index 8 is a boolean value indicating whether the level has a -/// difficulty rating that isn't N/A. This is equivalent to checking if -/// [`Level::difficulty`] is unequal to -/// [`LevelRating::NotAvailable`] -/// + Index `17`: Index 17 is a boolean value indicating whether -/// the level is a demon level. This is equivalent to checking if -/// [`Level::difficulty`] is the [`LevelRating::Demon`] variant. -/// + Index `25`: Index 25 is a boolean value indicating -/// whether the level is an auto level. This is equivalent to checking if -/// [`Level::difficulty`] is equal to -/// [`LevelRating::Auto`] -/// + Index `43`: This index is an indicator of demon difficulty as follows: -/// 3 = easy demon, -/// 4 = medium demon, -/// 5 = insane demon, -/// 6 = extreme demon. -/// In other cases it's hard demon (thanks Ryder!). However, since we extract this information from -/// index 9, dash-rs ignores this value. -/// -/// ### Value only provided via `downloadGJLevels` -/// These values are not provided for by the `getGJLevels` endpoint and are -/// thus modelled in the [`LevelData`] struct: `4`, `27`, -/// `28`, `29`, `36`, `40` -/// -/// ### Unused indices: -/// The following indices aren't used by the Geometry Dash servers: `11`, `16`, -/// `17`, `20`, `21`, `22`, `23`, `24`, `26`, `31`, `32`, `33`, `34`, `40`, -/// `41`, `44` -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct Level<'a, Data = LevelData<'a>, Song = Option, User = u64> { - /// The level's unique level id - /// - /// ## GD Internals: - /// This value is provided at index `1`. - pub level_id: u64, - - /// The level's name - /// - /// ## GD Internals: - /// This value is provided at index `2`. - #[serde(borrow)] - pub name: Cow<'a, str>, - - /// The level's description. Is [`None`] if the creator didn't put any - /// description. - /// - /// ## GD Internals: - /// This value is provided at index `3` and encoded using urlsafe base 64. - pub description: Option>>, - - /// The [`Level`]'s version. The version get incremented every time - /// the level is updated, and the initial version is always version 1. - /// - /// ## GD Internals: - /// This value is provided at index `5`. - pub version: u32, - - /// The ID of the level's creator - /// - /// ## GD Internals: - /// This value is provided at index `6`. - pub creator: User, - - /// The difficulty of this [`Level`] - /// - /// ## GD Internals: - /// This value is a construct from the value at the indices `9`, `17` and - /// `25`, whereas index 9 is an integer representation of either the - /// [`LevelRating`] or the [`DemonRating`] - /// struct, depending on the value of index 17. - /// - /// If index 25 is set to true, the level is an auto level and the value at - /// index 9 is some nonsense, in which case it is ignored. - pub difficulty: LevelRating, - - /// The amount of downloads - /// - /// ## GD Internals: - /// This value is provided at index `10` - pub downloads: u32, - - /// The [`MainSong`] the level uses, if any. - /// - /// ## GD Internals: - /// This value is provided at index `12`. Interpretation is additionally - /// dependant on the value at index `35` (the custom song id), as - /// without that information, a value of `0` for - /// this field could either mean the level uses `Stereo Madness` or no - /// main song. - pub main_song: Option, - - /// The gd version the request was uploaded/last updated in. - /// - /// ## GD Internals: - /// This value is provided at index `13` - pub gd_version: GameVersion, - - /// The amount of likes this [`Level`] has received - /// - /// ## GD Internals: - /// This value is provided at index `14` - pub likes: i32, - - /// The length of this [`Level`] - /// - /// ## GD Internals: - /// This value is provided as an integer representation of the - /// [`LevelLength`] struct at index `15` - pub length: LevelLength, - - /// The amount of stars completion of this [`Level`] awards - /// - /// ## GD Internals: - /// This value is provided at index `18` - pub stars: u8, - - /// This [`Level`]s featured state - /// - /// ## GD Internals: - /// This value is provided at index `19` - pub featured: Featured, - - /// The ID of the level this [`Level`] is a copy of, or [`None`], if - /// this [`Level`] isn't a copy. - /// - /// ## GD Internals: - /// This value is provided at index `30` - pub copy_of: Option, - - /// Value indicating whether this level is played in two-player mode - /// - /// ## GD Internals: - /// This value is provided at index `31` and actually sanely encoded - pub two_player: bool, - - /// The id of the newgrounds song this [`Level`] uses, or [`None`] - /// if it useds a main song. - /// - /// ## GD Internals: - /// This value is provided at index `35`, and a value of `0` means, that no - /// custom song is used. - pub custom_song: Song, - - /// The amount of coins in this [`Level`] - /// - /// ## GD Internals: - /// This value is provided at index `37` - pub coin_amount: u8, - - /// Value indicating whether the user coins (if present) in this - /// [`Level`] are verified - /// - /// ## GD Internals: - /// This value is provided at index `38`, as an integer - pub coins_verified: bool, - - /// The amount of stars the level creator has requested when uploading this - /// [`Level`], or [`None`] if no stars were requested. - /// - /// ## GD Internals: - /// This value is provided at index `39`, and a value of `0` means no stars - /// were requested - pub stars_requested: Option, - - /// Value indicating whether this [`Level`] is epic - /// - /// ## GD Internals: - /// This value is provided at index `42`, as an integer - pub is_epic: bool, - - /// The amount of objects in this [`Level`]. Note that a value of `None` _does not_ mean - /// that there are no objects in the level, but rather that the server's didn't provide an - /// object count. - /// - /// ## GD Internals: - /// This value is provided at index `45`, although only for levels uploaded - /// in version 2.1 or later. For all older levels this is always `None` - pub object_amount: Option, - - /// According to the GDPS source this is always `1`, although that is - /// evidently wrong - /// - /// ## GD Internals: - /// This value is provided at index `46` and seems to be an integer - pub index_46: Option>, - - /// According to the GDPS source, this is always `2`, although that is - /// evidently wrong - /// - /// ## GD Internals: - /// This value is provided at index `47` and seems to be an integer - pub index_47: Option>, - - /// Additional data about this level that can be retrieved by downloading the level. - /// - /// This is [`None`] for levels retrieved via the "overview" endpoint `getGJLevels`. - pub level_data: Data, -} - -/// Struct encapsulating the additional level data returned when actually downloading a level -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct LevelData<'a> { - /// The level's actual data. - /// - /// ## GD Internals: - /// This value is provided at index `4`, and is urlsafe base64 encoded and `DEFLATE` compressed - #[serde(borrow)] - pub level_data: Thunk<'a, Objects>, - - /// The level's password - /// - /// ## GD Internals: - /// This value is provided at index `27`. For encoding details, see the documentation on the - /// [`Password`] variants - pub password: Password, - - /// The time passed since the `Level` was uploaded, as a string. Note that these strings are - /// very imprecise, as they are only of the form "x months ago", or similar. - /// - /// TODO: Parse these into an enum - /// - /// ## GD Internals: - /// This value is provided at index `28` - pub time_since_upload: Cow<'a, str>, - - /// The time passed since the `Level` was last updated, as a string. Note that these strings are - /// very imprecise, as they are only of the form "x months ago", or similar. - /// - /// ## GD Internals: - /// This value is provided at index `29` - pub time_since_update: Cow<'a, str>, - - /// According to the GDPS source, this is a value called `extraString` - /// - /// ## GD Internals: - /// This value is provided at index `36` - pub index_36: Option>, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct Objects { - pub meta: LevelMetadata, - pub objects: Vec, -} - #[derive(Debug)] pub enum LevelProcessError { Deserialize(String), @@ -693,130 +504,6 @@ impl Display for LevelProcessError { impl<'a> std::error::Error for LevelProcessError {} -impl<'a> ThunkContent<'a> for Objects { - type Error = LevelProcessError; - - fn from_unprocessed(unprocessed: &'a str) -> Result { - // Doing the entire base64 in one go is actually faster than using base64::read::DecoderReader and - // having the two readers go back and forth. - let decoded = base64::decode_config(unprocessed, base64::URL_SAFE).map_err(LevelProcessError::Base64)?; - - // Here's the deal: Robtop decompresses all levels by calling the zlib function 'inflateInit2_' with - // the second argument set to 47. This basically tells zlib "this data might be compressed using - // zlib or gzip format, with window size at most 15, but you gotta figure it out yourself". - // However, flate2 doesnt expose this option, so we have to manually determine whether we - // have gzip or zlib compression. - - let mut decompressed = String::new(); - - match &decoded[..2] { - // gz magic bytes - [0x1f, 0x8b] => { - let mut decoder = GzDecoder::new(&decoded[..]); - - decoder.read_to_string(&mut decompressed).map_err(LevelProcessError::Compression)?; - }, - // There's no such thing as "zlib magic bytes", but the first byte stores some information about how the data is compressed. - // '0x78' is the first byte for the compression method robtop used (note: this is only used for very old levels, as he switched - // to gz for newer levels) - [0x78, _] => { - let mut decoder = ZlibDecoder::new(&decoded[..]); - - decoder.read_to_string(&mut decompressed).map_err(LevelProcessError::Compression)?; - }, - _ => return Err(LevelProcessError::UnknownCompression), - } - - let mut iter = decompressed.split_terminator(';'); - - let metadata_string = match iter.next() { - Some(meta) => meta, - None => return Err(LevelProcessError::MissingMetadata), - }; - - let meta = LevelMetadata::from_robtop_str(metadata_string).map_err(|err| LevelProcessError::Deserialize(err.to_string()))?; - - iter.map(|object_string| LevelObject::from_robtop_str(object_string)) - .collect::>() - .map(|objects| Objects { meta, objects }) - .map_err(|err| LevelProcessError::Deserialize(err.to_string())) - } - - fn as_unprocessed(&self) -> Result, LevelProcessError> { - let mut bytes = Vec::new(); - - self.meta.write_robtop_data(&mut bytes).map_err(LevelProcessError::Serialize)?; - - bytes.push(b';'); - - for object in &self.objects { - object.write_robtop_data(&mut bytes).map_err(LevelProcessError::Serialize)?; - bytes.push(b';'); - } - - // FIXME(game specific): Should we remember the compression scheme (zlib or gz) from above, or just - // always re-compress using gz? Since the game dyncamially detects the compression method, we're - // compatible either way. - - let mut encoder = GzEncoder::new(&bytes[..], Compression::new(9)); // TODO: idk what these values mean - let mut compressed = Vec::new(); - - encoder.read_to_end(&mut compressed).map_err(LevelProcessError::Compression)?; - - Ok(Cow::Owned(base64::encode_config(&compressed, base64::URL_SAFE))) - } -} - -impl Objects { - pub fn length_in_seconds(&self) -> f32 { - let mut portals = Vec::new(); - let mut furthest_x = 0.0; - - for object in &self.objects { - if let ObjectData::SpeedPortal { checked: true, speed } = object.metadata { - portals.push((object.x, speed)) - } - - furthest_x = f32::max(furthest_x, object.x); - } - - portals.sort_by(|(x1, _), (x2, _)| x1.partial_cmp(x2).unwrap()); - - get_seconds_from_x_pos(furthest_x, self.meta.starting_speed, &portals) - } -} - -fn get_seconds_from_x_pos(pos: f32, start_speed: Speed, portals: &[(f32, Speed)]) -> f32 { - let mut speed: f32 = start_speed.into(); - - if portals.is_empty() { - return pos / speed - } - - let mut last_obj_pos = 0.0; - let mut total_time = 0.0; - - for (x, portal_speed) in portals { - // distance between last portal and this one - let current_segment = x - last_obj_pos; - - // break if we're past the position we want to calculate the position to - if pos <= current_segment { - break - } - - // Calculate time spent in this segment and add to total time - total_time += current_segment / speed; - - speed = (*portal_speed).into(); - - last_obj_pos = *x; - } - - // add the time spent between end and last portal to total time and return - (pos - last_obj_pos) / speed + total_time -} - #[cfg(test)] mod tests { use base64::URL_SAFE; @@ -850,4 +537,4 @@ mod tests { // password of 'Breakthrough' by Hinds1324 assert_eq!(base64::encode_config(&robtop_encode_level_password(0), URL_SAFE), "AwYDBgQCBg==") } -} +} \ No newline at end of file diff --git a/src/model/level/online_level.rs b/src/model/level/online_level.rs new file mode 100644 index 0000000..8f0c216 --- /dev/null +++ b/src/model/level/online_level.rs @@ -0,0 +1,415 @@ +use std::{ + borrow::Cow, +}; +use serde::{ + Deserialize, Serialize, +}; +use crate::{ + Base64Decoded, Thunk, + model::{ + GameVersion, + creator::Creator, + song::{MainSong, NewgroundsSong}, + level::{ + Featured, LevelLength, LevelRating, + local_level::LevelData, + }, + }, +}; + +pub type ListedLevel<'a> = Level<'a, Option>, Option>, Option>>; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct Level<'a, Data = Option>, Song = Option, User = u64>{ + /// The ID of the [`Level`]. + /// + /// ## GD Internals + /// This value is provided at index `1` + pub level_id: u64, + + /// The name of the [`Level`]. + /// + /// ## GD Internals + /// This value is provided at index `2` + pub name: Cow<'a, str>, + + /// The description of the [`Level`]. + /// + /// ## GD Internals + /// This value is provided at index `3` + #[serde(borrow = "'a")] + pub description: Option>>, + + /// The version of the [`Level`]. + /// + /// ## GD Internals + /// This value is provided at index `5` + pub version: u8, + + /// The [`User`] ID of the [`Creator`] who uploaded this [`Level`]. + /// + /// ## GD Internals + /// This value is provided at index `6` + pub creator: User, + + /// The amount of times this [`Level`] has been downloaded. + /// + /// ## GD Internals + /// This value is provided at index `10` + pub downloads: u64, + + /// The amount of people who have completed this [`Level`]. + /// This is removed in update 2.1 + /// + /// ## GD Internals + /// This value is provided at index `11` + pub set_completes: Option, + + /// The main song used by this [`Level`]. + /// If this level does not use a main song, then the value is none. + /// + /// ## GD Internals + /// This value is provided at index `12` + pub main_song: Option, + + /// The game version this [`Level`] was updated in. + /// + /// ## GD Internals + /// This value is provided at index `13` + pub game_version: GameVersion, + + /// The number of likes this [`Level`] has. + /// + /// ## GD Internals + /// This value is provided at index `14` + pub likes: i32, + + /// The length of this [`Level`] has. + /// + /// ## GD Internals + /// This value is provided at index `15` + pub length: LevelLength, + + /// The amount of stars this [`Level`] awards upon completion. + /// + /// ## GD Internals + /// This value is provided at index `18` + pub stars: u8, + + /// The feature score this [`Level`] has. + /// If the [`Level`] is not featured, then this value is set to 0. + /// + /// ## GD Internals + /// This value is provided at index `19` + pub feature_score: Featured, + + /// The ID this [`Level`] was copied from. + /// If the [`Level`] is not copied, this value is `None` + /// + /// ## GD Internals + /// This value is provided at index `30` + pub copy_of: Option, + + /// If this [`Level`] has two player mode enabled. + /// + /// ## GD Internals + /// This value is provided at index `31` + pub is_two_player: bool, + + /// The custom song used by this [`Level`]. + /// If this level does not use a custom song, then the value is 0. + /// + /// ## GD Internals + /// This value is provided at index `35` + pub custom_song: Song, + + /// The number of coins this [`Level`] has. + /// + /// ## GD Internals + /// This value is provided at index `37` + pub coin_amount: u8, + + /// If this [`Level`] has verified coins. + /// + /// ## GD Internals + /// This value is provided at index `38` + pub has_verified_coins: bool, + + /// The amount of starts requested for this [`Level`]. + /// + /// ## GD Internals + /// This value is provided at index `39` + pub stars_requested: Option, + + /// The number that this daily or weekly [`Level`] was. + /// + /// ## GD Internals + /// This value is provided at index `41` + pub daily_number: Option, + + /// If this [`Level`] is rated Epic. + /// + /// ## GD Internals + /// This value is provided at index `42` + pub is_epic: bool, + + /// The difficulty of this [`Level`] + /// + /// ## GD Internals: + /// This value is a construct from the value at the indices `9`, `17` and + /// `25`, whereas index 9 is an integer representation of either the + /// [`LevelRating`] or the [`DemonRating`] + /// struct, depending on the value of index 17. + /// + /// If index 25 is set to true, the level is an auto level and the value at + /// index 9 is some nonsense, in which case it is ignored. + pub difficulty: LevelRating, + + /// If this [`Level`] is in a Gauntlet. + /// + /// ## GD Internals + /// This value is provided at index `44` + pub in_gauntlet: Option, + + /// The number of objects this [`Level`] has. + /// This value will be `None` for levels uploaded before update 2.1 + /// + /// ## GD Internals + /// This value is provided at index `45` + pub object_count: Option, + + /// The encoded data of the [`Level`]. + /// This data is only returned when downloading a level + /// + /// ## GD Internals + /// This value is provided at index `4` + pub level_data: Data, +} + + +#[cfg(test)] +mod tests { + +} + +mod internal { + use crate::model::{ + song::MainSong, + level::{ + DemonRating, Featured, LevelRating, Password, + online_level::Level, + local_level::{LevelData, Objects}, + } + }; + use crate::serde::Internal; + + #[allow(non_upper_case_globals, unused_imports)] + const _level: () = { + use crate::{ + serde::{DeError, HasRobtopFormat, IndexedDeserializer, IndexedSerializer, PercentDecoded, SerError, Thunk, RefThunk, Base64Decoded}, + }; + use serde::{Deserialize, Serialize}; + use std::{borrow::{Cow, Borrow}, io::Write}; + #[derive(Serialize, Deserialize)] + struct InternalLevel<'src, 'bor> { + #[serde(rename = "1")] + index_1: u64, + #[serde(rename = "2")] + index_2: &'src str, + #[serde(rename = "3")] + index_3: Option>>, + #[serde(rename = "4")] + index_4: Option>, + #[serde(rename = "5")] + index_5: u8, + #[serde(rename = "6")] + index_6: u64, + #[serde(serialize_with = "crate::util::true_to_ten")] + #[serde(rename = "8")] + index_8: bool, + #[serde(rename = "9")] + index_9: i8, + #[serde(rename = "10")] + index_10: u64, + #[serde(rename = "11")] + index_11: Option, + #[serde(rename = "12")] + index_12: u8, + #[serde(rename = "13")] + index_13: u8, + #[serde(rename = "14")] + index_14: i32, + #[serde(rename = "15")] + index_15: u8, + #[serde(rename = "17")] + index_17: bool, + #[serde(rename = "18")] + index_18: u8, + #[serde(rename = "19")] + index_19: Featured, + #[serde(serialize_with = "crate::util::false_to_empty_string")] + #[serde(rename = "25")] + index_25: bool, + #[serde(rename = "27")] + index_27: Option>, + #[serde(rename = "28")] + index_28: Option<&'src str>, + #[serde(rename = "29")] + index_29: Option<&'src str>, + #[serde(rename = "30")] + index_30: Option, + #[serde(rename = "31")] + index_31: bool, + #[serde(rename = "35")] + index_35: Option, + #[serde(rename = "36")] + index_36: Option<&'src str>, + #[serde(rename = "37")] + index_37: u8, + #[serde(rename = "38")] + index_38: bool, + #[serde(rename = "39")] + index_39: Option, + #[serde(rename = "40")] + index_40: Option, + #[serde(rename = "41")] + index_41: Option, + #[serde(rename = "42")] + index_42: bool, + #[serde(rename = "43")] + index_43: u8, + #[serde(rename = "44")] + index_44: Option, + #[serde(rename = "45")] + index_45: Option, + #[serde(rename = "46")] + index_46: Option, + #[serde(rename = "47")] + index_47: Option, + } + + impl<'src> HasRobtopFormat<'src> for Level<'src> { + fn from_robtop_str(input: &'src str) -> Result { + let internal = InternalLevel::deserialize(&mut IndexedDeserializer::new(input, ":", true))?; + Ok(Self { + level_id: internal.index_1, + name: Cow::Borrowed(internal.index_2), + description: match internal.index_3 { + None => None, + Some(RefThunk::Unprocessed(unproc)) => Some(Thunk::Unprocessed(unproc)), + _ => unreachable!() + }, + level_data: match internal.index_4 { + None => None, + Some(_data) => { + Some(LevelData::from_robtop_str(input)?) + } + }, + version: internal.index_5, + creator: internal.index_6, + downloads: internal.index_10, + set_completes: match internal.index_11 { + None => None, + Some(set_complete) => Some(set_complete) + }, + main_song: match internal.index_35 { + // Main song + Some(0) => Some(MainSong::from(internal.index_12)), + Some(_custom_song) => { + None + } + None => unreachable!(), + }, + game_version: internal.index_13.into(), + likes: internal.index_14, + length: internal.index_15.into(), + stars: internal.index_18, + feature_score: internal.index_19, + copy_of: internal.index_30, + is_two_player: internal.index_31, + custom_song: internal.index_35, + coin_amount: internal.index_37, + has_verified_coins: internal.index_38, + stars_requested: internal.index_39, + daily_number: internal.index_41, + is_epic: internal.index_42, + difficulty: if !internal.index_8 { + // has_difficulty_rating + LevelRating::NotAvailable + } + else if internal.index_25 { + // is_auto + LevelRating::Auto + } + else if internal.index_17 { + // is_demon + LevelRating::Demon(internal.index_9.into()) + } + else { + // rating + internal.index_9.into() + }, + in_gauntlet: match internal.index_44 { + None => None, + Some(flag) => Some(flag), + }, + object_count: internal.index_45, + }) + } + fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { + let internal = InternalLevel { + index_1: self.level_id, + index_2: self.name.as_ref(), + index_3: self.description.as_ref().map(|t| t.as_ref_thunk()), + index_4: None, + index_5: self.version, + index_6: self.creator, + index_25: self.difficulty == LevelRating::Auto, + index_8: self.difficulty != LevelRating::NotAvailable, + index_9: self.difficulty.into(), + index_17: self.difficulty.is_demon(), + index_10: self.downloads, + index_11: self.set_completes, + index_12: self.main_song.map(|song| song.main_song_id).unwrap_or(0), + index_13: self.game_version.into(), + index_14: self.likes, + index_15: self.length.into(), + index_18: self.stars, + index_19: self.feature_score, + index_27: None, + index_28: None, + index_29: None, + index_30: self.copy_of, + index_31: self.is_two_player, + index_35: self.custom_song, + index_36: None, + index_37: self.coin_amount, + index_38: self.has_verified_coins, + index_39: self.stars_requested, + index_40: None, + index_41: self.daily_number, + index_42: self.is_epic, + index_43: match self.difficulty { + LevelRating::Demon(DemonRating::Easy) => 3, + LevelRating::Demon(DemonRating::Medium) => 4, + LevelRating::Demon(DemonRating::Hard) => 0, + LevelRating::Demon(DemonRating::Insane) => 5, + LevelRating::Demon(DemonRating::Extreme) => 6, + _ => 5, // this seems to be the default for non-demons + }, + index_44: self.in_gauntlet, + index_45: self.object_count, + index_46: None, + index_47: None + }; + + if let Some(level_data) = &self.level_data { + let bytes = Vec::new(); + level_data.write_robtop_data(bytes)?; + } + + internal.serialize(&mut IndexedSerializer::new(":", writer, true)) + } + } + }; +} \ No newline at end of file diff --git a/src/model/song.rs b/src/model/song.rs index d58d1e8..6ab52aa 100644 --- a/src/model/song.rs +++ b/src/model/song.rs @@ -8,7 +8,66 @@ use std::{ mod internal { use crate::model::song::NewgroundsSong; - include!(concat!(env!("OUT_DIR"), "/newgrounds_song.boilerplate")); + #[allow(non_upper_case_globals, unused_imports)] + const _newgrounds_song: () = { + use crate::{ + serde::{DeError, HasRobtopFormat, IndexedDeserializer, IndexedSerializer, PercentDecoded, SerError, Thunk, RefThunk, Base64Decoded}, + }; + use serde::{Deserialize, Serialize}; + use std::{borrow::{Cow, Borrow}, io::Write}; + #[derive(Serialize, Deserialize)] + struct InternalNewgroundsSong<'src, 'bor> { + #[serde(rename = "1")] + index_1: u64, + #[serde(borrow)] + #[serde(rename = "2")] + index_2: &'src str, + #[serde(rename = "3")] + index_3: u64, + #[serde(rename = "4")] + index_4: &'src str, + #[serde(rename = "5")] + index_5: f64, + #[serde(rename = "6")] + index_6: Option<&'src str>, + #[serde(rename = "7")] + index_7: Option<&'src str>, + #[serde(rename = "8")] + index_8: &'src str, + #[serde(rename = "10")] + index_10: RefThunk<'src, 'bor, PercentDecoded<'src>>, + } + impl<'src> HasRobtopFormat<'src> for NewgroundsSong<'src> { + fn from_robtop_str(input: &'src str) -> Result { + let internal = InternalNewgroundsSong::deserialize(&mut IndexedDeserializer::new(input, "~|~", true))?; + Ok(Self { + song_id: internal.index_1, + name: Cow::Borrowed(internal.index_2), + index_3: internal.index_3, + artist: Cow::Borrowed(internal.index_4), + filesize: internal.index_5, + index_6: internal.index_6.map(Cow::Borrowed), + index_7: internal.index_7.map(Cow::Borrowed), + index_8: Cow::Borrowed(internal.index_8), + link: Thunk::Unprocessed(match internal.index_10 {RefThunk::Unprocessed(unproc) => unproc, _ => unreachable!() }), + }) + } + fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { + let internal = InternalNewgroundsSong { + index_1: self.song_id, + index_2: self.name.as_ref(), + index_3: self.index_3, + index_4: self.artist.as_ref(), + index_5: self.filesize, + index_6: self.index_6.as_deref(), + index_7: self.index_7.as_deref(), + index_8: self.index_8.as_ref(), + index_10: self.link.as_ref_thunk(), + }; + internal.serialize(&mut IndexedSerializer::new("~|~", writer, true)) + } + } + }; } /// Struct modelling a [`NewgroundsSong`] diff --git a/src/request/comment.rs b/src/request/comment.rs index f1db5de..2d22886 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -428,10 +428,11 @@ mod tests { #[test] fn serialize_upload_comment() { - let request = UploadCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) + let mut binding = UploadCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) .comment("This is a test comment") - .percent(56) - .generate_chk(); + .percent(56); + + let request = binding.generate_chk(); assert_eq!( super::super::to_string(request), diff --git a/src/request/level.rs b/src/request/level.rs index 59c4503..9d9473a 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -1,13 +1,15 @@ +use async_trait::async_trait; +use reqwest::Error; +use serde::{Deserialize, Serialize, Serializer}; use crate::{ + request::{BaseRequest, GD_21, REQUEST_BASE_URL, Executable}, model::{ level::{DemonRating, LevelLength, LevelRating}, song::MainSong, }, - request::{BaseRequest, GD_21, REQUEST_BASE_URL}, }; -use serde::{Deserialize, Serialize, Serializer}; -use async_trait::async_trait; -use crate::request::Executable; +use crate::model::level::online_level::{Level, ListedLevel}; +use crate::response::{parse_download_gj_level_response, parse_get_gj_levels_response, ResponseError}; pub const DOWNLOAD_LEVEL_ENDPOINT: &str = "downloadGJLevel22.php"; pub const SEARCH_LEVEL_ENDPOINT: &str = "getGJLevels21.php"; @@ -89,6 +91,14 @@ impl<'a> LevelRequest<'a> { extra: false, } } + + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await + } + + pub async fn into_robtop(self, response_body: &str) -> Result { + parse_download_gj_level_response(response_body) + } } impl Executable for LevelRequest<'_> { @@ -589,6 +599,15 @@ impl<'a> LevelsRequest<'a> { self.search_filters = filters; self } + + pub async fn get_response_body(&self) -> Result { + super::execute(&self, &self.to_url()).await + } + + pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { + parse_get_gj_levels_response(response_body) + } + } #[async_trait] @@ -616,7 +635,7 @@ impl Serialize for DemonFilter { DemonRating::Extreme => 5, }; - serializer.serialize_i32(numerical_value) + serializer.serialize_i8(numerical_value) } } @@ -638,7 +657,7 @@ impl Serialize for LengthFilter { LevelLength::ExtraLong => 4, }; - serializer.serialize_i32(numerical_value) + serializer.serialize_u8(numerical_value) } } @@ -664,7 +683,7 @@ impl Serialize for RatingFilter { * -2 means "search for any demon, regardless of difficulty" */ }; - serializer.serialize_i32(numerical_value) + serializer.serialize_i8(numerical_value) } } diff --git a/src/response.rs b/src/response.rs index e3eb5ce..85d801b 100644 --- a/src/response.rs +++ b/src/response.rs @@ -8,7 +8,9 @@ use crate::{ profile::ProfileComment, }, creator::Creator, - level::{Level, ListedLevel}, + level::{ + online_level::{Level, ListedLevel} + }, song::NewgroundsSong, user::{profile::Profile, searched::SearchedUser}, }, @@ -17,6 +19,7 @@ use crate::{ use serde::__private::Formatter; use std::fmt::Display; +use crate::model::level::local_level::LevelData; // Since NoneError is not stabilized, we cannot do `impl From for ResponseError<'_>`, so // this is the next best thing @@ -85,7 +88,7 @@ pub fn parse_get_gj_levels_response(response: &str) -> Result, levels .split('|') .map(|fragment| { - let level: Level<()> = Level::from_robtop_str(fragment)?; + let level: Level<'_, Option>> = Level::from_robtop_str(fragment)?; // Note: Cloning is cheap because none of the Thunks is evaluated, so we only have references lying // around. let creator = creators.iter().find(|creator| creator.user_id == level.creator).map(Clone::clone); @@ -98,27 +101,28 @@ pub fn parse_get_gj_levels_response(response: &str) -> Result, level_id: level.level_id, name: level.name, description: level.description, + level_data: level.level_data, version: level.version, creator, difficulty: level.difficulty, downloads: level.downloads, + set_completes: level.set_completes, main_song: level.main_song, - gd_version: level.gd_version, + game_version: level.game_version, likes: level.likes, length: level.length, stars: level.stars, - featured: level.featured, + feature_score: level.feature_score, copy_of: level.copy_of, - two_player: level.two_player, + is_two_player: level.is_two_player, custom_song: song, coin_amount: level.coin_amount, - coins_verified: level.coins_verified, + has_verified_coins: level.has_verified_coins, stars_requested: level.stars_requested, + daily_number: level.daily_number, is_epic: level.is_epic, - object_amount: level.object_amount, - index_46: level.index_46, - index_47: level.index_47, - level_data: level.level_data, + object_count: level.object_count, + in_gauntlet: level.in_gauntlet }) }) .collect::>() diff --git a/tests/comment.rs b/tests/comment.rs index 629cc14..c5eb047 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -81,7 +81,6 @@ async fn delete_comment() { .await .unwrap(); - let comment_history_request = CommentHistoryRequest::new(3713125) .sort_mode(SortMode::Recent) .limit(1) diff --git a/tests/level.rs b/tests/level.rs new file mode 100644 index 0000000..b5ffffb --- /dev/null +++ b/tests/level.rs @@ -0,0 +1,90 @@ +use std::borrow::Cow; + +use dash_rs::{ + model::{ + level::{DemonRating, Featured, LevelLength, LevelRating}, + song::MainSong, + GameVersion, + }, + Base64Decoded, Thunk, +}; +use dash_rs::model::level::DemonRating::Extreme; +use dash_rs::model::level::LevelRating::Demon; +use dash_rs::model::level::Password::PasswordCopy; +use dash_rs::request::level::{LevelRequest, LevelsRequest}; + +#[tokio::test] +async fn download_gj_level_test() { + let request = LevelRequest::new(76298358); + + let response_body = request.get_response_body() + .await + .unwrap(); + + let level = request.into_robtop(&response_body) + .await + .unwrap(); + + println!("{:?}", &level); + + assert_eq!(level.name, "Edooox Collab"); + assert_eq!(level.level_data.as_ref().unwrap().password, PasswordCopy(7678)); + assert_eq!(level.level_data.as_ref().unwrap().editor_time.unwrap(), 432); + +} + +#[tokio::test] +async fn get_gj_levels_test() { + let request = LevelsRequest::default() + .search("Spectrum Rave") + .page(0); + + let response_body = request.get_response_body() + .await + .unwrap(); + + + let levels = request.into_robtop(&response_body) + .await + .unwrap(); + + let level = levels.get(0).unwrap(); + + println!("{:?}", levels.get(0)); + + assert_eq!(level.level_id, 72308725); + assert_eq!(level.name, "Spectrum Rave"); + assert_eq!(level.creator.as_ref().unwrap().name, "Ryder"); + assert_eq!(level.main_song, None); + assert_eq!(level.length, LevelLength::Long); + assert_eq!(level.custom_song.as_ref().unwrap().song_id, 785444); + assert_eq!(level.has_verified_coins, false); + assert_eq!(level.is_epic, true); + assert_eq!(level.difficulty, Demon(Extreme)); + assert_eq!(level.length, LevelLength::Long); + assert!(level.level_data.as_ref().is_none()); +} + +#[tokio::test] +async fn get_gj_levels_should_use_main_song_test() { + let request = LevelsRequest::default() + .search("The Nightmare") + .page(0); + + let response_body = request.get_response_body() + .await + .unwrap(); + + + let levels = request.into_robtop(&response_body) + .await + .unwrap(); + + let level = levels.get(0).unwrap(); + + println!("{:?}", level); + + assert_eq!(level.level_id, 13519); + assert_eq!(level.main_song.as_ref().unwrap().name, "Polargeist"); + assert!(level.custom_song.as_ref().is_none()); +} \ No newline at end of file diff --git a/tests/level_meta.rs b/tests/level_meta.rs deleted file mode 100644 index df9ee52..0000000 --- a/tests/level_meta.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::borrow::Cow; - -use dash_rs::{ - model::{ - level::{DemonRating, Featured, Level, LevelLength, LevelRating}, - song::MainSong, - GameVersion, - }, - Base64Decoded, Thunk, -}; - -#[macro_use] -mod helper; - -// A 1.9 level -const DARK_REALM_DATA: &str = - "1:11774780:2:Dark \ - Realm:5:2:6:2073761:8:10:9:30:10:90786:12:0:13:20:14:10974:17:1:43:0:25::18:10:19:11994:42:0:45:0:3:\ - TXkgYmVzdCBsZXZlbCB5ZXQuIFZpZGVvIG9uIG15IFlvdVR1YmUuIEhhdmUgZnVuIGluIHRoaXMgZmFzdC1wYWNlZCBERU1PTiA-OikgdjIgRml4ZWQgc29tZSB0aGluZ3M=:\ - 15:3:30:0:31:0:37:3:38:1:39:10:46:1:47:2:35:444085"; - -// A 1.3 (?) level -const DEMON_WORLD_DATA: &str = "1:72540:2:demon \ - world:5:7:6:37573:8:10:9:30:10:272374:12:9:13:7:14:-3628:17:1:43:0:25::18:10:19:0:42:0:45:0:3:\ - aGFwcHkgbmV3IHllYXIhIQ==:15:3:30:0:31:0:37:0:38:0:39:0:46:1:47:2:35:0"; - -// A 2.1 level -const FANTASY_DATA: &str = "1:63355989:2:Fantasy:5:2:6:15557115:8:10:9:40:10:9352:12:0:13:21:14:912:17::43:5:25::18:7:19:24978:42:0:45:\ - 37866:3:Q29sbGFiIHdpdGggQnJpbmRpa3osIHRoYW5rIHlvdSBmb3IgdGhpcyBsZXZlbCB1d3UsIEVOSk9ZISEg:15:3:30:63309629:31:\ - 0:37:2:38:1:39:7:46:1:47:2:35:771517"; - -// A 2.0 level in two-player mode (also an extreme demon) -const DUELO_MAETSTRO_DATA: &str = "1:23298409:2:Duelo Maestro:5:8:6:1295392:8:10:9:40:10:3302831:12:0:13:21:14:268067:17:1:43:5:25::18:10:19:0:42:0:45:45133:3:RWwgZHVlbG8gZGUgdHVzIGRvcyBtYW5vcyBvIGRlIGRvcyB2ZXJkYWRlcm9zIG1hZXN0cm9zLiBBIHZlY2VzIHB1ZWRlcyBtb3JpciBpbmV4cGxpY2FibGVtZW50ZSBlbiBsYSBwcmltZXJhIGJvbGEsIHBvcmZhIHJlaW5pY2llbiBlbCBuaXZlbC4=:15:4:30:0:31:1:37:2:38:1:39:10:46:1:47:2:35:645631"; - -const DARK_REALM: Level<()> = Level { - level_id: 11774780, - name: Cow::Borrowed("Dark Realm"), - description: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed( - "My best level yet. Video on my YouTube. Have fun in this fast-paced DEMON >:) v2 Fixed some things", - )))), - version: 2, - creator: 2073761, - difficulty: LevelRating::Demon(DemonRating::Hard), - downloads: 90786, - main_song: None, - gd_version: GameVersion::Version { minor: 0, major: 2 }, - likes: 10974, - length: LevelLength::Long, - stars: 10, - featured: Featured::Featured(11994), - copy_of: None, - two_player: false, - custom_song: Some(444085), - coin_amount: 3, - coins_verified: true, - stars_requested: Some(10), - is_epic: false, - object_amount: None, - index_46: Some(Cow::Borrowed("1")), - index_47: Some(Cow::Borrowed("2")), - level_data: (), -}; - -const DEMON_WORLD: Level<()> = Level { - level_id: 72540, - name: Cow::Borrowed("demon world"), - description: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed("happy new year!!")))), - version: 7, - creator: 37573, - difficulty: LevelRating::Demon(DemonRating::Hard), - downloads: 272374, - main_song: Some(MainSong { - main_song_id: 9, - name: "xStep", - artist: "DJVI", - }), - gd_version: GameVersion::Version { minor: 7, major: 0 }, - likes: -3628, - length: LevelLength::Long, - stars: 10, - featured: Featured::NotFeatured, - copy_of: None, - two_player: false, - custom_song: None, - coin_amount: 0, - coins_verified: false, - stars_requested: None, - is_epic: false, - object_amount: None, - index_46: Some(Cow::Borrowed("1")), - index_47: Some(Cow::Borrowed("2")), - level_data: (), -}; - -const FANTASY: Level<()> = Level { - level_id: 63355989, - name: Cow::Borrowed("Fantasy"), - description: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed( - "Collab with Brindikz, thank you for this level uwu, ENJOY!! ", - )))), - version: 2, - creator: 15557115, - difficulty: LevelRating::Harder, - downloads: 9352, - main_song: None, - gd_version: GameVersion::Version { minor: 1, major: 2 }, - likes: 912, - length: LevelLength::Long, - stars: 7, - featured: Featured::Featured(24978), - copy_of: Some(63309629), - two_player: false, - custom_song: Some(771517), - coin_amount: 2, - coins_verified: true, - stars_requested: Some(7), - is_epic: false, - object_amount: Some(37866), - index_46: Some(Cow::Borrowed("1")), - index_47: Some(Cow::Borrowed("2")), - level_data: (), -}; - -const DUELO_MAESTRO: Level<()> = Level { - level_id: 23298409, - name: Cow::Borrowed("Duelo Maestro"), - description: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed( - "El duelo de tus dos manos o de dos verdaderos maestros. A veces puedes morir inexplicablemente en la primera bola, porfa \ - reinicien el nivel.", - )))), - version: 8, - creator: 1295392, - difficulty: LevelRating::Demon(DemonRating::Insane), - downloads: 3302831, - main_song: None, - gd_version: GameVersion::Version { minor: 1, major: 2 }, - likes: 268067, - length: LevelLength::ExtraLong, - stars: 10, - featured: Featured::NotFeatured, - copy_of: None, - two_player: true, - custom_song: Some(645631), - coin_amount: 2, - coins_verified: true, - stars_requested: Some(10), - is_epic: false, - object_amount: Some(45133), - index_46: Some(Cow::Borrowed("1")), - index_47: Some(Cow::Borrowed("2")), - level_data: (), -}; - -impl helper::ThunkProcessor for Level<'_, (), S, U> { - fn process_all_thunks(&mut self) { - if let Some(ref mut hunk) = self.description { - assert!(hunk.process().is_ok()) - } - } -} - -save_load_roundtrip!(save_load_roundtrip_dark_realm, Level<()>, DARK_REALM); -load_save_roundtrip!(load_save_roundtrip_dark_realm, Level<()>, DARK_REALM_DATA, DARK_REALM, ":", true); - -save_load_roundtrip!(save_load_roundtrip_demon_world, Level<()>, DEMON_WORLD); -load_save_roundtrip!(load_save_roundtrip_demon_world, Level<()>, DEMON_WORLD_DATA, DEMON_WORLD, ":", true); - -save_load_roundtrip!(save_load_roundtrip_fantasy, Level<()>, FANTASY); -load_save_roundtrip!(load_save_roundtrip_fantasy, Level<()>, FANTASY_DATA, FANTASY, ":", true); - -save_load_roundtrip!(save_load_roundtrip_duelo_maestro, Level<()>, DUELO_MAESTRO); -load_save_roundtrip!( - load_save_roundtrip_duelo_maestro, - Level<()>, - DUELO_MAETSTRO_DATA, - DUELO_MAESTRO, - ":", - true -); From a101d2fb45d0d389a0fdc2f3af05423324a92dc3 Mon Sep 17 00:00:00 2001 From: Ryder <36455274+zRyder@users.noreply.github.com> Date: Sun, 9 Jul 2023 20:31:16 -0700 Subject: [PATCH 14/29] Level (#9) * Added more verbose unit test cases * Integration testing * level parsing changes * level parsing testing * Refactoring and what not --------- Co-authored-by: Ryder Co-authored-by: Ryder --- src/request/account.rs | 37 +++++++++++++------- src/request/comment.rs | 79 ++++++++++++++++-------------------------- src/request/level.rs | 37 +++++--------------- src/request/mod.rs | 42 +--------------------- src/request/user.rs | 46 ++++++++++++------------ tests/account.rs | 10 +++++- tests/comment.rs | 75 ++++++++++++++++++++++++++++----------- tests/level.rs | 44 ++++++++++++++++------- tests/profile.rs | 40 +++++++++++++-------- 9 files changed, 206 insertions(+), 204 deletions(-) diff --git a/src/request/account.rs b/src/request/account.rs index f4c24e6..14c6c39 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -1,15 +1,18 @@ use std::fmt; use std::fmt::Formatter; +use reqwest::{Error, Response}; use serde::{Serialize}; use crate::{ util, request::{REQUEST_BASE_URL} }; -use crate::request::{AuthenticatedUser, Executable}; +use crate::request::{AuthenticatedUser}; pub const ACCOUNT_LOGIN_ENPOINT: &str = "accounts/loginGJAccount.php"; pub const XOR_KEY: &str = "37526"; +pub const CONTENT_TYPE: &str = "Content-Type"; +pub const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; #[derive(Debug, Clone, Serialize, Hash)] pub struct LoginRequest<'a> { @@ -57,6 +60,26 @@ impl<'a> LoginRequest<'a> { } } + fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, ACCOUNT_LOGIN_ENPOINT) + } + + fn to_string(&self) -> String { + super::to_string(&self) + } + + async fn execute(&self) -> Result { + let reqwest_client = reqwest::Client::new(); + println!("{:?}", self.to_string()); + println!("{:?}", self.to_url()); + reqwest_client + .post(self.to_url()) + .body(self.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + } + pub async fn to_authenticated_user(&self) -> Result { match self.execute().await { Ok(login_result) => { @@ -78,12 +101,6 @@ impl<'a> LoginRequest<'a> { } } -impl<'a> Executable for LoginRequest<'a> { - fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, ACCOUNT_LOGIN_ENPOINT) - } -} - #[derive(Debug, Clone)] pub struct AuthenticationError(String); @@ -95,12 +112,6 @@ impl fmt::Display for AuthenticationError { } } -impl ToString for LoginRequest<'_> { - fn to_string(&self) -> String { - super::to_string(self) - } -} - #[cfg(test)] mod tests { use crate::request::account::LoginRequest; diff --git a/src/request/comment.rs b/src/request/comment.rs index 2d22886..c87cf74 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -2,12 +2,7 @@ use std::borrow::Cow; use crate::{ - model::comment::{ - profile::ProfileComment, - level::LevelComment - }, request::{BaseRequest, GD_21, REQUEST_BASE_URL, AuthenticatedUser}, - response::{ResponseError, parse_get_gj_comments_response, parse_get_gj_acccount_comments_response}, util }; use serde::Serialize; @@ -95,7 +90,7 @@ impl<'a> LevelCommentsRequest<'a> { const_setter!(page: u32); - fn to_url(&self) -> String { + pub fn to_url(&self) -> String { format!("{}{}", REQUEST_BASE_URL, LEVEL_COMMENTS_ENDPOINT) } @@ -124,12 +119,8 @@ impl<'a> LevelCommentsRequest<'a> { self } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await - } - - pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { - parse_get_gj_comments_response(response_body) + pub fn to_string(&self) -> String { + super::to_string(&self) } } @@ -182,12 +173,8 @@ impl<'a> ProfileCommentsRequest<'a> { } } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await - } - - pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { - parse_get_gj_acccount_comments_response(response_body) + pub fn to_string(&self) -> String { + super::to_string(&self) } } @@ -245,25 +232,21 @@ impl<'a> CommentHistoryRequest<'a> { } } - fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, COMMENT_HISTORY_ENDPOINT) - } - pub const fn new(player: u64) -> Self { Self::with_base(GD_21, player) } - pub const fn sort_mode(mut self, sort_mode: SortMode) -> Self { - self.sort_mode = sort_mode; - self + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, COMMENT_HISTORY_ENDPOINT) } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await + pub fn to_string(&self) -> String { + super::to_string(&self) } - pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { - parse_get_gj_comments_response(response_body) + pub const fn sort_mode(mut self, sort_mode: SortMode) -> Self { + self.sort_mode = sort_mode; + self } } @@ -286,16 +269,13 @@ pub struct UploadCommentRequest<'a> { /// The percent completed to display on the comment, this should be a number between 0 or 100 if present pub percent: u8, - - /// The CHK for /uploadGJComment21.php - pub chk: Cow<'a, str> } impl<'a> UploadCommentRequest<'a> { const_setter!(level_id: u64); const_setter!(percent: u8); - fn to_url(&self) -> String { + pub fn to_url(&self) -> String { format!("{}{}", REQUEST_BASE_URL, UPLOAD_COMMENT_ENDPOINT) } @@ -310,7 +290,6 @@ impl<'a> UploadCommentRequest<'a> { comment: Cow::Borrowed(""), level_id, percent: 0, - chk: Cow::Borrowed("") } } @@ -319,18 +298,16 @@ impl<'a> UploadCommentRequest<'a> { self } - fn generate_chk(&mut self) -> &mut Self { - self.chk = format!("{}{}{}{}{}{}", self.authenticated_user.user_name, self.comment, self.level_id, self.percent, 0, COMMENT_CHK_SALT) + fn generate_chk(&self) -> Cow<'a, str> { + let chk: Cow<'a, str> = format!("{}{}{}{}{}{}", self.authenticated_user.user_name, self.comment, self.level_id, self.percent, 0, COMMENT_CHK_SALT) .into(); - let xor_chk = util::xor(util::sha_encrypt(&self.chk).as_bytes().to_vec(), COMMENT_XOR_CHK_KEY.as_bytes()); - self.chk = base64::encode_config(xor_chk.as_slice(), base64::URL_SAFE).into(); - self + let xor_chk = util::xor(util::sha_encrypt(&chk).as_bytes().to_vec(), COMMENT_XOR_CHK_KEY.as_bytes()); + base64::encode_config(xor_chk.as_slice(), base64::URL_SAFE).into() } - pub async fn get_response_body(&mut self) -> Result { - self.generate_chk(); - super::execute(&self, &self.to_url()).await + pub fn to_string(&self) -> String { + format!("{}&chk={}", super::to_string(&self), self.generate_chk()) } } @@ -359,7 +336,7 @@ impl<'a> DeleteCommentRequest<'a> { const_setter!(comment_id: u64); const_setter!(level_id: u64); - fn to_url(&self) -> String { + pub fn to_url(&self) -> String { format!("{}{}", REQUEST_BASE_URL, DELETE_COMMENT_ENDPOINT) } @@ -376,6 +353,10 @@ impl<'a> DeleteCommentRequest<'a> { } } + pub fn to_string(&self) -> String { + super::to_string(&self) + } + pub async fn get_response_body(&self) -> Result { super::execute(&self, &self.to_url()).await } @@ -398,7 +379,7 @@ mod tests { let request = LevelCommentsRequest::new(1234).most_liked().page(2).limit(15); assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=2&mode=1&levelID=1234&count=15" ); } @@ -408,7 +389,7 @@ mod tests { let request = ProfileCommentsRequest::new(1710032).page(2); assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=2&accountID=1710032" ); } @@ -421,7 +402,7 @@ mod tests { .limit(2); assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2" ); } @@ -432,10 +413,8 @@ mod tests { .comment("This is a test comment") .percent(56); - let request = binding.generate_chk(); - assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&comment=VGhpcyBpcyBhIHRlc3QgY29tbWVudA==&levelID=85179632&percent=56&chk=UQsGAAEACgQBVQBaAwoGVwtSDQIEWAYOUFEAVQoIBVtWDwEHDQEJVA==" ); } @@ -445,7 +424,7 @@ mod tests { let request = DeleteCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632, 7000000); assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&commentID=7000000&levelID=85179632" ); } diff --git a/src/request/level.rs b/src/request/level.rs index 9d9473a..1d45f9b 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -1,15 +1,11 @@ -use async_trait::async_trait; -use reqwest::Error; use serde::{Deserialize, Serialize, Serializer}; use crate::{ - request::{BaseRequest, GD_21, REQUEST_BASE_URL, Executable}, + request::{BaseRequest, GD_21, REQUEST_BASE_URL}, model::{ level::{DemonRating, LevelLength, LevelRating}, song::MainSong, }, }; -use crate::model::level::online_level::{Level, ListedLevel}; -use crate::response::{parse_download_gj_level_response, parse_get_gj_levels_response, ResponseError}; pub const DOWNLOAD_LEVEL_ENDPOINT: &str = "downloadGJLevel22.php"; pub const SEARCH_LEVEL_ENDPOINT: &str = "getGJLevels21.php"; @@ -92,18 +88,12 @@ impl<'a> LevelRequest<'a> { } } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, DOWNLOAD_LEVEL_ENDPOINT) } - pub async fn into_robtop(self, response_body: &str) -> Result { - parse_download_gj_level_response(response_body) - } -} - -impl Executable for LevelRequest<'_> { - fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, DOWNLOAD_LEVEL_ENDPOINT) + pub fn to_string(&self) -> String { + super::to_string(&self) } } @@ -600,23 +590,16 @@ impl<'a> LevelsRequest<'a> { self } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, SEARCH_LEVEL_ENDPOINT) } - pub async fn into_robtop(self, response_body: &str) -> Result, ResponseError> { - parse_get_gj_levels_response(response_body) + pub fn to_string(&self) -> String { + super::to_string(&self) } } -#[async_trait] -impl Executable for LevelsRequest<'_>{ - fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, SEARCH_LEVEL_ENDPOINT) - } -} - /// Newtype struct for [`DemonRating`] to implement robtop's serialization for requests on #[derive(Debug, Clone, Copy, Hash)] struct DemonFilter(DemonRating); @@ -693,7 +676,6 @@ mod tests { model::level::LevelLength, request::level::{CompletionFilter, LevelRequestType, LevelsRequest, SearchFilters}, }; - use crate::request::Executable; use crate::request::level::LevelRequest; #[tokio::test] @@ -708,7 +690,6 @@ mod tests { 18018958, 21373201, 22057275, 22488444, 22008823, 23144971, 17382902, 87600, 22031889, 22390740, 22243264, 21923305, ]), )); - println!("{:?}", request.execute().await.unwrap().text().await.unwrap()); assert_eq!( request.to_string(), diff --git a/src/request/mod.rs b/src/request/mod.rs index d28a818..1d7c14c 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -13,9 +13,7 @@ use crate::{ serde::RequestSerializer, }; use serde::{Deserialize, Serialize}; -use async_trait::async_trait; -use reqwest::{Error, Response}; -use crate::response::ResponseError; +use reqwest::Error; macro_rules! const_setter { ($name: ident, $field: ident, $t: ty) => { @@ -165,42 +163,4 @@ async fn execute(request: S, url: &str) -> Result { .await? .text() .await -} - -#[async_trait] -trait Executable { - fn to_url(&self) -> String; - - fn to_string(&self) -> String - where - Self: Serialize - { - to_string(&self) - } - - async fn execute(&self) -> Result - where - Self: Serialize - { - let reqwest_client = reqwest::Client::new(); - println!("{:?}", self.to_string()); - println!("{:?}", self.to_url()); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header(CONTENT_TYPE, URL_FORM_ENCODED) - .send() - .await - } - - async fn get_response_body(&self, response: Response) -> Result { - response.text().await - } -} - -#[async_trait] -pub trait ToRobtop { - type RobtopModel; - - async fn into_robtop(&self, response_body: &str) -> Result, ResponseError>; } \ No newline at end of file diff --git a/src/request/user.rs b/src/request/user.rs index 45a4334..defb409 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -1,14 +1,10 @@ //! Module containing request definitions for retrieving users -use reqwest::Error; use crate::{ request::{BaseRequest, GD_21, REQUEST_BASE_URL}, }; use serde::Serialize; -use crate::model::user::profile::Profile; -use crate::model::user::searched::SearchedUser; use crate::request::AuthenticatedUser; -use crate::response::{parse_get_gj_user_info_response, parse_get_gj_users_response, ResponseError}; pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20.php"; pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20.php"; @@ -35,10 +31,6 @@ pub struct UserRequest<'a> { impl<'a> UserRequest<'a> { - fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, GET_USER_ENDPOINT) - } - pub const fn new(user_id: u64) -> UserRequest<'a> { UserRequest { base: GD_21, @@ -55,13 +47,14 @@ impl<'a> UserRequest<'a> { } } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, GET_USER_ENDPOINT) } - pub async fn into_robtop(self, response_body: &str) -> Result { - parse_get_gj_user_info_response(response_body) + pub fn to_string(&self) -> String { + super::to_string(&self) } + } #[derive(Debug, Clone, Serialize, Copy, PartialEq, Eq)] @@ -96,10 +89,6 @@ pub struct UserSearchRequest<'a> { impl<'a> UserSearchRequest<'a> { - fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, SEARCH_USER_ENDPOINT) - } - pub const fn new(search_string: &'a str) -> Self { UserSearchRequest { base: GD_21, @@ -109,26 +98,35 @@ impl<'a> UserSearchRequest<'a> { } } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, SEARCH_USER_ENDPOINT) } - pub async fn into_robtop(self, response_body: &str) -> Result { - parse_get_gj_users_response(response_body) + pub fn to_string(&self) -> String { + super::to_string(&self) } + } #[cfg(test)] mod tests { + use std::borrow::Cow; + use crate::request::AuthenticatedUser; use crate::request::user::{UserRequest, UserSearchRequest}; + const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser { + user_name: "TestUser", + account_id: 472634, + password_hash: Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") + }; + #[test] fn serialize_user_request() { - let request = UserRequest::new(57903); + let request = UserRequest::with_authenticated_user(TEST_AUTHENTICATED_USER, 57903); assert_eq!( - super::super::to_string(request), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&targetAccountID=57903" + request.to_string(), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&targetAccountID=57903" ); } @@ -137,7 +135,7 @@ mod tests { let request = UserSearchRequest::new("Ryder"); assert_eq!( - super::super::to_string(request), + request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&str=Ryder" ); } diff --git a/tests/account.rs b/tests/account.rs index c02a848..20819df 100644 --- a/tests/account.rs +++ b/tests/account.rs @@ -12,5 +12,13 @@ async fn login_gj_account_test() { .password(&password); let result = request.to_authenticated_user().await.unwrap(); - println!("{:?}", result); + + assert_eq!( + result.user_name, + "Ryder" + ); + assert_eq!( + result.account_id, + "57903" + ); } \ No newline at end of file diff --git a/tests/comment.rs b/tests/comment.rs index c5eb047..ce4e7aa 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -4,40 +4,57 @@ use dash_rs::{ comment::{UploadCommentRequest, DeleteCommentRequest, CommentHistoryRequest, LevelCommentsRequest, ProfileCommentsRequest, SortMode}, }, }; +use dash_rs::response::{parse_get_gj_acccount_comments_response, parse_get_gj_comments_response}; + +const CONTENT_TYPE: &str = "Content-Type"; +const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; #[tokio::test] async fn get_level_comments() { + let client = reqwest::Client::new(); + let request = LevelCommentsRequest::new(76298358) .page(0) .limit(5) .most_recent(); - let request_body = request.get_response_body() + let raw_response = client.post(request.to_url()) + .body(request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() .await - .unwrap(); - - let level_comments = request.into_robtop(&request_body) + .unwrap() + .text() .await .unwrap(); + let level_comments = parse_get_gj_comments_response(&raw_response).unwrap(); + assert_eq!(level_comments.len(), 5); } #[tokio::test] async fn get_profile_comments() { + let client = reqwest::Client::new(); + let request = ProfileCommentsRequest::new(57903) .page(0); - let response_body = request.get_response_body() + let raw_response = client.post(request.to_url()) + .body(request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); - let profile_comments = request.into_robtop(&response_body) - .await + let profile_comments = parse_get_gj_acccount_comments_response(&raw_response) .unwrap(); - assert_eq!(profile_comments.len(), 2); + assert_eq!(profile_comments.len(), 3); } #[tokio::test] @@ -46,24 +63,31 @@ async fn upload_comment() { let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + let client = reqwest::Client::new(); - let request = LoginRequest::default() + let login_request = LoginRequest::default() .user_name(&user_name) .password(&password); - let login_response = request.to_authenticated_user() + let login_response = login_request.to_authenticated_user() .await .unwrap(); - let mut comment_upload_request = UploadCommentRequest::new(login_response, 76298358) + let comment_upload_request = UploadCommentRequest::new(login_response, 76298358) .comment("More tests still ignore me") .percent(69); - let comment_upload_response = comment_upload_request - .get_response_body() + + let response = client.post(comment_upload_request.to_url()) + .body(comment_upload_request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); - assert!(!comment_upload_response.eq("-1")) + assert!(!response.eq("-1")) } #[tokio::test] @@ -72,6 +96,7 @@ async fn delete_comment() { let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + let client = reqwest::Client::new(); let request = LoginRequest::default() .user_name(&user_name) @@ -86,19 +111,29 @@ async fn delete_comment() { .limit(1) .page(0); - let comment_history_response = comment_history_request.get_response_body() + let comment_history_response = client.post(comment_history_request.to_url()) + .body(comment_history_request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() .await - .unwrap(); - - let comment = comment_history_request.into_robtop(&comment_history_response) + .unwrap() + .text() .await .unwrap(); + let comment_history_response = parse_get_gj_comments_response(&comment_history_response) + .unwrap(); - let comment_id = comment.get(0).unwrap().comment_id; + let comment_id = comment_history_response.get(0).unwrap().comment_id; let comment_delete_request = DeleteCommentRequest::new(login_response, 76298358, comment_id); - let comment_delete_response = comment_delete_request.get_response_body() + let comment_delete_response = client.post(comment_delete_request.to_url()) + .body(comment_delete_request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); diff --git a/tests/level.rs b/tests/level.rs index b5ffffb..ebf80af 100644 --- a/tests/level.rs +++ b/tests/level.rs @@ -12,17 +12,27 @@ use dash_rs::model::level::DemonRating::Extreme; use dash_rs::model::level::LevelRating::Demon; use dash_rs::model::level::Password::PasswordCopy; use dash_rs::request::level::{LevelRequest, LevelsRequest}; +use dash_rs::response::{parse_download_gj_level_response, parse_get_gj_levels_response}; + +const CONTENT_TYPE: &str = "Content-Type"; +const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; #[tokio::test] async fn download_gj_level_test() { + let client = reqwest::Client::new(); let request = LevelRequest::new(76298358); - let response_body = request.get_response_body() + let raw_response = client.post(request.to_url()) + .body(request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); - let level = request.into_robtop(&response_body) - .await + let level = parse_download_gj_level_response(&raw_response) .unwrap(); println!("{:?}", &level); @@ -35,17 +45,22 @@ async fn download_gj_level_test() { #[tokio::test] async fn get_gj_levels_test() { + let client = reqwest::Client::new(); let request = LevelsRequest::default() .search("Spectrum Rave") .page(0); - let response_body = request.get_response_body() + let raw_response = client.post(request.to_url()) + .body(request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); - - let levels = request.into_robtop(&response_body) - .await + let levels = parse_get_gj_levels_response(&raw_response) .unwrap(); let level = levels.get(0).unwrap(); @@ -67,23 +82,26 @@ async fn get_gj_levels_test() { #[tokio::test] async fn get_gj_levels_should_use_main_song_test() { + let client = reqwest::Client::new(); let request = LevelsRequest::default() .search("The Nightmare") .page(0); - let response_body = request.get_response_body() + let raw_response = client.post(request.to_url()) + .body(request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); - - let levels = request.into_robtop(&response_body) - .await + let levels = parse_get_gj_levels_response(&raw_response) .unwrap(); let level = levels.get(0).unwrap(); - println!("{:?}", level); - assert_eq!(level.level_id, 13519); assert_eq!(level.main_song.as_ref().unwrap().name, "Polargeist"); assert!(level.custom_song.as_ref().is_none()); diff --git a/tests/profile.rs b/tests/profile.rs index 00db2a9..7042acd 100644 --- a/tests/profile.rs +++ b/tests/profile.rs @@ -57,6 +57,10 @@ use dash_rs::model::user::ModLevel; use dash_rs::request::account::LoginRequest; use dash_rs::request::user::UserRequest; +use dash_rs::response::parse_get_gj_user_info_response; + +const CONTENT_TYPE: &str = "Content-Type"; +const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; #[tokio::test] async fn test_get_user_profile_with_auth() { @@ -64,25 +68,30 @@ async fn test_get_user_profile_with_auth() { let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + let client = reqwest::Client::new(); - let request = LoginRequest::default() + let login_request = LoginRequest::default() .user_name(&user_name) .password(&password); - let login_response = request.to_authenticated_user() + let login_response = login_request.to_authenticated_user() .await .unwrap(); let request = UserRequest::with_authenticated_user(login_response, 57903); - let response_body = request.get_response_body() + let raw_response = client.post(request.to_url()) + .body(request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); - println!("{}", response_body); - - let user = request.into_robtop(&response_body) - .await.unwrap(); + let user = parse_get_gj_user_info_response(&raw_response) + .unwrap(); println!("{:?}", user); @@ -99,18 +108,21 @@ async fn test_get_user_profile_with_auth() { #[tokio::test] async fn test_get_user_profile_without_auth() { + let client = reqwest::Client::new(); let request = UserRequest::new(57903); - let response_body = request.get_response_body() + let raw_response = client.post(request.to_url()) + .body(request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() .await .unwrap(); - println!("{}", response_body); - - let user = request.into_robtop(&response_body) - .await.unwrap(); - - println!("{:?}", user); + let user = parse_get_gj_user_info_response(&raw_response) + .unwrap(); assert_eq!( user.name, From dbca9e323feb45f0adc2ad3cb22a1cd42d87279b Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 3 Aug 2023 17:54:10 -0700 Subject: [PATCH 15/29] rand downgrade --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ec56017..462a050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ percent-encoding = "2.1.0" itoa = "0.4.4" dtoa = "0.4.4" flate2 = {version = "1.0.14", features = ["zlib"], default-features=false} -rand = "0.8.5" +rand = "0.8.4" reqwest = "0.11" async-trait = "0.1.57" tokio = { version = "1.21.2", features = ["macros", "rt"]} From 25418d672e14dfb5bfeaa908829f55033703cbef Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 3 Aug 2023 17:57:45 -0700 Subject: [PATCH 16/29] version fixes --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 462a050..17948be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ flate2 = {version = "1.0.14", features = ["zlib"], default-features=false} rand = "0.8.4" reqwest = "0.11" async-trait = "0.1.57" -tokio = { version = "1.21.2", features = ["macros", "rt"]} +tokio = { version = "^1.21.2", features = ["macros", "rt"]} [dev-dependencies] # benchmark From 1873c69e0462c50ea2d1e17788c75c775defd9b4 Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 3 Aug 2023 18:34:58 -0700 Subject: [PATCH 17/29] version fixes --- Cargo.toml | 4 ++-- src/request/comment.rs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17948be..ec56017 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ percent-encoding = "2.1.0" itoa = "0.4.4" dtoa = "0.4.4" flate2 = {version = "1.0.14", features = ["zlib"], default-features=false} -rand = "0.8.4" +rand = "0.8.5" reqwest = "0.11" async-trait = "0.1.57" -tokio = { version = "^1.21.2", features = ["macros", "rt"]} +tokio = { version = "1.21.2", features = ["macros", "rt"]} [dev-dependencies] # benchmark diff --git a/src/request/comment.rs b/src/request/comment.rs index c87cf74..72a041f 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -196,6 +196,12 @@ pub struct CommentHistoryRequest<'a> { /// This field is called `page` in the boomlings API pub page: u32, + /// The number of comments to retrieve. The limit of this is 100, even if more than 100 is passed in + /// + /// ## GD Internals: + /// This field is called `count` in the boomlings API + pub count: u32, + /// What to sort by comments by /// /// ## GD Internals: From b36f299cb5ae60e6d1632b8851882f5d892fd85f Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 3 Aug 2023 18:36:49 -0700 Subject: [PATCH 18/29] version fixes --- src/request/comment.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/request/comment.rs b/src/request/comment.rs index 72a041f..7e3af03 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -226,12 +226,14 @@ pub struct CommentHistoryRequest<'a> { impl<'a> CommentHistoryRequest<'a> { const_setter!(limit: u32); const_setter!(page: u32); + const_setter!(count: u32); pub const fn with_base(base: BaseRequest<'a>, player: u64) -> Self { CommentHistoryRequest { player_id: player, base, page: 0, + count: 10, total: 0, sort_mode: SortMode::Recent, limit: 20, From db2dac3e347afa2fda4f3eacf857537c81715576 Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 3 Aug 2023 18:39:08 -0700 Subject: [PATCH 19/29] version fixes --- src/request/comment.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/request/comment.rs b/src/request/comment.rs index 7e3af03..baedb01 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -215,16 +215,9 @@ pub struct CommentHistoryRequest<'a> { /// This field is called `userID` in the boomlings API. #[serde(rename = "userID")] pub player_id: u64, - - /// The amount of comments to retrieve. Note that while in-game this can only be set to 20 or 40 however, a max of 100 comments can be returned - /// ## GD Internals: - /// This field is called `count` in the boomlings API - #[serde(rename = "count")] - pub limit: u32, } impl<'a> CommentHistoryRequest<'a> { - const_setter!(limit: u32); const_setter!(page: u32); const_setter!(count: u32); @@ -236,7 +229,6 @@ impl<'a> CommentHistoryRequest<'a> { count: 10, total: 0, sort_mode: SortMode::Recent, - limit: 20, } } From 7096fed98421dc09fdd9eb848d5d4b4129198f79 Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 3 Aug 2023 22:22:21 -0700 Subject: [PATCH 20/29] fixes --- src/model/comment/level.rs | 10 ++++++++++ src/request/comment.rs | 10 +++++----- src/request/user.rs | 19 ------------------- tests/comment.rs | 27 ++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/model/comment/level.rs b/src/model/comment/level.rs index 31288b1..144e4db 100644 --- a/src/model/comment/level.rs +++ b/src/model/comment/level.rs @@ -14,6 +14,12 @@ pub struct LevelComment<'a> { /// object pub user: Option>, + /// The id of the the level that this [`CommentEntry`] was posted to + /// + /// ## GD Internals + /// This value is provided at index `1` + pub level_id: Option, + /// The actual content of the [`LevelComment`] made. /// /// ## GD Internals @@ -181,6 +187,8 @@ mod internal { use std::{borrow::{Cow, Borrow}, io::Write}; #[derive(Serialize, Deserialize)] struct InternalLevelComment<'src, 'bor> { + #[serde(rename = "1")] + index_1: Option, #[serde(rename = "2")] index_2: Option>>, #[serde(rename = "3")] @@ -210,6 +218,7 @@ mod internal { Some (RefThunk::Unprocessed(unproc)) => Some(Thunk::Unprocessed(unproc)), _ => unreachable!()}, + level_id: internal.index_1, user_id: internal.index_3, likes: internal.index_4, comment_id: internal.index_6, @@ -230,6 +239,7 @@ mod internal { fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { let internal = InternalLevelComment { + index_1: self.level_id, index_2: self.content.as_ref().map(|t| t.as_ref_thunk()), index_3: self.user_id, index_4: self.likes, diff --git a/src/request/comment.rs b/src/request/comment.rs index baedb01..fc3bc34 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -196,8 +196,7 @@ pub struct CommentHistoryRequest<'a> { /// This field is called `page` in the boomlings API pub page: u32, - /// The number of comments to retrieve. The limit of this is 100, even if more than 100 is passed in - /// + /// The amount of comments to retrieve. Note that while in-game this can only be set to 20 or 40 however, a max of 100 comments can be returned /// ## GD Internals: /// This field is called `count` in the boomlings API pub count: u32, @@ -279,6 +278,7 @@ impl<'a> UploadCommentRequest<'a> { format!("{}{}", REQUEST_BASE_URL, UPLOAD_COMMENT_ENDPOINT) } + pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { Self::with_base(GD_21, authenticated_user, level_id) } @@ -399,17 +399,17 @@ mod tests { let request = CommentHistoryRequest::new(159782398) .sort_mode(SortMode::Recent) .page(0) - .limit(2); + .count(2); assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&mode=0&userID=159782398&count=2" + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&count=2&mode=0&userID=159782398" ); } #[test] fn serialize_upload_comment() { - let mut binding = UploadCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) + let request = UploadCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) .comment("This is a test comment") .percent(56); diff --git a/src/request/user.rs b/src/request/user.rs index a8b43b8..defb409 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -140,22 +140,3 @@ mod tests { ); } } - -#[cfg(test)] -mod tests { - use crate::request::user::{UserRequest, UserSearchRequest}; - - #[test] - fn serialize_user_request() { - let request = UserRequest::new(57903); - - assert_eq!(request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&targetAccountID=57903"); - } - - #[test] - fn test_user_searc_request() { - let request = UserSearchRequest::new("Ryder"); - - assert_eq!(request.to_string(), "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&str=Ryder"); - } -} diff --git a/tests/comment.rs b/tests/comment.rs index ce4e7aa..9d3fc14 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -27,6 +27,7 @@ async fn get_level_comments() { .text() .await .unwrap(); + println!("{}", &raw_response); let level_comments = parse_get_gj_comments_response(&raw_response).unwrap(); @@ -51,11 +52,34 @@ async fn get_profile_comments() { .await .unwrap(); + let profile_comments = parse_get_gj_acccount_comments_response(&raw_response) .unwrap(); assert_eq!(profile_comments.len(), 3); } +#[tokio::test] +async fn get_comment_history() { + let client = reqwest::Client::new(); + + let comment_history_request = CommentHistoryRequest::new(3713125) + .sort_mode(SortMode::Recent) + .count(1) + .page(0); + + let comment_history_response = client.post(comment_history_request.to_url()) + .body(comment_history_request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + let comment_history_response = parse_get_gj_comments_response(&comment_history_response) + .unwrap(); +} #[tokio::test] async fn upload_comment() { @@ -108,7 +132,7 @@ async fn delete_comment() { let comment_history_request = CommentHistoryRequest::new(3713125) .sort_mode(SortMode::Recent) - .limit(1) + .count(1) .page(0); let comment_history_response = client.post(comment_history_request.to_url()) @@ -120,6 +144,7 @@ async fn delete_comment() { .text() .await .unwrap(); + let comment_history_response = parse_get_gj_comments_response(&comment_history_response) .unwrap(); From c3b7ef5fdc92abed2821d5d1a43f1828770492aa Mon Sep 17 00:00:00 2001 From: Ryder Date: Sat, 11 Nov 2023 15:38:20 -0800 Subject: [PATCH 21/29] Cleanup and removed unneeded tests --- src/model/level/online_level.rs | 34 +++++- src/request/account.rs | 2 - tests/account.rs | 2 +- tests/comment.rs | 3 +- .../897837_time_pressure_gjdownload_response | 2 +- tests/level.rs | 7 +- tests/misc.rs | 109 ------------------ tests/response.rs | 2 - 8 files changed, 34 insertions(+), 127 deletions(-) delete mode 100644 tests/misc.rs diff --git a/src/model/level/online_level.rs b/src/model/level/online_level.rs index 8f0c216..b9ba195 100644 --- a/src/model/level/online_level.rs +++ b/src/model/level/online_level.rs @@ -256,10 +256,12 @@ mod internal { index_28: Option<&'src str>, #[serde(rename = "29")] index_29: Option<&'src str>, + // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] #[serde(rename = "30")] index_30: Option, #[serde(rename = "31")] index_31: bool, + // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] #[serde(rename = "35")] index_35: Option, #[serde(rename = "36")] @@ -272,6 +274,7 @@ mod internal { index_39: Option, #[serde(rename = "40")] index_40: Option, + // #[serde(deserialize_with = "crate::util::negative_or_zero_u32_to_none")] #[serde(rename = "41")] index_41: Option, #[serde(rename = "42")] @@ -282,8 +285,10 @@ mod internal { index_44: Option, #[serde(rename = "45")] index_45: Option, + // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] #[serde(rename = "46")] index_46: Option, + // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] #[serde(rename = "47")] index_47: Option, } @@ -379,15 +384,30 @@ mod internal { index_27: None, index_28: None, index_29: None, - index_30: self.copy_of, + index_30: match self.copy_of { + Some(0) => None, + Some(id) => Some(id), + None => None + }, index_31: self.is_two_player, - index_35: self.custom_song, + index_35: match self.custom_song { + Some(0) => None, + Some(id) => Some(id), + None => None + }, index_36: None, index_37: self.coin_amount, index_38: self.has_verified_coins, - index_39: self.stars_requested, + index_39: match self.stars_requested { + Some(0) => None, + Some(id) => Some(id), + None => None + }, index_40: None, - index_41: self.daily_number, + index_41: match self.daily_number { + Some(daily_number) => Some(daily_number), + None => None + }, index_42: self.is_epic, index_43: match self.difficulty { LevelRating::Demon(DemonRating::Easy) => 3, @@ -398,7 +418,11 @@ mod internal { _ => 5, // this seems to be the default for non-demons }, index_44: self.in_gauntlet, - index_45: self.object_count, + index_45: match self.object_count { + Some(0) => None, + Some(id) => Some(id), + None => None + }, index_46: None, index_47: None }; diff --git a/src/request/account.rs b/src/request/account.rs index 14c6c39..171ac2e 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -70,8 +70,6 @@ impl<'a> LoginRequest<'a> { async fn execute(&self) -> Result { let reqwest_client = reqwest::Client::new(); - println!("{:?}", self.to_string()); - println!("{:?}", self.to_url()); reqwest_client .post(self.to_url()) .body(self.to_string()) diff --git a/tests/account.rs b/tests/account.rs index 20819df..7e57625 100644 --- a/tests/account.rs +++ b/tests/account.rs @@ -19,6 +19,6 @@ async fn login_gj_account_test() { ); assert_eq!( result.account_id, - "57903" + 57903 ); } \ No newline at end of file diff --git a/tests/comment.rs b/tests/comment.rs index 9d3fc14..fa5929e 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -62,7 +62,7 @@ async fn get_profile_comments() { async fn get_comment_history() { let client = reqwest::Client::new(); - let comment_history_request = CommentHistoryRequest::new(3713125) + let comment_history_request = CommentHistoryRequest::new(17577805) .sort_mode(SortMode::Recent) .count(1) .page(0); @@ -79,6 +79,7 @@ async fn get_comment_history() { let comment_history_response = parse_get_gj_comments_response(&comment_history_response) .unwrap(); + println!("{:?}", &comment_history_response) } #[tokio::test] diff --git a/tests/data/897837_time_pressure_gjdownload_response b/tests/data/897837_time_pressure_gjdownload_response index 8309cb8..160174f 100644 --- a/tests/data/897837_time_pressure_gjdownload_response +++ b/tests/data/897837_time_pressure_gjdownload_response @@ -1 +1 @@ -1:897837:2:time pressure:3:cGxlYXNlIHJhdGUgYW5kIGxpa2UgIDgtOSBzdGFycyBtYWJ5ZT8=:4:eJycvVuW5SqsLNqhumuYN4z9tdtwmnH6_38zjQQhBc7peX4qawbiLaQAY_n__p_wL1___u__if9CLD9_079Yfv_mf79o-Reu-PO3Ctr-pfTzp88_41_KP3_C9a_df8O_kH-lQvwXU__9T7pLCfnn92-GUP7F39r-91fy50_8Tf7fW-h_Z4Whzj9t_unzz7j_xOsW_GnTz7-33P_-iP1P-Jd-iv2Xfgr_afzPz5jz7-_8-zv__K7h92fTn6H0lRzSf-WG2hIRKMa4pGK8hfrlkJK9SIqrJT9w_TeuG22A_pY91u_fOn9SY__306iY_437_z8jev33k7H9C0t8duf6Ff8Zwn5P17in5ZZOKp12K2fht_DPLD4Lh1303y25R_JXEAY2J_uz7nFuS9qM88xhoQrQLGe4coaZnN-Rzz8D8wNdc3Lqruznt_s5Z3P_Lpf5GXJw5UmG4acfkeqBOdHS0msrxEL-5T2hOYVv5j_X8MX8T-nP8x_GnrNwN3iM1eCxSkvhGwUp0WhEKXYmZ7KZ_ilioe41YiJ5TZf8HksfAJgzIWXATCzkdyZ-1uf_Nxfo2M2WUeirVVXrr8H2Y_7GFtZlcmLu63coq6KYxxYrW65t4Na7SmYHkZIcEGepX-lTbd_o05T-rE9xNratkZzjsmyvdC_sdZnc72x-3-t0_5xtyW-1cY5uu9xwt-qBBMBYQBg4c1vwZ9DPeHnA-xlP4QHPhFdoz49C7ITBCSWsHGn2aSLjAQke-emHR3pxVW4_1r6zY-0rO9Ze2rGpd9Lp2SFApPmIFEIiIIOQa2m0AMkD1QPDLoJm3WazbrNV-3N5v3hbMqmuFo8kQi6PtNuKgH7NTAa5tu2f7Rz7d3K_q_ttfUV77StCb3Z13j4bV-ttHAAolxO4u9DLw-Lo8ZDw2-Ten3L0vb4HwWXDd0sE7rT4ZsW4jCaCS01kOsnAAp09M0gHBDpJw4FAtwu2l68WbO_fLNgp_XbByiDAQputPyCFkO4RXPgyUIhs946ZKM9esd3ymF7sz0VH5mKT3LBEBUmEXB6BJYrFTF7SkZcE23ABdsPnJJTXHvJuxAjerc22LHzpkiA_9ZWZ_9bKiYpWzhKzL1HyZ5d_os2hs4xGrbotxBi_vYvXMhkITIJhkBQ2AktyNgUX16wQF9cIbi0NT93Gpm51D2VNN0u8FqActDlkKp6MH6iiNA6RRkhw-juyBxwPG8Fo8LA75LF3yLdhn7_H0jKoTwCo7x6McF2niVQYteEecEnAEQ9XOmlIuOoZVsLi9EwToufekvBr-1WDL5BHpiZtZ8onCZXU0zQ1Fuhn8romUhYaPBre2UlRBvF0VgraSvkDpKWV9z5IkN-NkDmk2Lia_x_ktbvoF0wq6KH230DE1bT_BvK8S4rikrigrfc6jPt3cr-r--3ol5YIxl2hxNBFENh3U9RcaNK8AUDyQPWAZV0yS-83RSEscmQWVFhk6neDbJP6IWlqbkCPLFB_gkqhjIWkkCqJlIWILKlU5rI29_nJ9w2bEfG35yg6RHXbP2iZak8EKKFZwSIwoV_QY9TzgBzSQIWhiBCxLJ2cSBkD5QuUDZZasKRJssDvTZtwJnkieR5zgZH2HAhbogi0f6pwDI8qPJOMJsZAKhwDaWJE8iPV5OdqMlfTGMpcc3uAbGPyE5Q544RChZHZmitwPiq0SLP2irxJiLvK3wQYkgStkdUxtSoi8RQoM9QY8pxIW7QVNFoSJALwex9WyWw2ZymxdtaETGPPQ5_B-EdHs7APikAfbq-Srl-zFFrzG9iflKNZT-kMVw_fTUqLI5ETSMCrkG8lIGKwRVbc7JHn-KWLZi8lhuhwRqHCUCfImMvE_CIRv0jELxLxi7T5hRl2tAGJeFoinpbgKG3ShcQ8IzHPSMwzEvEMFLJQ2u5wKtLrs4CfXn3lPlN6eRowF4eoBFrCdMHMImRsXMJ9hEBMuVPijImLT9wI5uoJuHqFNmw7khzHTI5jpv2Aa5pI0UWwncLJV8I2GskzxuQZY_KMMTnGOKf-C8aYlW38tM_YnKzsz5qK3B08F09mHpPp-EYyW4joSCY6kjcJMK3GVZf9AZnkmkb5PlEIuaW1DhmZhwwWmr5sQmKuDHTr0_3A2Oab6pSZzmams7l7pcuObWXHtvJ-Zja1YAqAt8lEmzLRpuJPfeZgFyRDIqn0xu_Hy8OBkBSSqQyDwPOx6VZK3rpoWHKhgxPJfYCMvyjIwrB_Jp8_YZFsaFJKoHkrTMGKOWsaUDg9dNI6PAEpSH8mXZtFNETaRqaZKXj8lKDJDZEsQp7gSVsgoUIRW_GKY1XFHS4JAIpYiPYUOl-q5GYrudlKbraOM_Jras2IVmIDldhAJTZQgQ2MXcx7pyp9eOtUp_hrp1rZN1b2oJX9bGVvXOn5oEImY32AbI3sjSv77Mqut1ZuBLvxym680hNQhabWzfUxM47gtY7Icz1zalGQuWXaLHlAGm6PCmrVIHtWmSVXZsmVWXKlZ5iqqIMhPJmQ3u7LKgm0oW0vNrOOdetDF-f-7QhQdQSobgI0YN2ASYDVp0glZK--BCP2Ba9pxBAaMYQWncFAQYa7g-dgSyG40W2Foc4QMZ8GzGdqcsNTHoHKAyQqgBkt1B8gWxYf5zU-9Gv0nFOhwk09QB2W5ix-0EhnHr6tpy07PW2OMzXHmZrnTDMDUOnmSVQj2tnDUTN6PsPtDBM36sSNevMj0Im2dKItvRGiB0CUrXI-pK49kLL0QMrS-Xit8_Fa53OvzgdmPXPxjRvBB2adT9p647LyLmset0zkfnwydaqnoTolQF9Ahc5tpeqOD3XgQ8HNmiKZkD1rsiIGOatxHVVppDNcz7DnR1g2at8g-jWIfg2gX1OLBvGsQTxrEM8ag5CLFHQkhipD6PbmrI-LFGEwARpMk8ZFqjcSQ5Wh8QDZRvDTxJGeoMJQ57I613iACmcse0lIrwOcQEpCfUpIaxEJMNyqGn4RDccohmMUwzGK4RnFgFOR4LRKkUEIaOetwfE6ev94Hb1_vLz3x0Jy8fkN4t2ZVo2IP_TQCjlX5WyV88Eq0IwDIaIMmpEh1EnJaCEiKQoVzli4-APUOWPn4vvSXO0iKaj26pBQlqJOsyui2_7HK1nV1dnev4v77UiGZBgAOJJhpnmqSziSjBiOJCOGI8mQQlALgycZkhX1MniSobUi4kmG5qqcrXI-1MtAJEMzGohYgEBG4wLRByn-BBXOWLj4A9Q5Y-fiQS_Dvjk0dSo4khGDs486dft3dr8dycBZU8STDJy12bDIpl7UIh65RoxHrhHjkWvEeOQaUjYqYfRcQ0o0iOca2kZEPNfQliHiuYaWU7mgyiVVLgq1ORJlEMhoYCT6oRAqUiQ3r1BhKBJkG0EkRVvPkC2LWISUdYIKZywsBe9TSVNhJUS_EqJlCqor-3d1vx1TEAEwv3FsIDhFUgQUaapfOhOFdCYKiRhBAkYwtSeR_0_k_xN7-4Tefo5rYt-b2Pcm2q5LWYeMRisS04TEfjyBh55TmbaVu09DY7qfjsjpafk94Lz-uG0WE5GfhMTACFWGBmeMDE3iC13fmpScY0_esU8AFAmmVJE9penaYyBnWH-NwXWvj5R_xX_WxO_7lvey2aWOoOgsOl9jDW8IPyUXc9aczdG0SM-GHM6at_RcqBm97X1uKJCMs4EqQ4mhiyB4mCFI9semMRM9yURPMtGTHEhzcmYIHPx8sG56ZKFra0523jo7b52b-z1bM6pYDcxFaLOozN5or5aSaOosGmxgbh4ICogy3RV81tP5VDUW8EIKbYf9BKDruh-8oowiIHSfqKOMAIl0swxSsTJIEcsgdS3mJD1tyBafvHIKfp2VthBVKURVClGVQlSlXKSzJTFEd2a0D6jZ2HkL3aNWYPTnoFXs-v7t_G8Z7vd1-u21XHrh0XpEh0WTjMN2Lp-t35T-bP1kdRR3KUNahoA7j8DJkwVRwdsyIuqO0NRuIzTVrJazmlWiFJUoRSWvWumuqdbZKZ_4d2gq6kZ1HrQ6D1q7-x1P8yuVe7RbVIa5ksuFLivS_VQ08GYK7c3yE9BotkBGkebnD2QEMJcb05Yxc9xwX2ygylBiCE1fe_CljXxpI1_ayJc29qWNfWljX4rttxBYnNa8VjXnXZvzrs1513b0ru3oXdvRu7bwjqgm6fw3vGtKv7Y8zV2mwDlTZM9ZQo1767t7cooOgGg1IFOHUWTqWE-kdj2RcvZEKtxxt5qgut8rRRk3BJ28aCcv2smLdvKinb1oZy_a2Yt29qLYIQsBP-zOB3bnQ7vzod350H70mf3oM_vRZ_Zx0vOOL3h_1PNeN-f8rOdT-rWed-9he_IAuNzgJlYaiC8of95fDvKWA4-Uw0b2TAzn0YbzaCOexnnmejvOA17L-jzOA14P-nMfF9oUz1M5ty_AhIZ4h_If8PBQjrknaBK6u18mg-MVdjh3rzNEfGYQnxlMXgaQF9EemH0Z9vJuZz5jDl3WKaVrBwi5r-eJgBi_e9uu0EVQIyRzNkKkhwaaJzLYgl8rqi9-9Bu-j2WyXmLCSZW04NPaTKtLcxhvZ3hO9wHfrxPgeDotkFEltD2jeU26GTdBPLuRTAYhdqOt6JRPoBnzKb3bMEtcJjgG-rjGVTp9XuNpNgVVrwJUC0ldBDVCMmcjRLURqvttYAHjk0LIs2FetySBrJImeOvzg4eTtRKcrFUKZUz5g9LPNFoQEtmrrhYfcFoMgp9hXgyze2QTUzg5fB1Vj54cfgrjiFoiotPrysquFFhTwVMwow-CeAomxRiEKJhVIyzJQkDBZCnNhZc-ev2pmtG_0oCwsNCp17HQmokFVxZCF0GNkMzZytaFmPKszuugJPCqmQn5KQMtgnjzI7vKIIFXU2wSNu20bGYiLanZglZXXw44LaeJ5wfx_SZ7mOj92kXe9jGGvaf6bHtlMl7aXpH-aHvngomnEw3VOY_2I2oZp9bvcsGSjJ7GGt0SxJMkbWinbBbqfrXJUH9cbbOx6ejV09Grp6NXT45n-diPyXv9RF4_kddP5PUTe_3EXj-R18_xG6-f6zdeX6Rfal4-uowd0VG6kMmAZzbFmWx6JpueyaZnNuCZDXgGAy5tdE4oOyeUh_t9dIP56Aazc4PzyEOF0_BQ-UX-BGYmjSG6ZQwiQglaFn7M5b5lYVICptwHgpqShwtBshP-R45iTI2IFGzC8MWJQsaXvvNWyIL37z8qe4Fgfp-VXaQ_U9ypRYWMXiGjV8joFTZ6hY1e6V5DXdjLVJxNLt39PnqBcvQCxW90ExSBM2YSygP-owZeY1ZJIFseyii-DE3oDxn6U4ZyVjhRoc8KN6eikjGvZMwrGfPKxryyMa_NT3N1vqY6X1Ob-330bvXo3arzbmJ1pIN5WxlExKYYaNqHCf1MiJ6OAGqMz4Q6IfEaW0VAyCAHIbGZCJVteUxLEwwHq44ME6vOHKlDhvZnBrNU5gxTaMdEsR0TBXeUXEZVWvKq4kI-JhfzMbmgj1qoU4rmPJlpk6nOOTfpuXSQjcRM4DGUZrCRWCVt1WzsI5t3iShDIqK9jd1mIwfV1BMS1EDrG3hF03pyilADAmZpiDLA6MlgO77R_Nm5zBIRIBNRdW4rTVhAA-2XC-eGEMIFGiQTEgjRN4URKgyZhskeFXZugMuh4W9ibLAh6_V8uCsJ2QZflo1ir3JSDG9Nhgv7My88YRfzfePpNpRXQNG0psXFcEwuiGPqzin3o1PuR6fcj6fPiQJNJoo0mTpzDBPVcszyNzeqv-8xll83eB15lMimv2Vl-jSSn9uUQ5ocNplpHZeeyUpUGD-9UyCf0vtMH__wDHiOogtKmFxUwjScPx1HfzqO_nQ4fzrHaoS9HfvMOUX6M-cc0NoPcyYWbuBO8Q5266DsoWmmEAkB8oXfYOXzSdgap2D73soLPblHL7uoedlFzcsual6-hvt9cmdSCqGnjZmU6OYvX-GLPYNKf54_-UQAGOx7SiySHSIfEoBrPAEyxZ5Nf1xwtOyCo-XQ3e-TNZJchHprdI0N3zay9Z0bgfsEYa9Whe9IkHEQLmx-D9mKGbjHzELZQ3PUEJHvDmgngumaC9mVXciuHJv7fTIRkotQT7nb3RA507rSHkmJwjXZh0AN2MfVAfpxTdsKTiMo6XmyaEq-nZ0kT9IjSINqG1TbCbmzyTRJazNMdLyfXRoiKG2S8Xpq0w7Pj7VFqh9F8hGI2MIot5Z3a8auzvRl7jw2p80mNJ-BCkORILwwlVfoO4ISQxAofXXJKquLA5VdHKjs4kDl5KxmOlrNdLSa6Wg1kz_OmjMgBd8qrVAECGtPoOQSEa0NvWczlSYtYpxJYyQt-CwXLBdslOwFDRQI6oQURNIZ6diV6ts19oAV39oKK9X1r5oMoqZSOi65WXJHpDfVerSqqbS1NnFp6lyeGjGdjjbaTDIPpx8WBOoRiDSUEb4I077xwSL9jkPpivnAocTRagQ3dbSyXBPusgxUGIoEGQNhg_IhlBi6yh6j9J5ruUBe2QXyytkxg-yvEE1NlthdVi-z9_g5N_H4qIL5JrpxGHoumb2Jyf6erRQhW8jg4EklWvKmXgKaXcmTDI0I5wu6qcpJPnpSknGDO0mJhbKHJilBRFRMxq_uuTYlGSgSZFQpd1Kl3EmVsvlMAECNkMzZwEOpllgP5WJ1ZRerKxdHp8qRTpUjnSr-quw0Y1LEjwbYA4NpXSRTSwBlgKa9k8BqMPkme7ooe0L6Jk3IkF3KzZRgmpUT9GPKhj_7EdJWTKk1OkUuN_f5LccpssQ8q8Gua4H7lk_Q9rdmWKRfmuEC7379sZWVgWnaQMM5NDyeHkHL98Ygspto6g4HJx279hPkT7bTBZ7KLvBUdoGncnWMqx4ZVz0yrnpkXNUf_s3eS8Go11KqgSqpuoQlm6ouj_ywSKPrEjXtoiJV_Ts0cdpg0dqpW5KSl07j4pDG5aeWyOowncMFI_lDosUi-QPnx_WjYczc-qk3i4J1iGtII7C5NSRwd_2Udl5WfxVOR7XWQIHDwcnC4opWRLXtiiyUPTRdESL6ob79aEIWzg6aNlXRRbfKLrpVbo5ItOMRQzt6_XY-YphF_GrrYTI0_pebwHZ28O1mJk9FTYLSbOKnbmsjZ4W-YQjMd8Oo1urOQfw4iqro4DxsomcyVjfO298mT1FBsvtNhIZL2zrWgB9MFTNIdki5HCBUR0PUAVKO_NrEcDNQJMgwIBMEz0CJoYugRkjmbIgQb2pMrhpTsMZErTGda0z6GlNDjIo3YCZmJEqDNI_ki2QCySRCKiFjPSfMLjZZdrHJJN0v_udcorgSwSzAYViXR8YPa6Lvkx5F8IxtGtUuB2YewRUiVefghSJlA3czcxEQtzHdUdpUgg_jOh3G9X0YZ5BICNBT6TkeJQrER4na6_hwntfxWNI0PA9fo4mhjNn35Vecb9IKx_enznWIbmyQ5hHU767PJjxSCRlUMqyKnh8QbE94QJpHTAsztTBTCzO1MDwgnIvqOrQnUZv3-ym532_k_97mebMhEOnybkMwH9HOsj88j1TZ8mnzgEr0Yy01gMWF8O00o0fG8MjUSBc0LrugcXkHjRObo6Hshj1JG3TCOOAUci6hQeecY1fhzxNHAmNoDeFIJoOsbykdzxNnyUgFRrHniVpZBTbiKpubg1NvwRbOygmINB5gLnf8PR2IgVVpi7s9Z1JZPRx2o7Zur2QygCPSoSk2rJcTi9Twhk5hvGEbx-3fcNs_IUnjfAgp0QaRaqyAmwRVhgZBhmpgVEYLTapRYbYDIZmQ5hExR4gkQiohYOhEizIhzSNo-kaCuhCphHBdYPZFqQkxJV9U8kUlX9TCi3px7Z7eulJ2CMCpeQIkAO4NTVpaXeuNQmgCRDIhjRDZLBpofjfkZ2o2VKGVdn9YMDAhANvFlPlW20sXo9LvXEyZrxbOsufNzxD6k5NR6fJJWnsCz5tnZ10AveIC6BUMoDd2hvl6cd856q4Egx7eN2rKfHPMIPPzTQXKHUPKnZHitWZ8Cyf_mpqyI53NO08FQ9XNvCskHOSdCWpDKEGfcEBCh4Thgtl3KK3YtHtnWVaMuF2gJNSntun6oSboUqMEXe9UR6KiJOGiQe3YXPj4RzEh7RDCD4lId05QZqghlB6gaWVmuDTJaKHxAElZBsqcMXHxCXQn6cbbz0_SbbM9PZtTkQokAozh8Jz2pBUl12rOCmbnJ2_FcLMNmGOXwNRZqDI0EIoMUWS4wpHhCkaGM2VZKYqwVzicXuFAeQUD5SlEofmKCXWHUmbmBWoEGS1K-F0mbISF-pNUZqgRZGvsnLFzxv6Q0ahyKgxFgKYOt705-uxdRPqzd7lryxVs-u1rc0__zNGTIKN5JESGCkOdoBh-oXm_WaVwNedwXDOSuD7JdFjRWQPbrrut0KeYG0jqjRq3kHM4Fi-JGYr3izkHWlocma9wZL6CkflECXIgfc30jUzJaKFAapcDKWcOpMKZPvakxTNki88PkK2xPUBG93NmCE4Pkgxq-oLIifRLIjcV5e1SE-kXRO7WwhLD5roCzeUH-yqFcNUIVAiaC0mhWXxkqDDUCTKLUqQg8o82YvoXgS632gT2ARK0gDM8jktWGjWXbIfBUuiCFuBq4piBhWMGFowZKJpWLlpNJTFUGRoMXbR0SmKoMsQMqDB1KkywCtOwcj1AZhmWxFBliMlaYWZWKkOJIdjNxpxB6n90B-MCL5YdeFH2KxJQ8t6VDtCjfdulYBTKO_SW1mIW20Tcu0RaH3zTElU7b2NUwt75fTZGIv3SGJWxDy4_GyOR_myMCvQkpP_uJV4aDqrBBmCyZKvSQ7uSa3HwXGocgrFUpomVeVxlHleZJlbmcZV5XGUeV5l71fgA2bKAhVZE9m93OlH96YREbQT1nVn2ixBS5gDgfot4v-OltZLyUgTNJPPwxRm7Sr9U1dq-OQAR6Req2nY_rVpqfE6DdcREJ9twS17mgvbltZ-1l_cqlTcTlfcElfcElfcEFfYEYVYYwiKms4GTe9b7jkHcFy225EYml55U_X56XpoPcltcyMniQk6W1pyyatTKrawtO2VtcVhlbevESYZdI4V6ZeXwnQlbhRY4-P5MbZolv9Vrkf6o16JTGoMU9UxiUlqssZlswWlUhsFjmLYu0DtjPTkgaWm8hWhAnC10EWS0tPHmoPF2pPF-oTHtb7w5aLyFaLw5aLz3aLwBarwBarCrKDCC87GyIHdUpfm-ZYHxww8PaNNh4exIl3KouUNdyrroN2mFdbEjP4padyIlGm3UfLdS6yICYsKX5lnnXhCyRhCaawSfgX5eI_gU9LPtn7W9tf0i_db2S1hUXWsDxlDWWklbTo-nImR1HzqV3LDOpgb0tZdx23oOqFpMQNUC82X0tTPX7szIO5LhAjpipZikd95PdObanXcwHfY52Hc9gpmjdN6sddqsXSBtxog3WJ23O523O513U533XJ23KJ03RZ33I513LZ33Nh33XLPrQ49JD6fCI4KqndOqSxuQlujUeETSRilNT4ztvIzu4I7F4LxwAFGFCkORIHO-PPiseuAFtDmWAw-mETKHyQMPpg3UCTKaMHhLMHgvMXgvMXjjMJjGDd4SDN44DN5eDD6rHryXGEwJBxPHFWOXMsIx8ajfcHCR_miHpzGtGG_09lAVY7kmkGkokyfiyF29msOndzMlIIL6PnbBv7DcMYMc6sqNfdcctZirJf2CSkCPFRoIZYaaV23JiCokGS1E9EahwBkZwjVRTVBXAxWGIjf1UHxkqFBTT1BnKC4WUueb9S9ZiEp_ZCFSdvviIEal3zGcnyH44lRYpT-urArDm_dv9-xeBAYAO2r-zZHWYoD9iGomcqSK8U3nqpaQqbCGJ9KUjcq6VHSvMimrWj9UVyhe44dUGpdOSLR0TNxUhIyKhouUb0XkJanIUgyZZRiIyKlU4LJ27MgayhcMW6Vf6l-AHe5n_Quww_3Tss-yxze6LdKfdbvtsby_Dan6Uc_6UVkZiCwqFAiyM19Zi_ASmjRknBtC3w2sgYisQLYh9EVAhSJn3LetqgtSXF2Q4hrg4HvsDGAOgvt0gPYBjEHgs14dcovhWe-c8Bi_Masi_VKtY__iubZKf1brOcErwLGdYBMKeU5KpJsNChWGOkEJDWp0PFzmSOD12pBwkrjuhtg9pyaYVhJlVqgw1AlK8L3HGuN5bOjziALZKiI3hCi4QoEz7shv1YUDri4ccI3dKf8UAF2H-MCKdK_9kY-PVQ0s1kn7U_ziUbNKf9b-qSsSqPdyujLhvlUlgTBs0WsKbhYFzg6eupbag64lZrqJmW5ippuYPiamoompaGIqmph3JuadiclvYoqcmEgnZqeJOWwCwlqhpVshXYDmmjxZS3DREmYTNBYCNoveJT5iVimLWUp361DW03CrQwLv1xRzOcGiMfnM5bL31VORcnpQpMwkLyeG2NVnZnSZqWBmKpiZ5GUmeZnZYWbakJk2ZDrmE8g2gllJrl6RdqjqCuXAb-f2s3f7-XKKlb3bh7jWiuAV2w4ITCkI2kFjPpOZCGW6WqpVDFDcbM9wGyquwQ6UJJvHz7Pn4YtzDpX-SBtEumym_dngi_RHgz_n1AVari7Qci3e25Xi5nxKwJxjpOg5w8W7doE9GxK4H_WhsIsvTJIKM5LC9KMwbzKRqg1UuKzCZRXKaFZfoaM-KcvoaaEbtDpIBgIiFvKEJFxS36EFRFfLgWiUA9EonXS61G-osEi_PGHYkZ5FayDwtCLZ61E9k4vqyYXAdOYGFRkNqezoK3v1yjyiMh2oTC0qc5Kaae4rU57KlKe2BwhPyitG2jaQrRGoxXxIVyu-K34_yjNQ6A2aOmNoEzLfmTdQuTxSYWTy1sJ6oB0Vacd8yFl3YPD5-E3HBXlyvb7hySL9cpdY4zcHFDW-O6AQ6fLN7lakP7d7nlG3RavojLrho0n3iEcSzxecNdHfQDaJ_Y_E5MLnzoMajZDdpJnppovtfrsn3kXc-umiElcXlbh278G692B9ebA5pP364oqbSr-c3J6-uO-k0p_vhdyhwGtXQ_i7LotH5iJEZMa7sNB8FtLRIhqoMNQJSoGhRlC-WGp-FPWe2GFaSpAIwWFx9IiI6DeVQAagqSUucnJ1kZPrjpws7R3yMSaPZEJwLIY-IiLoIqgRkjkbIvskXfRh3DQzpq0PiMjsG0hKmtAgKEWGCkM7vLIpXZbb8NvR4R8mjGYX5Li-sbbj-sbajvSNtR3pm7PE8c07Mir9-Swxwqjp8his6eO0GtoF9_PmajDQre3NRatuLlp1c9Gq245WPRVBiwSTolAiqLHQxRAheSt6w8eBt6Ib5NZEg0zVt5CUTfarmSelBmoEZYhpK6M4AEgeqB4YCqTZ6G9eK1Hpz9qp4nveRTkyKwdC2lWe3MyTm2lyM09uXlOpjXqsb9PUhg8NZzMRuSli2_HBRUmC944GmUoSyDtaaDYokHdUqDDUCTLaFcgVqlRm6B7Nmwe1eCkPmpoTJaTTZlgTDmqCfvneCzOkOcoXOUoydXxWVMmx6vhoSlvEJ-lZvmL0858f2fDf5drjnqOvaCkgOflni_ejw-BvNAme_Zu7gsszZ0DwZrQpwr4gjgl0jUoT-kOCfTs7QELeJwFNYqJD7vkEAJ6Tt1QI6YRE7RduEiQrw93B9w5TCsFNp0C4NVWoMBQRKgx1hmazCclUUqaCxAwV6CcinZAC7UYh3P2rVEeoM1RgpFDqBGXOaKAI0Jyf-cJhI4RmbMJmSCeEgyN5CTH9FqgTZHokkGlrprY2QvJRDacgdypzpxpD2avFFDI9z9TzRkgmtZDazPBkHp7GUOYRa09Q5ow4ruXyozgRGq4Jm7GZEI6E5CXEdFKgTpBpvkCmrYnaWgkZhKSjVsysDI9z7xP3vjI0GEpef2a-TNnMOCYax0rIICSRjkkjC-WzM5J4RipDg6HE81YZGk9Q5oyZi0cdqOS3KvmtSn6rnv1WPfutevZbld1PZfdT2f1UcjaVnE0lZ1Ojn99K7qeS-6mRJryyR6rskWqk2a3spCo7qRppdiv7rcpOqrIrq-y3Kvqt6ybfdci-Qj9OJvPWyJ-1sz9r7M8a-ioDdYIMdWnBT2UDRzj70NifNfU-hwNQTTyejkqiPx21ifmvYulEduxEoawShxpTGn6vDMvLxb5GoCVteJYS5vnp3j38MvB63DuIbHklm9sq9_MeQ6TLp_3FPMFwoZ2bC-0s6TGZEHYitdEY24YDIvKkcpwEGS4rf57AeiVDgjrmOuHxb-2673B9re-QWbOJLtRscwFq2w7LivK-o3LS69Fm0fvTdCq7w4wDOoO9zl5qjFtAMiGNkHAcMq3Uw83BzY-S7PLxwFvOCxCapwNGaODwlaV2vW4l_aTQIvtB-WVmh1ycMCEdGxz6KpIIqe6oWGQyIY0QPa2JAOEpz9CPt7i3KdoKzmhjfTcXUrS5kKJtuKPFHQcT83tNlKCRHq1HdBwXrYYuBSQRUgkZhFxHTZQWElzP8DjqLc5tM1qx1dYEt5zKjdDUZEREk2Wgy9JOPXV_Z2711P3jcc6tHv3il8wkoTwl9KNC9R0askIB8Lu73_GgFJKLUGffb1OmstbAafu2gdM-BkAKId5VaPucRqxKPew9yLjRPN4aIZV944GltpcqodKfVeI2cD1gkKtlvfo6bHVwc_DUkhAe1CfkpwR8y9pAcskiAbTuNFaobeuWi3nZQ3O_T85Ve-hR51xlwoP3mtozRLzXXGPrtCccveYaW2t9ujn2vi2LtmabH0RuU2NEBs7RpupS-nudDV_obPhKZ8N3Ohv1Zidep5gKGi-roHNWo3V_UgD8ru73cL9P7k-b4dGT-5MSvU2L1Vuv6P2h1GIQ7w-ldIMc_eEaOQ8f_aE2esMyxnCZVCYEuM7U2oj3cRsJJRjWg3WI6SmB-I0mECMStY_Jqr3C1TLIHvP2u5-UW2RfLQR9hPJuIehDlFfbp56cE07OCaej001Hp5uOTlfDhgarGIl8bNo-FsvLIEE-N519bjr73OR97lTGBPdeBSlePaXqYmLcrwKLtbdYYDPZt19aYWG3AqboqHxfgWg9lBF5Il6JiJdo7gruKs2GeZpPpMout2zNSum9fovsK_1O9Rv9FumX-p2PjjsfHXduR9RRheyoQm5OSzM5-kyOPpOjz951Y2EMe_8_9TafKVc-E7TsmdhU6ty89h4QvIuLQvhsXfs9GELSlp-YX26UMNdOXpQQDY2ID1Xo7Hb4PffXO3yV_aC_UwPK0bUXRxh2UEKZw0J-upCfLt4Hz5mTrOuQUM4CJTH9lVjPifcAFX9SMbANhirNOS9-W7uK2LNIYRIFaYQYO1fYsc_J11CDZY-so15lnEae5udIssrxjEGHDRHiS-VMjMqZGBVHjKYOKxos1Sgu-JDCg4b2idwUJjdll1y24pf4xeGBSn9m3VO6v3cgIvtqAbqAid0FTOzVnSTUI6mpR1JTjyfFUgROf6Ujgkp0pZ7pSj3TldpPKiL13GYuAyBXQxmay6biuY08IrgPBzWlFoKuQt8sSzBUJkC-ScDX-aREeDn5gF_4SEMaTOxlKkYN37AFkX7JFlwww-6CGcpvryLtyBtaOCpOI3bQiB20s9Nv501_ayc10TLs9z-1euC6LRMC7j5BXw5mpfHhzJw9jWCIhegbyh5qhOyQBr15etU2deodh-U3z3090CBy3tH8MwErNDve8Uz-Hi4NsCcecVbYt4-b9QEwywZA6gckoMjlT0W7ow07KqD8ds6u73c7O7QYzgM68YxOPKOTf-vbv2HDvJb3o2_tR9_ajwcY3bk802pS9n52sv3sZLs_fZiTmt_zwJ7f8ECR7d-4zt6_OnfvBz6UYTJxpfXDs50IstdTwopnZWswn-uKANUdZxLhC6niDPrTe5l3gRmKYJ4GbrxvobF3zlOLhnP2wzn74U8NBrnqQa56dKft40gRxpEijDNFGGdnP87OfviziQTtOFjeQe5RE_g44B7JcXnTPy40_eNGmrF3d_8kY96_s_u93-nDYrcJ0qoDIN7padW-WjfYkpHQ0wNtRZ1p0YLdDGjJBHv3WjY6D0dujzdWbLk1FQpdBDVCMiIPTlWHyFRBTnVc3qnqBM15niVNszb0ds4cxWB9zwhuyyoCOGshuVkLJzchYoSe3MQIxy2YFuynJxwdghZNsN91XdAQGcW5Ela8JeufhwupNIL1v1rU8E0J1Iqwl7kOLK1mHd9DwsNOTwvHnd79gEcTFm1BYLIURG4iY0SmzVgBe8yJxFhhjxzsDkjnmLlIPcNF6hk7Us_tKkccr-8lqeybfaO2zulfPJl-7aDXyuidiwgaxO8D1yA67YxH17AG0boGrZrNRFwewMrr3oyggRD7lbkgVpwd8O3pVqykGndbx8tDc09qoDmkLh7OcPFwxo6HM6c2xS-ukan0u6OJn9a95oQq-0rF0tGJpaMTS0cnpsGMgkX3YadoSSIfm8jHJn_svGbVqVw6-8LkD6kTdPKgiQk9GR46aAocOih0ObXdga2coeMYVGLoICjSNHQAiKEDRBSWqFCGzeD9dbMBMXEUqYRsgzkn1cXqGS5Wz3CxesaO1TOVLacvNhUq_e64Q-ryapiPHjwfPXh2Hlz0JBNPyH7LqWOHiD9S1aZ4Ncxnn5_PPj_7TWCCbh70agewspukkXGTRLDZ9Ij-5yfvnZ-8d-at21RHDc5jmLkLAjRcEKBRnCMt13tHKrJvdr6jpG8ss0i_VNJy2nNJz7yt1CBC4YCirSzktQt57UJeu3g_jFlJv8rDzk2rOiTw89pp0DA20rRoJlrSNGkITZtmhaDju62oRrCKp8pV2jBWf1Y4qru2PKpz6tU59R0AZipDje8db42vD2NGrd_YTZF-qZL16NTr0anX420wHVtEyFtX8tb17Jbr2YnXs7euD4e52gJ245X3nZX3nZX2nRX3nbe6NH_COpo9YZ2D1pzTbMtphnS3qN32swyPhLYe90qmd5olsq80q7XwhWaJ9EvNao4NtOF-H31zO_rmdtxdt_PuupHHbuSxG3nsRh67eWeLxTN8duTNO_IEnT8obntys-3Jzbbz49AxHwoUqLRzVF2TQGepoz88HNMEXEYdNmDTaHfdSN_L79o1WSg-QBhgTKHKUGLoohobCWXOhkghpJ-RTLkYsQ0q3OzC_X0aOzsEHYovOJeJoYFQ2aMiCNzYGlPkPcES2Vc2p_dvCFbv3xAsF4druDhcY8fhQnlvV_qRpvUjTVPttTRNJ3TbgU40rRNN60TT-pmmdU_TsI4NzyONjmclOrUAoUKYE5WxIh06H2qi_ogsB1E2CWxBRnhKoKtS05qYsFVT_UdgCDx3x3yD8x0gzkdlB8qVCVGiOv5u-MXVJYYqN-EAXQwdasQpHxRxUjJaqO8QOrM7X3CR8QUXGV9xkfEVF3GR1YaLrDZ2ZDWU9xZgHFnxOLPiQRx4EAcexJyH57lYGMOeLOMsZprqTDONr5eM4Q-tMC-6ihEIofhx4boOZw-SwoGyNWVQyt22n5TqGmfLIjOycqTC2OYoq5kJIXx7rV_YsmQw-PiLEXNZ0wGDzYMRs1B9gDKXdYCOVaZDBenQjusgdx1GpB7qqIe8g7HTMKFXWXXg7BaTUhCqTi1V6gAhZVpV70-G_Rb2mv0s4Tdm7rcBX_CfJf7O0C1lACB5oHpgeOC0M1t6TPBpb7ZWgqVLWzODg_1VnwUFhPyubU2ngfz-by9Laz-3HhJ-3Mzt1luDu4aQdeoAnbRxGOw6YAkwa2BPi-PaOj6j1a2xAxrosHKQu3eB4Qp4FRKffawkc-lR2hcitU9T-MGcGIFAezrFjJkNflO3yrwOWD1gxhwF2n8t7JCVocwZM_fgAFkLGGjPpxU4jHa5MA_c3nSQM1MeYPurEEQ6GpDxpUEU4XcGMYwviN8Sf2sQg90SrvkBoHvgtCtc00PwaV-4JzOcYGPogt8aLikD-c3h1g1vpcJxe7h1zluvgMctdvFWNi4hHjRoHyfc9-xWDxKaG4OZnIZ-RdrVLcyod8yPWDpg9VDeCTOQ302qVOYKDpC1KJE2ags7yNklb8ZEhi56Gq7VHiCzuGN7gDIXb91QpO8WaAUOI_MR2-v3JZbwm1sJa0IAyB5oHjht71ZPCD7dZdjTFNwqi35Tt-bAQH4zuAv0yzYeN397MvYLVYP0-OqkBFf_WwmcksGBg8xOur54a2GJv7XXyRPYdB2HP11kSBMTv3R86WyJFhi25J7eNEDT_aXG-3nlDzSjLco6zoCJoRAxiGMlHAfz5_UCVO6PRcx4a8CTeodWzVhhKgvQfIxqMTGuRgw7_vt8v93mv9-jN-7_3wFr4wrxn7DcyezmE4eJ53VFzvc5S9zH3z5z4oxgVnFABUs4HBL4Mv1ZyuXH9VjS_cEKNbEZMDXFIhcF49q6phzbMsuq1JZTHfj1KG0LfmRK5fTTXFzbut_pajuVcn-6AsP4ikrlyZNurVZIolYgtFnx6iohWFKhklQ5IeOCTmJQmugw5FQEMioE-bTz62NKx0kr92RL2DyZCMGCKaXoED6XIsH0YDKOJd0hRLKxJBNLxg6UIBjX1jTl2JYsPfJtOdUxI0KaRShYsr0XjGub0rQAj6XcyyeP5NWw3J6q5uKhgPr0CNWMuoNiFptyoiooZqApVY0UIps0jWlKq5x38-DUoMN2mih9ETDB4E_sZ4hQLuuwPZeSa3ITcCxp7tD6wBpl1zZQLgrGtXVNObZFtovDt-VUx72jq9m0ZWLByFXBuLa5J-xUG5Qi2jSnqKXiIbiT5KQKSakyPWMNdzQoZqApJepU93FyRSH5RIyBTD44sVEIvzU4oRm-sWZWzRZ1WE8T2e5Hu7Xh5AiWB5eSeXJa15Q_yg9-6o513BQJFUBmcMawbBUmtd2fEJg2zkjlAZNqMgoUtnE0UnngNKPYwk5yUIPMPWZVCHIaqOG0Njg0slBhqCN0r7qExmsWb71dv9JaLjoPiOk89JT-0BjJYWZuYnuBrhrl8lPDGgFb-W-sRoMNkTu3Imkqpczya1ttEbx54y1DNcN9JlyZEtH0YghXpsQ5xSmUWJ3wdZNwzWiN5Z7D-5m9hXSsENOxsnIFsW6w5ZhOYyU5FoGU9SLxONHLIqQajpjorhEbUEePug383dePn83ar307HAGI-No1xnRvScrx6x8_4vfnWfrbPamIf9yTjruHQUMO3p4-AJRhZ2AT6lNCekq4HhKQ9tmE_JQQHhLwsx42oTwlxIeE8NRzPDKckxPmw4vPczkn59c-vpocFU_fKJaW_lFTZOpnnED5rszuqwRddNs_m1j_Skx_JV5_JHqubxPzX4nhj0Qf_dsmlr8S4x-JfoNsE90IyQyF-2r0a3UR8Y_zH7TurDOqhxc2IewGrZSmKdhcm5rB00z7GcL91YHa2jaWBgv9IqwwJFY6hCTYTz3yEE5TpnT4TTHP4CSlpcZYPWCDsV4O2PTDMv43Ues_9O7v5aqzW15a6kvk59boR1Xk_V4tZ36fL7_VkikePmmJit_HJL9f2nvXqSkePhqV27sHCbh584JRELp5gZVKDF0ETXJioYzFT65kyprQRVBuDGWGAkHzm4sWusvSlSKBR--VYpaQJPREi2tGpjwtSolZOZfdXA-2am5zCjxKeCAjZdZ7t2uX2Pxa2Fx8A-qSIJM4bwJVhi6GEkFmKgXCqZRYhhEHdkZSLIlsmiREHr4ZI7Gk8ZSyDaRtS9g7Kim-zt28WDjE1MIZORlRadoYy2iVg3DCfrRwwPIBa4z1dMAu06C-Jx_9UboDDLy2NCL-1tKkm4-8tjQi_tnSzD7NgIQFdVGgRJBZ9wJlznixVCDImIIcSK9zAL02UEC9ntERf48c1xp3SWntQV1yc8njkLqZi7EfWRkRQYWhSFAYDPEEBJ6AwEMLxxdhPgCQNSCLDTFdbEZOdDuDYTOLzQjL4hCsMSYLxmLpgI0DVk2DLql3W9orQMd_3-wShy-qXHSTusdI4lIOhipDiaELzGqZX77FSS_6MVyCIkG2EROqDCWGLmqXWSkCZYYClWUzTigzZJZYSW4pmIUiqbzKpvGaj0KM3zEJOTkfMlsQeZQjj3LkUQ48yoFHOfAoBx5l24iL1veKFeqhwPoWWN8C61vAh1Fhvjvb7k285Rol7rWBg1fvU4tyrzcLXQTlxlBm6F73sgKkrEpSqTNUGIoExTFAl-olK4yttSQ1VMCpSDXdjWpekaQCbmzk4Yk8PIGHJ_DwhEAQD4UhNfOhSIVJlWfmmnKtSRWrPl9hbfjcymBq1Y2c1jZN5T2ixqobYbHCE-vXAUsHrDI24kFumAa1rdNIoeYs_r6z8YpCiXh-SaHmM6VRX1IoEc-fKJRMUOMHiwbTCTJyMh7yDiZPkBGWAZ1Yuw5YOmCVsR4PcmaC5lukaHRkUOSF1Ot6OUVTHmb07ymaXwkb4-UUiXj7eEgg3ZoRBevWO5k7eanRzB1iOndGTsuMD4vLCMtY98iLS7B0wCpjdnGJnJk7eZN1dzIEEa7iz-4EEwftJ3E-MIwZoMrQ2FDMGQZVsHmfSktLAbF6wMbGZE41YONlb_WF9KBhU37qwBb_ncWzePlKw0T8o4bJ-EjsQhwyCU5ooI6jmEAsh7ovyBvp3xR5DhUqFrxjXim-ruQWiGJ8Ncw0FauNA9YRywGx29we5eb9AG3AOuDP0ADN0MYuxPYELxb_gel1LBw3c93JSncuQT9qHxEz63TQkxObcqyPnmvI0zdbg2i-BrPE1aCRLw1WDpg8CVrPwkMy-etW9xG2R_1rdYi6D1hMf6m7it-lv1-rUvzHxafifXzV-HnX7nNr5sKKEqsSVmaUOJQGagwFuyJNZobbcf2KuNwMsFgoXtY9tsE1rZkyrGmHdcRiQOy-jnGUqwMb0LQBvKbjVYdfQdBit06gKEoJtLbmM21pklxRTDjcZn3v8WO5yBi-1rqwdRnTturwGqqqxmMK9kRsgemJxfSdq4Zl47pXOYuFJ2zbB5v_gLejPYnXbfDzlTVQwCXFp3esa6D4y0X_05hd-udFH-d7j28tlpT-2mJp8R8bL2s9XGQsAhGrGIhYxTAYuo7mRCLUElzP8DgbHw0WWwiDBaC4vlR4Mj4a_BWNj8U6Yg2NT5i754NcDGh8gl5TOxmfoDesYfkFPQyjhRnSY8rglIiTOlykcFOiufttSjymXN482NJOJjDUbTiWNM6ixTpj-gwrIWaMdhi-djVRKG0xbZEZqwrveS5xfF9VLI8GAUZrFHjvoNpjsXHArrOFk0XDeH3Ax9kiqrxaRLESU5Ff26xZymubJcvktc2S4t_aLAlN7C1HLGe4n2Ha-EgBFsKNT8KcpxUZn0i4FH1M0XcUH_IcU-imki3ttE7j07ZDSjMpolSRKb-2ymK8DdBJIkWVWWLcbRFENWL8yrnG_JVznaW_V1Qp_i0zmH163_g53K-3A-nM6tOZ1ad2hnlPkXhPkWBPoU4uHbYD6WE7kP7aDqTDdiAdtgPpsB1Ih-1AOmwH0l_bgXTYDqTH7UDKT6tPAz5TlBcd5eGp-grYTbL6dlOCOuW6GrXF-s502FIkovc2Bfup7g_bYTHrEtOB-KcD8U8H4p8eiH96IP7pgfhr2XWvpvl65mvSL-JvfVA-k9p8JrV5nOEzYc7MtjOz7czMPQNzV8XXOMyFMObNGT9h51dpxkgdskot1hEzvDkH5s3SMMObs_Lw0yo17xim4Frs9TozYxVemtPW6yWNo2SxzpjlpRlupdnaT9Yj18eUA583s3_qZf075bTi8yNnxpGxmDkK1PaYlZwPhDcfiHE-EOj8QILzAwnODyQ4P5Dg7EiwLJdy5pHlzCPLmUcW5pGFeWRBHinKXtbVdbsKix75nlZh6aCAsgot1rHwgauw3K-ZneRSGtQwVegI3Uz7GY1LCKxnux_eG5bFOK03WyNCpdh1nKBmu441djY3XdaI3eFhERazG8dVBChbOZDjciDH5UCOywM5Lg_kuJzPz1d9-_z8hzZ-cwQdS3p3NKbi18sTa21NeneUJsuknhltPTPaema0lRltZUZbD4y2rtcJDu6nXvCes2ZYhK8cMnRweim4GvyZTc2QYu66a5JZjxXJd8IumbepTcppEdXDObVGMOf6To6mMuk1nT05GqzVYvJqp8H0XcSG9Zn1VA90sx7oZn2gm_WBblZHN0V32pm3tTMnbGdO2Ji_NT55bcwF22DNbesUSzog7qLhEZa4C4t1xAxpk3jghqC1dXZ4WCEtAkEzDbP0s60jTev4ml4SPDm-Bq8DrJ4YrCNWTE9u-nmUG6Z3VRtw6l2B92JNTzLejoBe-PWwi6eU8XcKvt9mSzumPNLF9kgX2-FQOEKKOmWDlX2FxPb-tOZNObK-2oFItgORbAci2Q5Esj0QyfZAJJsjkupXtQ0VsetIIpt_XmG6BlWKyvTFZxyuLzeflL_jzQBRfot1xCoqf7-fiZ3k0oXK3xcdPSh_b2MXYnpizxk6X004y8Unhd_jQCnIBK3WraDyhyQ6X1WFxOD0FrNudQ-Md56rhEO1j0e0nV5-tcXJ69im9Wj-TxBvOHp_yEh-qJ-3O91f4hG97gc62w-0tx_ocX-gvf2B9vbzmfC8v9XSS4rbb7c0xV9QXCm9vmS4Unp5eSTca_uq7Xecj_dtn6W_bvss_XPbxQrMkOmyusUKjHV-iHZLdF2ifh8W88iPKeHvlNOaGgd2GqEFM-WUcLgKMehE1FZzeuI6DqfFJqXB5fBquoNLdPA-ZfBtnnHeJY3zLmn42zyyuMaBH48Djx4Hvj3aecGOB349PL-efi1d8A6I-DWLTSVL12KM7JvSheGtFJuFzEcAFosBC9Y3Tqw3VryZMjPcJjQNO19dTCbisPYOMS1kBYc_9a7D9UPFZiF9MJZNL9andVzv8LrD1G2LTYeUVjRzvxI0pZgSOMa9STm4PCnH-Fud7EZryc6KaYkJ96Kt6we5wDUdTI20-I-UeOiLvn1jT7W2jhF2HCv6hkA1WcAQqKyBaL-o82Kg4wZWymP4uIGVcsmwaEOTwZjAL-00GG8ItB5vVLRZjJ_JvnbasYkf8fSFR05X2Q78s0fW0l96ZC39rUdO4T7SNdvrtKJtn2ySCbstNsliHbE8GLvQlKyo487EBD3ixfwDtuFGzu_5ZS2YQmRtB3NGHLEQs8IOcZ41pVCKrOAVVt2tYBNa3WCHHYyUf0x52vVIysnehPKYsr7y_ZDHfxd9DQqagkB7B-2ugY57B8nM8HnvoNWb1R1476ANOGC8qsN576At9qs9hE2oX6z2kPZW48Vql9LfrnYp_eXeQYbgfdv7y42MLMUVA_tEQ-IFgWXFlkR4_V1ticG0YD3FPRac4FhNsVnIGIwVbjEdJApujuW2LNiNiKfxZv8ubT4tu_i0P0nxabeTVlR2TsmPKU8bFEk5mZE90m7hR9praM0Gor2GDqVf4fG415ACeOFH3mto_RbjvYaWSQs8ns_s1-Svj8VdoMCfV7KsNRF_uzTnxbDXSzO2b7b1WvpbsyKlf267aE26nvQ2PXxm-ycFaapdOok_6SKYuV-hGN7GFUxMBTXEqPw1fXaaXD__vpA8s4guJvwE1AxSqNhsmpETY2HktmGpZjhwfaTrrPHpQILTmYCm9JXupPqV7kjpb3VHSv-oO9p983plRCytOYzTgqfe_rtTys9fKCIdisDvVmn2HCV7_Y_sWzrvXJLfopgmZvNxH9fK1NN_cGZjW4YxDX37Uo__Ybniv9J8BjUjIYsDFW2fWBsHrCOWG_g4UdGYTk41zWdVh0IiPvBamAZWViK8cS1YM1xaK9J1U1oqiRa5YHZlVl6FePadMpPVzGQ1M1nNZ7Kaz2Q1P5DVfCCr-UBW84Gs5vNBtzaB8eNBd8pwco3voj9Yhlz2TvaFZZDSb8sQfqyElv9gGaT0z15FDLPOlbyAssyzKEvBiweKLQa6FpemtJ1iE9bJrz1ZLHiHQVZAqYOxLPvcXaWmxLAbI0bAZrqC81KmTxbreK5W8K6HcbiFr3oo1rhU9bEodyKM5ZFK4rieUo5tJPopK6kwySxMMguTzHImmeVMMssDySwHklkOJLMcSGY5H1xrExj3pHSI-HblLxZs-Y4GFqCBLxZseUkD5dmn0TOLiamWhS19j2G_XeYXd1X_ihjffjDSsI2TNVwh0qIuPcEGr9fawi7frlfJVNZ6NU0y68I0yVxlUSydm2lP6AXrT820nxy0mbI3K_XxsLjSYfFK4a6Z0k5LfkXzfyjtmLIeoJxLO6bUx5SnuzNa2imF9UqOCit-s1alcWINZs125Y_XqBoFlpsGu5rpQgNX-aC-8kF95YP6ej6or2e6W88H9fXhoL4e9ij1cFBfDwf19XBQX8-3b7S1jD8c4Fd_4D_t2Qxv9Nq41vbNY38t_a1xldJfG1eji2JIdb4fjGiD02JjQswXGozwXO8HYfP14dQeT44bnRyvFAqbbks7Lef1dYaH0o4pj2fk7fFcvT2exbfHpwCNnwKY0g5P4BpvSBpvSBpvSNp5Q9LOG5L2sCFphw1JO2xI2mFD0h42JO1hQ9LOG5IGhOXFEpx34F4vQSn97RKU0j9vSGSJdL0EjVgmrTbSvLHo-BFLWWN98MZC5A4bi14eNxaSiTYWnTdGmsIdMHlOS6w_bj_6I-lfnwp5Ku2U8nhO3R_PwztvYmTT1Ncm5pd22gd0a7L2Kuq8Gem8Gem8GennzUg_b0b6w2akHzYj_bAZ6YfNSH848e4Pm5HuNy9DtPKrzYiIf1594r2kNX9tAwa_66YpSqXc6hpAp3RRjBZ2KXa9iHj0xH4cdg6C1cGNAUZslPHQfk15isohhZ1UezyS9fFI1scjWR-PZH0wWZclNAZ1NGICvnIs2OFOnaaYs3_NdHjvMWKKfdZmkviKnA4mLs_BVHowlR5MpceZSo8zlR5nKj0eqPQ4UOlxoNLjQKXHgUqPhzsv44EyjweKPRwll7U-4ruDB7EkIv6a7A7YEVvMvPKzdQLMik7m2aTki95DnMs7X4He4Rfhw4XRfD3F1sj8FSBb2mHNS2nHlKeIIJm_T2TrOabQu5DVdBTUXXtoIGKmktGruWRm-MxMtfpkMGam2oADRpqrTWD8eK8j_76R-N7ZSSlvmamW_pKZaukvHx7_9OWbY34V_9wYWTIBj8IV46vURpreJFMc3xr7UYTB2KwtEO_NIYZdp_HjWnhzfjwHvr0dntqvKYc72gEKe045lvYUNyPvD0i5YycdAXyurVj3nleL4WfveX3bartXxYyRw-mtZnRwoQfivtprAx25r2Rm-Mx9tXqzzANzX23AQY6XfTgf0GsbnIP70bV3j8JkBYr4Wwdn5mFh-KDnwvn6iyPniDegFWN2aaU9b1YcebNilVdizE-MWjN5Rp3Xp4e8342d_W58in5x6pmmPL20KHmOKU_RP0xz7eqMfHNdsXjAOmPmAFhqOlmIyJHxTLsPr2NLDfvUoZqO4lKNxIM1s4GIB0tZtJjjkQdLmQyfebA21CzpyDxYm2ox5sFaDy37eObB2gvGj1dvfhbmV6whfnUbVEt_yxriy9uganCWrhAm2i1GSDXlwQAlvP2tGNNjI830IOEVaTENUvKBCsxbRkcqIAU1dwR2apKmPLL8xLFDTJ7nlJOFSszyZdUmDJgidiaZu-tGjo_XVLrzdbo9Pzupmo7j4k7M-hOz_nRm_enM-tMD608H1p8OrD8dWH96YP3pgfWnM-tP4SvmnPI3F2S09LfrV0r_uH4HKoh8S8lAgyD5LpOBCmdMCJUniGuUj0OhlC2-cCMKN7XsstQe4SqwmKyWC1fGn0QpA-U3xiRfFHYiZ-btKn2_wyopYnpkpeXHTUV-in4iFT2nnExMzo8pvKkwKSezlB83PPlxw5Ofng1oaacUejZQzbChgcm828i828jn3UY-7zbyw24jH3Yb-bDbyIfdRj6ftGsTGHe7ENH8rB9YJygSZJaMQJUhXLfSmsrQIMjWmLldmRa8NLVz8aYR4QmqBNkaAxcfuKlhd0iNgqrMg0FY3zFzVAQ_eaarXrDktzUFOLoxE-W-HGkeFmb8vJoruA1aL-Vxq1P4ZoyQBvwA3pLGWoVI4LfzbKkn24EffNNzh4JbJ8MqCu8iCm8_Cm8_Cu81ynmvUdxeQ5dXOeweymH3UA67h3LYPZSH3UBxu4FhmnX8grkmjj8S_bcGbWL5K7H_VWd1iagrZkGab0QaaBBkVp9AhSFctjLEiNQzkglpVA4j8i1DhLggK5SopETZErUxcUEXFXRRQRcVBF8_VHaDS9NilqGUpzfMbYplKGU9Nn1Iss8DI4zbHymGC3UcbPPdIcXyfjnHFLMiB1l7UnmXUmGXomuyHnYTFXcTA6DXjP-7S2D5u0tg-e0lsGG6fS1nY_B2hvMZDhvOOH6o1DV6pZ5IJgSDGgqUDhhErbpw1qxLlPwXb_vrFUizTabft0Gsg66FF4NmyRCTSlRxtd2paF033ujyyO4brNglT5h8ZNZUN9LWiRa_UtCWv1JQKf2tgkrpnxV0dqeTDRGd63TIaRP6QwJyHJuQnxLaQwK-cmWaOz2g6ot8pcxAZdvtWeJY2mD8__pam4P7GY7gf7EAC_HOfIC-WagTlAJDmaFGUEZDIO1iyGYsXHzhRhRuauEO9QfIFt-5EZ2b2rlGCPgQzHzAt6OLfGnqRgogmZBfmyyTa2D8xGVZn9ViDD17kY9eGahtqGMN7QQZPRMU1C9CLeZYUDPA8cJAHF5PlwwJM_RDUbjpw9ItRFtWbXQnCHWprE-ueQjnX6UaF18QyiyVuazMNWZuV3uCGkOZIHnvZUzoml9Rf-cPinzBbLzzB1L676tqb_yBlF7CO8JSwkUaIwnpKeEi7QiJoUoKEy4azPXlK4Dqk1RgqcDFH6QaQUZh9DtMVNYBsmVhbFqxFqHAoYLFImPmJRXF6gFLB-wCGzehRkh2lmZ9iMtamoA-3hiOgE5eUjKmmIkaPEbwmpi2AX36ECh9s4Km-OclIU4ibsIggyNfIiJkP_Sy8IDmx8U_zcjGcoa7hXUOIz6Ss1g5YGhlI3n_Esn7a3saZTxBmTMaiPy6Qo0gM9WRCJQ21UBAoGSGY9gm9fPlDhH_bPRU_Dt1iy_V7d5DlLQpiQCbGaRZ__z0Td0jkMJRefynlhRuFu5Yz1QpoRsIyTfEDTa_Cm7FUviElQOGCprY5yd2yol9a2IPnNjnJyYLiSlFYmaQGiloYvaTmP2kRgqavlPQ9FJBxWLl6nQoJ69D84MqdZuqDHcFLFSd_uTrqFbrwzcO9veREuJWQXJlBclwy8Z0L1NL53vJ0_HIB2jC0GcCMomZWURmFpGZRWQmA5nJQGYykJkMZCYDGciAeNV88J0BU_zBj-L-nKhk2B_D9ljxRPLM0TJztAwcLV4XqkzcsXYGyr41mfm-Mvaa487SX3PcWfprcy9vs__R9t-zd9uiMr7qQGmfOgBVaLPGu17Ms7BS955TkbaWUewBICNEUc5MbntDDQvRSPAifRvKgci0zAa6M1aGfm2BWA-T-_cwUJ_bRKhHvgaSGMoMYUZ1demAZcLE_UFpBpGMQzoyvlaimeelVsi0tP2YQhF4ltHahCpD8HylXwBlRMCvoIxBtqtRhWjXA2R0r-0nNQuqDA3qH70ApV1a7z9NHTGdZ3hY2HRnKrMZBQuZWADYUr3ehR1yWDpg1yNmltXEBq4Y_UKMweoBm827_MqSJjKeHvDL4mEM1IxZZ8IZkV1rxu5vkiFn6Rvf66Fdm3e_IE1T_LWZb-kTq-cV2so7Gqfi6dNOgM38rOJzL4xyuoWVFBJd7_AkSFbR_JACvmCnCfBISlKkqr5PzRWJZ8Q0p0da1L08QDbj3nWbftBS3t_6KGgaO75PNfmUaaVdvr0fsPKIFe6iXg3pODZmDepXOQzWaUlLU8gJSjW09KRJe6maLgXwSvNF8-90vgMdeqHzs4rvdH5W8YLazPkewSuGfE0AkUbIeq_GKM_-7ISFm4V1DmYphrPMEizEzEYKrH5Sh2c2piLAk2lvQZs6sl0AalMXDkox9nnDgjJDjaH7ZS0Y0XpdxSPAKuYqrJdyD72xgtamXkICbOJcppoVGiHy2UBkAQVqBgIega33E1_XdwwcPByM7f-FzUUHW6sxINV8isB0y_h6aR_qkIgNA7Gr11k4YGRTtDWMpwf8cmqpPSDOJ0WA-fkR-poU1yu9IwEqfn3BobVZ6Rvi8DOUXxMHyfO2WWZMgZDrzBKCazCgsxeoMNQ9_1cIkeIbEApNcyD_LlIW6g8QN53WXihnuFvYlmsXWGBPXkPkRRfYa2ufLdafMF4yUjfj0eHJjEIEC6_tGs7Cb3yrWvjqqFHE3_JNrbB4BKdxxuFGWzsjdsMursb1lN1b_dh8DTETQk5MIKNgcbOCBXlaIEKeU4qg55TVR8Q3vLKuEPG-N1bxIp6Lm7Y6LDxiRmnjihFkFSxmiwtf1DIYz8u9LKhtjyPItengW-sX4zenY1LFdzZ8VvHChptxREVIl9falAhhfpPO_EamP_kzFS0CEX_KIhUbhN1roiMVyWchpkKJqNCKz27XwIqN7uDh4I5NHOelkehcpaaLITgbMaNjofEEdc7YueuGEKtc5IF02PWI4cZPx6H51SWDzHh9wMef8nGwLB6-VBNTO2Nfd8BSdSMYGX-Vuc-abBfQGsgDsa-sQUpfWYMUvrcGs4r31iDtPYQoeWYWlZlFZWZRmVhUJhaV4xnhXGbFZ2ZamZlW3kzL9sWv4-xuMco6zvharVnDGY45LNSp4RY6MLJs3obF3hyxyAOhLyh3LI-cmnSelpF0k3FggE42guLn-vVJijTxteLPKr5T_FnFe8XPxMELkbdC5K08krdC5K0QeStM3gqTt7Id9IKYzxXicztivFH2sh5fGWVfQeFJ2UtgnS0HMmejuRuscH-sqyibcC2oPUC8fygPNK442ieqUeqna4essWV8c_dQqvhOY2cVb8_-aiWWVoml1QrqCU-INCH5h5u1gq-U-a_E3Coxt0rMrRJzq8zcKjO3ysyt8rlWZTJXxwM0nZLD0gGrxY8u7bSruwao8HBwh76Yc3fbTbtYKj_nEsysgspnXzL890dPtW7lplTYXj-6Tqo_4kpmEMz-uy5ya4nTwrFDMTnNaUAmDNQIOWTDslf04t3ltry7vOByiegdawQ-g6TyRYoQeYW7ro2Mc9zicepbOcPdwtqRQp0lTtT80yVtESH5MCBGsTUE8gEzi6L1J6whEdZ2ICOQ4voguX00uvBocTF8c37-sseV7HFLXzGIWcVf9riSPZ5VvLbHPewxVKgxlB8gMjX97Kc7hUEQlejBK0knwtGJlPTg1a9nQui6jGQzxrZnhhpDBzLRD8Sh81Ua7aE5atQgxQbLZzOnMY5RVXt7kA3OJAoMavdWVXv5apfX5X3pb1R1VvFaVcdFSjjwrswcxgFbYYXYUQ7vKFFSHwcZxR7n50ojWViUb1xeZQcxi0GPB0YipJ4RUw4xncGkYlwM0QUeKfwAWSM7DgxlHBjKuHiJjAOFGIejnoGP3mSJaKRgXAoaUNhglS3-cA_KFu4erJkhDFtl2hW9FrWrMNQZOrpgyczw0QW3y_MNbc-FUGGITjgEaoSgqdTSk8HYL2v5CaZJe-YfietAeAerdaEz1ros5hy0HQgeBx4Gksk0oJlk5nKQJnw8PSdL-lPuN5ZUqvjKkkoVby1pC_De47R8TaN5WlbZgnIBBy93Xlj6ZD5bcHRBRjh4Ry4Iamfwrl0biwjdd9BOpgN2GSw_Yc1AxAoEylyBsWTa9c5ZjXVTOW-htG9o4TQ_yzoCYAYVHuX_IJ8egrMih_TN7Tmp4jtFnlW8fZTfwviag0uel80S7Y3-1KJFf2rRYiXEP6iRcsjmx3SGj9t2KZY8RPSUQ4o1iCcPUphZOJGON6QkC9HxhpSVUacjHQRIWRaqDI2zD5ExYTydfcssB5-C6CQsUdGLCI9CPj93F_G31_lboiucLYE7VYjZQ_LsATNrhBajCIn8YSJ_mMj7pX5GGiFGWRJdGBXIWMpEjzm0TcbsJt7QK2YIRoIz_oWVzfkW1jeJsHlZUZInLQJ_vGXMFiZ9dctYqvjOTqaXt4zldYm2opHeWDpgmTDRtBVDESC4lyhqmvFtxAsyNkIMGc35CUoItSeoMnQR1AjJnI1bwC0nJFMuRoza5wMryRiGQrH8iA2DwXeSVMUl5mRAoydTlg3WDtjMO_zykPyMN4uLfub-6WEFL5uZ5zvHjIHwFIJn_6KvGC8LHhu0AhHK4VBKSjW6VdIThEpZ6hOESomx0QzUWIhL4hYUajnlylRyJplGJTNiG309QYmgRkimwo0XKXwyr22wWH3EzOIp4xEzvkU0KqPPEJWy2OBFJnqWDxgtHClzOBZSwrsndrLOpvhrFlL1DMMtBA3LRrg65COObxnKRFY6pqi0q67EPSrxk0r79UoPUxRKB8xQiHqgGpUZSaUrrgIZRlvhsGJhB-5R-YapDhuRVwmJRjoiMcu8ca0zokwfXxhXGfQ7zwtOUiGszFtOMqsoP-ArXWwaG8mqVhMv6mF6gGDkzeuEkkBxnMVBND4raHRZQSArRbSg8YUCbWnnnEesGIxvliqGl2WkDrNhavQWi-RMDO1bMzY3XUHVzrgHCq319LUGzjwvVUrmsNOn9zRBfa-f3L4doCKJkEoInfZ2d9POCia39-50FVKhghDdjlQIaXOvDI0HaLo9k9FCdNNSiz80orNU57KMDne-U6kzYBxyPzjuDoxqYQfH3dFxX3M7s95-XAGkUV-MxktzstdsCU44toL2Gdfro4mUNTDFP5s7Fe-fjDZd35E8b5fM1NARjxo6IinHiplIUGHITPvgRyEQq0-RQkh_kPGrbPRzHwr3oTNUuFuFu0W3HBtGJbRQYcio_CistgOOctSfS_BFpIcybsHRQHk2-FYXp_hrXRz5k_lmXZx5Xuqi5qlfragp_oJA3FrSIRikIo2QTEhwKtdRMMGlJ01ZXyl2Vww7hoo0ECpfv_ITVBBqT1Dn4gtDnYvHJdsvPmPQPp-wYbD2hBlWInWgeouYhYLV-ChwKt_qpOT5Sid_3MH4QidF_MXKmodqW4_-Zx2-OSwTJpoZ_AOEHvwDBJHxV8FF0L_S0QM8pBDEv_IrCJfoXikR1Qr-DEKrRoSYj0KopIFIh0ImI_GQHgZDxGlUCpm0YNlA9BRDWnGAgoHoWqh2MnJOh_ELK9pcs5zCfiqiayfQ0wxtm78Hr_PS3DKTt_2-WmYzz3fLLOSvltkUf236Y_QLIxZC_HsUkotUPZYz3I8rIPqnK5ofEX96IbmM0kZiID0SA9GMhTMWzmi0HYJeLqgwRM9vtMrIOR3WWWennFFQGQyviLF8zUEkz3eKGNs3HETEXytiCl7JUnDaNAcmyaMAc4-iS6RBe2dXZEkhUzsqZPKPJgQxapSYOCQmIYlJSKJXYnviQ4-e6ExFe8GQUbZEJy_a1sg5HXagIFJpgxcYVQ_TjumxIHh_Q5A-vtbKBOHpXu7SevaHEj37Q4me_aGE5qpOMXzcR5ufYDgh6YhUf5orCWbyMzv3zG4707FGz3SsIQ08QYUzHorvLNW5LKOn-eCjMx9Y6EhbjJ806Bjj4zfJazhvHgxdDM1Ks90L9hy2pXxhwab4271glzfIvtJ4fEvtrcYXfxzQiz8y6MUfEKiM1-FSzrCL6iyKUOigQwqwUH-CCkOdyzpAJiM78sJHK9pho2SlHLDOilf28cKCoqoU2cNZglFAKdQrYLm-4nJT_LUClva9yZ15vlPASscFNTgtmmNW_fuSvfrwVCLj370VQX2H3ihhZQ9c2U9Xdrf14G45FGiv7IEre-AaWJXqwd_Wg7-t_MCiV2AMqkXVhbOQOavXV2asXl-ZMRmRr7SoQtiyt3Syjq_WQh1vjw8iTtk8kpEjBYtlwkQlMZKoIP6OYm90oNDo0GHH5zS29RyGs_swnKZqH52zQ2RRRfwVhk5BSzsFLe2NiQkH_uwc-LMfAn_2xscCHJVRyrcQXZTsh0Cg_RAwtB8Ci3YTgFTs947uSfa78VlASw8QvWTcKZyowO3rpxSS57tlJGP-dhlN8de7sk7HAxCxUhE6HjhHp-wUnRLzk4ZTSM1OYTd7p-OBzscDnVnFIcRl77zL51CcncN6dhPz0uR0WGet7HuXbyGjqDJ3vX6vUjPPdyrV-1cq1ftLlZr9G_RSZB-09x8Qe6GDjP1KnRH2L5RJRf5FiT5olw9RHxVhCjDorYU-mDsM3pcP3pcPDmfVR2CVHIfHDwNIjJqgsS8xWIit1fAhFAQe7WvVmnm-oo7jojcPxwqLCRAF7h4mniaW5ad97ECUFj6-dTh8NMupAOPy7nVc3r2Oy7vXATE5jYxB_JW9AYELDYLqNyBq54LoFcNxJYbodcVxXaRWw8TdNBU4jF8mHIcomzqAFnNhBkQnB0buXJh7mTDJyMRtAj-bJhF_6-0GR4ccHENyYMBII0XKdQ7XOHy4Rpl1CB-oiL_fNyBSpEEC5TII3fgbHIVycKzKwREtR-Ct9jjElNROop3StpqJxjiTCysnIzXCx9t6ZKQkz1f-T4f9rZKFl7f1ZGdiVEp2Jg7LT6q3YkV6yL9AOKK-DWCOyYeGnzyhnpiNmI96Gr0DHRBCUBHaMI9IPnVwWMoBETMXRLEGRiTPOyL7zxHZf45DGMphwlCKsYocbEDrIAMGsSEt5HcFI8bvVTjG71VYRvatCk_x13Yy-TP3kfyZ-4DojqJlGqbRPKIZ6bhJHimd4ePeeSS3dxYt5LiKg6MvjkSb25F4czs48ORIvEcdifeoI_EedSTeo4ocHjuqmOdxUjU-g9FavMol2CC-Vbkku8RnlStE7bLf9M0OrKh1dJA3OGTeyAdPknl_NTA-ntbvgyBfCbLjh5lMQn3AQ18J2jZ8owv7jGoPwQpNuwL6wUy317ST7qhv5PDNIZmIv17HK7KbXU0rkJuD2xn2p8EDAt2ZrGYMCt1nkpIsRF87ylggnu45LBI2P1FkxaSW_werXD5a5UOe76xy-c4qV7LBEINNVmOlE7oBwdzU6EwIb8BLUffcmMJJHVYQMwf7CGnY5odCjL5U91qySPo4W9JPCCqmgsrt3T33sWJkuZvxY4UH87VZ6BiTZPiwYGJDKLbXoNheg2N7jRWSjLGLMUOVGrN6CDm2IDrWGhhzLeCo4mmV9tO_BKytw6gj4xAqbJxDgg35vNBXKxK_F_R2Rbb-1X6y9Xf7STU_DV2XmCmLZcJEgXogVev0qGL0xtDZqPezUe_tqKcUFmxAxC-D4EuLA6ObKUSvEg-KQjYoUtmgKGQD4ouZcriRgWSM-nd4BXph-YDhC8ai7iJnVFjKO2Ck1v37Z2sDPwn0Vq37V8_WRPy1oxkXKd24jmqEgcYsREgjxFijAa_Dap2Jm1EZOrup4R3PnN8V4MxuUsY4d4_e5B0Y5MtApn_-HeVBkcfG8I_qBsUrGxQJbUAkMoNkKseOLochG4cwZANDwi0MXt7WZTIqLwmRs5iPTSLw_8MyGf8Py2R8t0zGN8skXhAd7NauH2S9UQTHLz_woiYWdhsaLbYcFFrLdiqq8J7-1QhECiE-PIlC2UCeRyjUDOTPEFcTLi4fzwtXw-BscDd_XTX8gca3F2A1zzc73J-9mXtM9YM0QjIh_oAOBRk-PbTSQnCKfOQuzWomjeJqaT4L-SdUq9GdczqMom-p3MBJC_6ATGs4SLkz_1_JL-4dq_jrVeoDUilCUxD9-Y9CmSE4JPiBEtVwCnWpsK3B35VYmc2QR_dkResZVHM9I6SH8fRcTKVR6yjslUK20d7nrIo753R98y9vqJiF3Ac5foW-3UJonu-sQjqb6XR69vMD-ydFHWAIaqUFAOPRqiAmiEINEbLvPmiVIpnKYcRMLEeVWoUfMGPyKZSVQpkhqxMrPhc1xOoJhq1SrdA-GMw9XEo6bF_sBFX8tZXBYEUK-ffWfqD2IEXePvuzuA7V_LbCHbauFhSPZEJQmXImpJ2RTLkCyQSSMbOPUacWRjElV1cOmNWI7A4PVQO0eWAs8tcngJrnG8a5ev1Wy_I3J4A_rsdvkhQiWuijGymSCTFLFuMU6ShrUQNyJirLPTFaiGlrZWgwdNrQaWaG3c5t9SSxDmGAqYX5CFOaFVeAj_qk2RjJlIuRQLmCGe961mrNjLauuACNMigYxkchf6lBpciDVe_vZrPqeW9Tnb_Tca0HS18Plr72A3bYPlT2LxSYSEvDaankLX0IJUUy5TII-U8IvKRTVw_7mXq66_ADg6F4a5CqWItvDFKtX2yBVfy1QWr-vsIP5D-AoBApWvMbpzmI7RRIWeH9-FGnnIMIrQotRvcCFlYKlde4uMY5DxBqSCMH3PIZCZTLII00rcGnDizmby-sAiO4w5a_fZNE83ynfa198SaDir89fgdVubF0wDJhonydtoWdtnGdNlOdNlz9dL1BC2P4dL1Bi2Xd7nT_YJVsMbp_sMq0GO9nO29AO-_lOjm3TgSjJ0LI3XV2d91HNF7duRzDpdBRazA6VWoh3HVOTetfP3_SPN8tAIxl9GIBaBijl-Z3sJ8f7OfH2c-Ps58fvFNd4Y9g16GawUGPVoUWO_h5jKFkscxQ4xoaV4Cuf5DrH-T6IdCSkckkk1FpB4UsXL0m8zvczQ-Z6u_PwDXPdxr41Rm4ir_VwHARAQgXEYBwHY9Iw-V5QZ8w7pIRsvtemfhwMQEIHNdnlWExcvaStXHOA4TbBikrExIQ8d4-XH7jLEimXDciSqUddAeCIdAGMQTah4VAm64QaGsWwumih5bH8HF7FsJ5e6YNRYqvLbUYb9m0-Tj2wW9uQ_Bb1BC8f9JGENIol0H8ZjAEv9ELwfvLEPyGUbtGMugdl9BAzN2UmStX6_zGoEielwZFWNee6s26HJYJE3WI_srND4RXbmYHIwTN00mPvCdULBmsHDA6Vg3Rb_BC9HZfi6JcjDTKZRDvmbRBF0GZhbhBeDqjTeqoIBKCxgXJVdkD3i0uyhG_uZuo4q89R_L3X34girK2xMx8Jh_2foldBDVCMmdDxO_QQvJHpCGRFU-ZZyZlnpnkYsaLwicMnDe_MqxDor5wBfk1-HXG2xnOD6UsOESAY958WQuYupDyz_T-mon6h2lJwFV-jcrM0cvKYqW_Yiki_vKV9xhmPJLf0CJTfJm4-F8yt-tHveXr_W2de4J_8mxEXPDUQggj0Oc04qvmvRCk9hOxcrFYCojNAAQJs0qwgYTtwLDO2kJ4h3xBiaHB0HZNWst-111rWSn1MWVQSk_QDNuPhoFkLFYPWDpgF2ONoXzIWfbk9-wnfyKGf_Xb2I099z3S3COkc4-YzL0RSygGn2HQ5nYIejLb-7NpdO0VBNsbrzq_1Ptrd9Zq17RbvA5Jm68k318D1rSR_5uWbdY4n8AHLD_MYPy7vSraWPQezfuItOSJdBoog_2-gEXYtLw2b6osJ7XGS2uVYYu12YkXJMNATqRh3-M2DQsa1Md4fzlrm-GF36Fh23EaZmXjOA3S2GjnYX7axlSc4hpcRbof7oQLTYYbMR1ug8lwm7wy3AaTpuXgRzfnM5IJaTADkuvCPuY7igL4vpVwf476N-EwvFJStOM7V29cL7Gs1WsgHSXE5uo1kEyWvuMyJ0tB6f4Eg6DlDt2C8U4ttuTu5YuBTSwm3ShAXopHtBMASR8wGxYkh2iFIFvUxExZE9LG31DKtuvz2-GqhZmwJXcHw9d5yIRJi_Ej9sUj2l6ApLmYbUAbJlI9ItsdhLQgyCYtmm9UmCYBtNoEmJYFkI7C9FyXGAAZh3abE4kpJJKISZfkFB66BIh2CSBpBmabXZK3CC7oEkLaJcS0LIC0oROqs0tiS_qt_GYfaTBpSCdF76zonRS9k6ILcnFJ16Goi8vCHgkkPRKbBrd6DZIJaZzrYigxVBkaBM3VYqEClha_LCx6M2gpDF4Kg5YCIHUOjNx-vnM1QALsQuPIYTYAeKyk3GEj5RCJU-dLlZcado2pkrBufP1njkGCO7AGyYQ0QnBiEn7Y10KVoUGQTExBqOzBS3LDF4DiRlNEQkCobOhCobSHLl138JjfbZMbVG1Hc4MaeoWyfjz_WjYGkmVjsHIRFOZuwGZN2GuzG0gXM-W0r1yvRsowya1XALIHmh9IuYaLAyn3cA3UtvJ2FALelcId208Ot_f4ygCGOfK57QFESAcQMRlAhHQATVYZrInZwRIs2Ta5HGbI16eBWTql87qKfkeV4JKrQRohZl3FROsqJlpXMdG6irC7tBAYvIT3bEUh5BYrAMMDl1cZuTaM-iGXaA207Z0sR0FwOcbwYAFTLH9YQOkJWUDVtYjBYwtBqmuIlYsg1TWTNeEwmW366p_B1Kv5AwGVvvQ5gUwb3Gw1SCakEWJ0CS_bWmgQZBQnwYm1KM66-bvVQm7XAhC9niS20XJ7FZxgSunJIqd2tCeaWo72Wgd-XT-mZaz3i3fCZF0pA5GXTmVvUbO3qJksamaLmtmiZrKo-i3VRQ1Ng1S18YurotoIqWojJqqNkKq2yZqwz1aNzf3UhFU0OzuiQwWuYlgoEmT0Eb_EaKHE0EVQIyRzNlDsdZdyT2i53AzL1U8Aqp_ywhZRbmMaqPop1-9YglqX9mQRZ1sfbR5-2lQUAyFVDMREMRBSxTBZEw6EtXl453VhbPMSVo4pMqTVM75aPND9oFc2L7Uw1P2gixAOeg1HZyMjW-moxEA6spWOSgykI2uyJuytHdlacGSxijl-1txVMncybs1bsOYtWCML1tiCNbZgjSyYflURxg4_UChjZ75ZKGOHmIwdQjp2JmvC5tuxa43HrrWHEepEcPpezmI5elobPIM0QoxR6soY__--zi2JYRCEoluqEV_731g7AnIB098TBYLGqDiSUM9oJeTWSYJartigJ8_y9jed7d_fVI6Vvf1N5yc6TxvY5RpsCWkDI5MGRqTSFnR6kYZIpSETaYhEGsFZl4MghNViISVw-JQVXpHVYxNuBKqxJwkj84FJFLgiW66czt_iwEoPrB0nE4jYMsi2WDqSKM-yjPCJ5FPDZfuqyHqaaVGFz5CNgBunBNQAILGFKsAUFM5K0rM_yX6q9uRWZbkN81BLbmj3cSg62RxSLUz94Fm_WLbeXgZ3bdzXp9LgPdlXkCRBQIsAbmoqe5ZLfUh8AT2w2IYBh6k4MKoIvnwej0ivFNflixbnmEOSfBqEXPEzIXCTyAYPTQPbPx6Lj6hw3CdElKxLM5PCQ-lP6h5KywD1tk5wVh3_BT5DlyulotLezTq8h8OhaPC5kMR8I8VjB3d8uB6ssnDyxUcuxPR5ddZMzlId6G6RsLyEB2hq9Lma9yBH0Eiuvq6UUN9oiHb-6e3dINYuN2mMAt18ncjYVvIFNlCOFQ==:5:2:6:842519:8:10:9:10:10:3189574:12:14:13:10:14:198542:17:1:43:3:25::18:10:19:700:42:0:45:0:15:3:30:0:31:0:28:5 years:29:5 years:35:0:36::37:0:38:0:39:0:46::47::40::27:AwYDBQUCBw== \ No newline at end of file +1:897837:2:time pressure:3:Rml4ZWQgdGhlIGJ1ZyBhdCA5MSUgMTUvMDkvMjAyMA==:4:H4sIAAAAAAAAC6R9aXLtuM7khqpfiOIc369eQy-gFtBb6MW3LQJiAgn5-chREdd1kjOJIUlR0P_9P3n8k_4tx7_nv-ms_-Z_z1r_TWn9OdefBbZ_03Ec__Z_07-pfv8z_j3-Hf-m_5f-vUof538tnd6WLv_-r7SqmGEV-Svb9z9f_5Uf2j___S4XVdDLdyfKNQF5_FBFeRzCebV_fk3lU-nH1r-LH1-FH4ueb4rJpOWw7HeGr1rrfy3-Neb6Y8e_8_5q6dpDNf_83_-d8j_H95-6_rT1p_zz9e_6_74Q-TO-__yfPK9f5_XvquBK-N_l-nelpmP9Sf8c_5P-yfWf85_81Uz-5_z6eZby_bt8_y5fv1v6_tn1Z6rjTk75P_WC-p1FoPM871zneWUah0Nq8VnyeffkC27_zONCO6Dfdc_799Vm-yf985XlHP989ews_8zr_7_H-J9rcF8lvvr1paZ32TW24y5bjq-iX5r-XbTWVTT7onn3H5q9Sh7nY8l0l0yu0f_e4aSzf5Xai1Gy_dn22vQ7t1mbVcJCDaBVz3T1TLOg36tVvubvu2trQdtu7Ou3-7kkYP-uh_mZSnL1SYHpRQaR5oElHNLTYwvRjfxTVm_mNQvptcyUlt7KzCr6ocykuZc2XeOa8x7XvKvO6bVQ1dNIUa129VeyEZmVxULDS9FCyr3E8nveMgTAWj2pA1bvRr5X78sO_K9lCObutkzJuHvVtP2W7DjWb-xhu03bWcb9O9W7obPMna3ufH0Dl6w2Mm-I1OyAc9X6XgZbfy2Dq-iHMniuMfV7wtf03a5AZiFtlc_udzG_LxOwf66OlVcSvFakH26JevNABmDeQJq42jvj10LFeH3AR4zn9IAXwhv050uIdsLkhJruEnmNaSHzAUke-RqHR0Z1TW4f2_9gL_t7e9nf2MslqzI3a9yAyCgRqYScgExCjlsLBMgeaB6YVnG69eLdevHe7M_bGZ-XkZTmWvVIJuTwSL8MFIjhKmSQY_uY1c-5f2f3u7nf1if1dz4pjW41-uITqOGXdQGgHi7DNZ5RHxRqnEHCd__HeCoxtk2YBNcNXz0ReJDCroZR9RaC6il5BuUBpV4jM8gABAZJ04HAsEo-6nslH-O1kq-ir5Rc5gqUcw0yQCohwyNoLGQ-EdlsAwtRma3lw9KqUe3Pmx0tBZXSoNaCZEIOj4BaYzWLJg2kScl2XIDd8bUi9Z0nvno0k3efq2M3fsufIF-N10USLkleqEjyqrH4GqV8ceUX2h266ujUq8uqzPk91PO4zQwCi9UYJKeNgBqvrqBCrgZRISfsW6H_CGxa2fZUtnwx2OMGlB93hywplPkDuZTOIdIJSU6YZ_GAI3_zZtdtz8D-uU8JLs-wfs9b5KA9AaC9azK-D1aChVQYpeGacEnAGU9HjiQkHS2GlRg5OdOE0-8LJOHbX6gEH5AfGaH0namlJDQST9PVs8I4s5c1yWWhybPhHaRUZRBPm6WiLZRfQL6l8tqjCfK9STMHNRtXL_GFvHMx44AVBqHUyTAQMT-dDAN5FidVcU1c0VYCndP9O7vfzf12ZE5rBLOvUGboIAgsv6lqaZ10bwKQPdA8YDmcLNnLbVlKN9UyqpZuava9rbdJI0haMp3QcQs0nqBaqWClXEi8JJeFiHpprsJ1bSb1Ve41HZKyrw6JdPLatpnQZxWyE6CMpgirwIRxwFygOiTkqgaqDJ0IEU3TZTupYKJyiYqBRibLuqQI_N68C9eYl5hXuFSYaU-isCeKQP-XcJ_pUbhXkpHRM8H0ImRk9ETCJM2U52YKN9MZKtxyf4BsZ8oTVLjgglKDmdmSK3AJBVpys_RKfpNw7ia_E2BKMvRGtGNJ1YlkVaDCUGfI8yjt0RbQ0xInyQC_96marGZ3NhRbZ0koNPc89QV8xOmoGY5BERjD5Xzy8W2jUu9-o_yVEhr8nGO4efjqUr55FbmHDFwMOVoG8gZbccXNXnzNXz5o9XJmiE6EFKoMDYKMucxMQzLRkEw0JBMNyZuGmGlHG5CJ22XidhmO-RaryExHMtORzHQkEx3BTBbK21EuQXp35vA1xPdeNvsTzV-dOiwdEslBg5kPEACEjCnMuEURiNl8zlwwc_WZO8HbgAzbgAZ92OYmO8aaHWPN-1ngsqQismBihe7fCdu2ZM8_s-ef2fPP7PjnkpC3_LMoQ_nqrLFTRbmkNS9lOHgpXGHuU-jMSApbiChMIQpTNnEwvUZNLf7wTkotQ36dXKTS8627jKzDDAst_7cgMXEGuoTrejhvyy3ZKkyOC5PjMrwEFsfQimNoZT83XCKxMoCHKkS1ClGt6k-X1mRXJFCSUymR3_fXh4MnqaRQHQaBZ4TLFdWyZdEw60oHNFI6gIyPqcjccHymnD_JkWJoX2qidatM26o505pQOT1E0zY8aalImRbFW1V0RPpGls2peMyVocsdkSKZPCmUvkBCgyq24FXHxKo7xBIABLESVap0jtXINTdyzY1cc5sx8m13zYw2YhCNGEQjBtGAQcxdzUtHLAN65YhX2XeOuLE_bex1G_vmxh680WNRhUzB9gDZFtmDN_bzjd11a9wJdv2NXX-jB78KLeFcarQKzuSFk3h5i-m6yNHajW0CPiENd14VhW-S2WtMwBsT8MYEvNEzWZXnyRAeesho912gDNLQt7NbRed9QUZ1eP92pKk50tQ2aZqgXmA5QEkVaYRsJc0wY2-5UCdW0YlV9NMZGczI8HDwmnmpBDfUvTI0GCK21IEtLbHueJokUH2ARB6woIXGA2Tr4gPFzseOnZ7bKlS5qwE0QE9X9ZNmuvD0baHtxQltdzyrO57VPc9aBYCLd0-8OlHVkULJGCWGewwTnxrEp0b3MzCI6gyiOqMTogdNVKxxOaS7I5GwjETCMvgYb_Ax3uDztcEHc6Nw9Z07wQdzg0_0Rue6yq5rHess5Hq0s2Rq5KkyJcC4gQaD20I1HIcawKGSWzVFCiF71UQjJnmueYSiNHMMtxj2nArrRumbRNkmUbYJlG1J0SRuNombTeJmcxJykIDOzFBjCH3gWvV5kCBMZkOTOdM8SPRmZqgxNB8g2wl-0jnzE1QZGlzX4BYDqHLBulVCRp3gpFMS2lNCvpVIgOm0anolmo5eTEcvpqMX09OLCccqyUmVIpMQkM5Lgs8j9P7nEXr_8_DeHysp1Zc3iHdn2jQi_qBEG-RSjYs1LgdaoAUnQkQZtCBDKJNS0EJEUhSqXLBy9QE0uODg6sctuTpEElAdVZBQb0FdZleybvt_HtmKrq72_l3db0cypMAEwJEMs8xLXFJIMs4UkowzhSRDKkEpTJ5kSFGUy-RJhraKiCcZWqpxscblUC4TkQwtaCBiAQIZiUtEH6T6CKpcsHL1ATS44ODqQS7TvtW0ZCo5knEmZx916fbv4n47koGrpognGbhqq2Mnm3oRizPkGucZco3zDLnGeYZcQ-pGITw915AaDeK5hvYREc81tGeIeK6h9TSuqHFNjatCaT6JMghkJPAk-qEQCtJJbl6hytBJkO0EkRTtPUO2LmIRUlcEVS5YORe87yZdBU04vSaclimorOzfzf12TEEygPk95waSEyRFQJCW-OWYKOSYKGRiBBkYwZKeTP4_k__P7O0zevs1r5l9b2bfm2m7LnUFBY1UZKYJmf14Bg-9ljJvK3cdmp75eqKCJ671--jz-MVNuDMTE8rIEkymxtDkgidDiwXDPGyxys7LZ-_lFwBSBeuryF7ffOwJwdOt_zIh11wel-bk8l32S1u-X5W9FGo3MZOiq51yTDvxKX01U81hdaF25i4KXQwOqwut2VLugh76OngUSJbDQI2hzNBBEDw0EaT4c9ezEKUpRGkKUZqSSMBKYQhIwXrob0ZkoWMLWHEevjgPX7r7vXozm1gaLEVot6gs5eyfq59I92oHjGjpHkgKiMxdrX0o2-u57lnBpym03f8TgI7wevSLeRSBTNdhPeYRAOtZklYnCV-dJKLVHL8jJNYnb8hWn73YCn7E4lyJ-FQiPpWITyXiU5FzYEUWops-OgaUeRy8ha5ZqzD7a9IaDn3_dt68Tvf7iH57-ZdReLSF6LRolnlwrur3FnMV_dBiihJVd3tE-oyAO_fAZRVVaeDVGRFFQGjJvcm0BLDVWAAbUZdG1KWRw250d1bbHFROeAR0FaWmOefcnHNuw_0-o5WXxj06LCrT3Mibw5AVGX4pOnhAhfam_AnotFqQR5Hu1w_yCGAua-adx6xxx_23gRpDmSE0iv3B_3byv538byf_29n_dva_nf0v9t9CYIt691LVnUfuziN355F76JF76JF76JF7ekGIs8zEa0q3ir4zUN3dB8GlVWQvbUbBfMUExj4ZIEA0AZAl95hlyeVCjKiOTAI9cNNvoAkcc9xvNBV4Avz1rxffQT55kE8e5JMH--TBPnmwTx7sk3FAFgIeOpxHHc4jD-eRh_PII_TAI_TAI_TAw3ngJaWDXsD_tW6M5oju73VjFX2nG8M775E9AN48ufWXrtML5L_ZES4JmOSVJx6Rp43s1ZvOc07nOecZrc0q9Wptpn8f7vdrM_17Wb_aiqa-ypYl99s1YUJHfEBjD3h6qMdcqTQJw13Fkzn0ujAd-9CFJHo1iV5N5lITuJRIHAiJLEh9cQax4mId1mHmYweXua41SgYxstcBhUIHQZ2QwsUIkeEaaJ1KYQ--rbW-ZDMu-DqaKnqrC1dY0pJP6yut3WLEeI_htfYBvl_dwPl0IiGzSmh_RsstAWbeBPHMSwoZhJiX9mJQOYFWXLL84Z5_xxZL_ijs13ZBi-YP7UJePUYJbQC1SrkOgjohhYsRokILzX13VCV0hXxLZXXMi6AkkCXTBG-xvvAUWTjBycLlVOfKH-jGSiO9kSB17e5xgJPOCB7DrDNreGRHc4r4h86qRyP-kdMMUcuLdHldXcXVAqqXPCM08iCIZ4RSjUGIEVoxwposBIxQlGzpZ_61fu73EvPpXx9BWBjyEvKzkgKdFdUMoYOgTkjhYnULxpnLas4LpCSwCq2E8lSANOK8SJlVOUhg1Tq7hPaLdGglkn6tHvR2jyXASbcWXh6y7-gEaaHXKy5lW84zuZ3g7-21rMwbey1FP7PXS8nO6CBHRdOjI0QtAdbOuFKgxqdn1UYEBfFkTDs6qJiFhtdQWZHPNHT1PIfsIYfsIYfsITs-5-OgZs8uMrGLTOwiE7vIzC4ys4tM7KKcr9lFaa_ZhRR9I60ldE07oKmMtJCjKGzyC_mOQr6jkO8o7CgKO4oCjkL66Jxdcc6uTPc7dLcldLfFudt10qOZ8_RQ_UZ-BFYhDbu78xhEMmXoWfqyxPvii0lJmHKdnWpKmS5izU74HzmBMi0iUrEL01cnonq-8dGXqFZ6WeLXClJ9KMrfK4gU_ZB-L8mrZFwrGddKxrWyca1sXOvwUu2ivubqbH8d7nfobWrobarfuGeoAlfZJNQH_Et0vJTdNUHe-lBH9XVowngoMJ4K1FhIRdI-FNK1Lo2cRiOn0chpNHYajZ1G637Nm_Npzfm01t3v0Iu20Is250XFbMkAyzZTiIhRMtAyMAv6Wh09-gHUWK8FDULOY255gUwGCTKJ0UWobtNlepphOliOZJpYjtZMBQX6jwWM3qwVplikmYKRZopGKqWMqPTsRcXFKM0uSGl2UUq1UicU3blC0yfTnPOOMnIZIFuMlcBzKN1gi3HXtEWzs5Pt3qdiHsoi0tvZ73bycF1dKUEdpL6DWzW9J68KLSBgVEOEAWZPJtsRlu6fOcgqEYMykYLXltfEpDTQfnt0bVYhVqVBCiGJEH1jHKHKkOmY7J9hVwm4HIJ-J54dNoujxSfXklBsIHLZxI4mx-B1vxabDhzPuqqGQyzXXbXLUB4Js-Z7WVwA0ewiiObhPPQIPfQIPfQIj9YzRTnNFOY0DyYcJqTqXPU7CtW-X1St3z7x-JF7ScH8y4Jw-KIxJd1RAqTJeZlZ8Hno6bPEGvILvzKUKH2s9PkPnnav-XXhMbOLj5mn87Qz9LQz9LTTedo1cTO5DeHvSasU_ZC0ThjHJ-ssJnLilvaK6Oyg4qFl5xBJCcql78j_69niPZ3JTlGvn8rWNePFhXksLsxjcWEeyzHd78g5Si2ERvtEqdGteTnS242KFv1wzeVTHuAYrpWzSHGIfPADrmYlKHSOYkbqAvgVF8CvpOF-R1ZPShHqrd4xN3zZ4j52aQSuc4-t-wpfEU7PSbhsIfaU3REv95xZqHhozRoi8uEPHUQyQ3Nh5YoLK1fO7n5HBkdKEeqpfb86Igd2R94zKZHiFssRqAPLOQZAXy5w29RlUiW9LLZOyZdTleRFrgTp0GyHZgchVzFZJultgYU-rwfAhnBKn2S-nvq0v3WBrZ3UPmYpIXBiD0-51757M3dzZixrh7O5czHhIw1UGToJwktw5Q7PSFBmCL4gcA_JCqsLQlZcELLigpCV7OxpDu1pDu1pDu1p9uduawWk4kukFToBwtYzCLlE7etT70Etock3AS8kMZKWfJED1AU7JXtOAyWCBiEVkRwjA4fSfL_mnrDqe9tAU934mikgYiq1o8qtmgcio6vUo1XNtd-6iaqpaxl1Yjkd7bRZZJ5OPy0ItBA4aSpP-CST_0rP772zFH3ByFSXPmFk4o81yqD6Y9HqjJs-A1WGToKMHbHxJRHKDB11T2V-ydxcsLnigs2V4thE8Ve8lvRLfDkry8WzhFK6sAQU23Kx63OaDYIU9map-PvWUoVsb5ODF_3o2bsHCbp3ZE9MNGqhr-iiN1H-0xOZgpvvRWQsVDy0iAwiIm8yf20vvKnJQCdBRq7KILkqg-SqmO9nANQJKVwMvJpKifVqLp5ccfHkSnUUrIYUrIYUrPor08v0SRVfEmAPM5ZFkkI9A1QAWjZSgv_B4pvi-aDiGSmfdKFAcam3UILpVskwjpU3_TiOlLdgSqunE-R68aXvepwgS1y-lqxeCzx2_gx9f2W6pegb013964W_2EzL_HUdh6EzGulRT9HlW4IQpFAEekc2lPEf7sn7b-2ti5RWXKS04iKlleaYXQuZXQuZXQuZXfOHmWsqpGLUBanVQI3UQ-LoLfWQZ6BYpdEPCfN3UJWqMgO6uOy2SPqSR0kptx6gQknnylNPRKPM4FDJpHzKpGBSPnF51DmNu-d0rl1sDXQX9U5DBjq9E3i4cUo_DyvMCudQxjUA5nRwtrC4rzsE4HZfFioeWu4LEf0i537UIlq0o_wtUXQR2IqLwFa6Ix89PMroIVPo8VHGquJbWoPF0Bh1bgF7TAr6xWaeqlqkptvE_zZs7eRq0HcMgfXGIbXa3HmLn0cRFZ2ch836SsbmZrzN7vKIGHJeyKDZg_fTJE8HETNIcUg9HCD0SMMoAlJDgm7iDBroJMiwJhOo0UCZoYOgTkjhYogQ1-pMyDrTts7krjMF7EwUO9NJjNw4YSVW6FSDdI-Ug_IkypMJaYTM-7lncfHzioufJ-le-Z9LieBKlL0Eh25DHoE_6MTYJ0qK4FneMqpDDuY8ghoiTZfkM51UDNzNKkXAuY3pjiSoOfjQb9Ch39iHfgY5CQFKKyPHI0uB-MhSR30-nBsOPP40HS_Tt2hig2PxfdEY15ukwu0RlswNiNptkO4RlO-hj0o80giZVDNoxSgPCPYnPSDdI6aHhXpYqIeFepgeEC5FbQX9ydTn_TJRGVfQh-uq0qebCCla6-ebiPUwGlr95ZNXLVh_WXCRDhS8fAdaEQMi8OVoT4_M6ZElxS4YYnHBEMsOhih2SkM0TnvKN-n0c8IJ6VK7SWewczfhzzpnBgNqjefMpoDYBKkdzzpXzUgfZrVnndpYAwbjGlsbimi0YD9X4wScNB9gYndcSZ2IiU1pj4c9z9K8enDtZu2-wVPIaM6TDnSxY6NGzFPDdjqB8cZwhlvG6baMQqxmfPIpUTSRntyBZAlqDE2CDD3BaKMWWvSkwWonQgoh3SNiwhDJhDRCwDiKFBVCukfQXM4MbSHSCOG2wFWIUBNiaj6o5oNqPqiHB43i2CO9ZKXu0JZL8gTIAFyboHxLdWsXCqEwECmEdEJkg2mg9UGdr6XZUINe2j1lxYCbAGy3VNebim_ckhZ94ZbqepEUWl2XZFMa_80xadH666IJRg_Pz9cEuWCS1QWTrBhMcu4C6z31sUu03QgGAL3uG9X1aqBB1ifTKtQ7p9S7PqGgLeObVeXbPNUd9W_dFasYtnGVvcMjQtmVoHaHEvRRDCQMSJjuKw8Daqs27drB1jte4q5QEtpT31TnqAuqnpSgNoLayFSVJBw0qQO7Cx_PqSa8I0L4IR4ZTgQVhjpC-QFalmmFDpSCFpoPkNRloMIFM1efQXaybvD9-mTdnttTurUUuUIiwBga0klPviNGW8m5Azv6xbtDGNoOrLnLYB4t1BiaCJ0MUWDEyoERKwZGNHXZXBRtsnJoycpBIysGjVSIwlRWE-kRc5mVF6gTZKQo43fNsBMWGk-5CkOdINvi4IKDC46HgkaUc2XoBGjJcHebsN_7HSn6od-5-lEaWPvLc5eR_zGHX4LM7pF0MlQZGgSd6RtaN8Y1F-p5SaE2SeL9sbNA14uGf75vC8OYztIhp94dcipeUli9JBao3qt5SaR0HIuycizKirEoRTxKIkku9MVaKWihRAJZEoltSSTchT6jptUzZKsvD5BtsT9ARitKYQjOL7JMan5LC6XoG1q4ROiVekrRT2nhJbn1TJttC7RUFnZ2CqGmCVQJWsqn0Kr-ZKgyNAgyiiy5IG6VdmJ5K4EOp6EC-3gaWkEMz1DNpVNLzQdMlkIH9AA1kONnVo6fWTF-pkhnPUgDa2aoMTQZOkjdamaoMcR8qjIRq0zXKpO6ejxARnVrZqgxxNSvMs-rjaHMEOynz1Ig1__ofsgFIa07CKnsfiS46rUvniBH-15PxYisV-A4bcUo20LcG13aHnxuFkW7bANWk9th_t6ASdE3BqxOd9z6ewMmRT80YBVGn_J_LrNQOy6EwSZgouZNCarV_lYdvNSTA5XWxkS1MZNszCQbE9XGTLIxk2zMJBuzv3Y-QLYu4MENkf3bnak0f6YisU1B5FeR_QaK1DkBuN4M32_naask8BRnNss6vH2aoEXfiHfrr49tpOin4t333FhR1si3BhuIiRz36UyLrB-dJrQRSzzvsBpvgRrvZBrvZBrvZBrsZNJqMKWbNK8OLl7crhsY576GsnNuZPH8tY247hZUee1zB3OtLphrdcFca-9OwDUe7BbwXpyA93NaAe_3OZlMu8bg9QLOgXEz9gotffLjWaK1an6lC1L0M10QadO4vyiBEuDVYp2Nbk9O1gpMK8O04YJxG1vMQYBr541PB7pvoYMgI7-dtzSdN1GddzmdNyudtzSdNz6dtzSdd0ydt22dt20d9kIVZnA9jhfkCgO23rutMH_4URHtOqjUjhQrh7Q7VKxozLhoM2jMDokqAj-IFmm0XvOBWm2LKJAJ_1tWm1tVRHsQWtpDT4h_rz30jPj3nmT145UnkaKvPImEIlb9nDDvop8173x6RHdCUfcVZCkNurmkZtw7MHeAwUGMqwliXGGNjYwP3iEM3kcMpPAV5Mrm4q3F4F3Q4B3C4H3XgN0Zjl0Pm9YsxVvMQVvMA3KbOeJt4eBN2uBN2uA94OCd4uCN1eCt3OBd1OC91uAd2cCd4hr61KPi4GR8niBqcVpzaRPSMp2cz5OkUWrTU3O7LnM4eGA1uC4cWVehytBJkDljn3xeP_Gy35rLiYfzCJkD9YmH8wYaBBlJmLwpmbybmbybmbx1mUwKJ29KJm9dJm9wJp_XT97NTCaYk2noHaOaCsJR-Wyvub4U_cxCLzPbMCrv5e8aRjzOkKdjnrIQRyLb0R2-fKWpARHUhLkr_oblph-UUGJgLL-WaNVc1hkHNAISrtBEqDDUvdBLQRQuKWghIksKJS7IEGpLM6GPDVQZOrmrQfUnQ5W6GkGDofPmNG1FWHjDabToZ5xGWvW74w9a9bvjXzOpr2l7e2auRT_T0waLVfZvd09CMkwA9ucxLi52qxbslVTOkYs1DBa8bITEHwaLsJCuTFm0XNGts1JXs_6u3XGtjb_T3KiIkhMV0QQhRsgIfDpIlO_w1pTr5FwMGaVORBg1V-K6dlDVlupb9q9F38hs8jv238ts8jv2X_mW1ep8rSlS9ENN6Xtlri_cqrS1WNoaixZRXIUSQVaOGsskXjmUjsy4I_T105aIfgtkO0LfNVXo5IL7bl1z8cObix_eEjxkmLsAGJfkvh6iYwDTouHs0dwkPiPXkgtbq3-erw27FH2jJOd4e1dBi36oJEso7hDlVihMMPO1kCfdY1GoMjQIymjST7fjkHUV-H4ZTTjWed8EsrtrTTC9pM2BQpWhQVCGL92284znhj4MK5Bt4uSO0GZDocQFd3zE5iJ1Nxepu53wiGLuDKAfELpbEXivfWnCycfuKgYWw2P3JYD5fHt9QIt-qDFLiiSg9uGkaMFjC1GGzHBM0XJy6ytwcfCSwtwfpDAzp8_M6TNz-sxEOTPpzky6M5PuzAw7M8POTPMzbwYybxky8_DMbD0DNW_Q0y2qLpB6y55IZrhwC6sJsgyB1UUiMx_Nay6LWbp5yVDR5wtWhgTer8WWGsEiMSXmmcV7_iVIJT8IUmECWjJDTBwKs83CNLUwTS1MQAsT0MLMtTAJKUxCTNB1hGwnmOOU5gVpx4pvUA_8diSieBJRDidYxZMICCyvCF61HoDAkkJGO2nMjgrTqkJXjLWJCYJb7Dl2R8E1WEBwirkEsEbuL0v8nmoUf1niN1RDil6-5JWTkKKfOYklBy7SeXORzlv1vrNWJycrB8gJhmpfUlE9URDYcyuBRyhDlQlDZcpVmd9UJjOVWZgJFW-gynVVrqtSQaOxlY5IpS4j25VuX-skGQhoXSoLktBhY4e_EPmuAW2pAW2pSFuWWNX2mnJL0TdnKTvuusgThIFXpHgJazFVaZ6qCExnldCQkZ3GtKExR2jMShqTi8ZEpTHDaYWkojGBakygWn-A8NlDw7j3BrItAlFZj0pbw0gH1wNVA6XRoasroj0hK-KDgerhkQYzU7Z8toDENCQx61Fz22H61wNNnRfk4-14zcel6JsdbPO7gN-rU_O7gN-7lVZf79al6IdjXU8K-k356ElBx0fH7hGcJMZX7TXR34U3ieOHxOwCYK8jKY1-36Wb-aKy_XoD7byquKTdRRxvLuJ4G95TDu8px-0p1_yO4-31SC36RiBGfntXTot-eD_o-jRAG2qKvy1D9cgyA4iseDEWWk-xBtpkA1WGBkE5MdQJKgfnWh-XvoRhmp4SJJngYP70iGTR779BHoCWZLl46c3FS287Xrr0d8qH4zxSCMG5mPpwj6CDoE5I4WKI7KcWIg_zYspn3vKAiKy-gaSmBU2C8slQZWhHSze1i4pOv72e_sHN7FaJp_clv1fi6X3J75V4-jeGf6_E078x_HsPNl-_L6ZFPzyDPWENVNkm682MdKsfcB906ZaBLt3pLnp9d9Hru4te33f0-iVWWiUYKIUyQZ0zHQwRUrbadHwsfKmNQS65NshSJAtJ3WQNu3libqBOUIFI1jKLE4DsgeaBqUBenX79ipUW_VCiteyWCBGbwmKDkE4CL3vhZS-07IWXvdyLrJ16bG8T8o4Pglc3EbnIcN_fCxDxSd4LG2SJTyIvbKHVoUReWKHK0CDIyF0il6u5CkPXbF4crZ-HcrQlU6eEXtvsb8FJzdY3F_3UdGnx-lHxtIvXzK3_XriluG391ya7n3QLo8jn177-56vg05ijGxh3tKOHh2X9vJ4LJ3-vTvDi36EXXG4kAIK3_U0VNlQDJtBlPk0YDwk2TkKChLLPVbp8bQFKrwcycIui50rIIOTUceFWSIoyPBx87cqlEtyoC4TbeYUqQydClaHB0Oo2IYVqKlSRGLQK40RkEFKh35gJT0w010BoMFRhpjBXBBUuaKAToLU-653eTgit2ILNlC4IJ0fKEmLGLdAgyIxIINPXQn3thJRQDFdGHlThQXWGiheLlcmMvNDIOyGFxEJaM9NTeHo6Q4VnrD9BhQvivNbDz-JCaLoWbOZmQTgTUpYQM0iBBkGm-wKZvmbqayNkEpJDqVhFGZ7x6DOPvjE0Gcpefla5QsXMPGaax0bIJCSTjEknK5WzK5J5RRpDk6HM69YYmk9Q4YKFq0cZaOS3GvmtRn6rxX6rxX6rxX6rsftp7H4au59GzqaRs2nkbNrp17eR-2nkftpJC97YIzX2SO2k1W3spBo7qXbS6jb2W42dVGNX1thvNfRbx0Xj25Qdin5eUdatkz_rsT_r7M86-ioDDYIMdenJL2UHR7jG0NmfdfU-wTGvJoZnwJLoz4BtYvmpWjp3njtRKKtEnseUjt9VxPpKtS-zaE0bXrWkdUrsth7fZLz9uG-RgvWXBfeOZQU3hxZ_v1uRovWjnco6WXEB4LsLAC_pZzZBKyXXRs-zbzghIs-KZ5SR4XqXLwu4XyaSMK6lLXj-c-_5rwCdfeyAd6uLLiB1d2Gs-w7ejPn9QOU826PdotdnNzXv_hgBoCsk9BqlRsIGpBDSCUnhlGmjHu4O7n6W5IwBj_XltAKhdTZhMk2cvnrL4GhOfH-rMFLwE4WRNZ9yEcaEd-1w6K1IJqS5o3LJUwjphOgp0gkQnj5N_XqUew-o34Fa7bcCugsv3F144T7dYeiOiYvlvYxKAFmPthCdoTprGGNAMiGNkEnIEcqo9JDgFsMzlGhc226kYgu0CXS7xB6hJeOIiIzLRNdbVPWpwwsTrU8dPjtMugRnHPxKpSTUp4QRitrYAWQbVAC_h_t9BuIipQh1PuEyf5rXGkXt3zaKOsYESCXEuxftn5OVu1EPe68zL7TMV4ZLC37s6aUfb8RIi34oRpe5HAkD3t22cNxHyg7uDl6SldKDyKXylICxCwwkl2YyQPeN1watbXl0kXFH6u535MR1hB51TlyEJHnvrCNDxHvne26dxKXQO99za23ZMIf7l53S3mxjhshluEyWiWu0txFS-0s5T2_lPL2X8_QHOT_1rjBegllCfR5WqJcknNYBSwXwu7nf0_2OHLB2w6ORA5Yave08m7eSp_fI0opBvEeW2g0SeuR75jwcemTt9IZljuF6siwIsK0l6Sfe8O6UKcO0BhblzE8JxLA0gTiZqMqZraoo3Cy7HWdxnv-3yiMFP1cefYT0Qnn0MdLn28GRHUHIjiDkkBDkkBDkkBBoEONkhSmT_8_b_2N9BXIQH8gxH8gxH8ieDywBznCTWpDqRVqaruYrHXeF1dp1rLCb4tv_3UGqt9Dm021Axh0W20MFkSdSmIkUirTfoaal27BO66lc3fXWLWY5v9QJKfi5TuT2Wiek6BudKCGpKCGpKD1EHY0pjsaU7iS7EAkpREIKkZDiaQVWxrDnJkvWS0wHS0wei2eJSxFK9xIfIHjvGzPh7QYd92QICWV5YqWlU8LSt3LTVTROkn2qEhR3yjHKeHfKoQU_kfklGzUkF9VRlh0iVVa3ElOoxBSqZwFrTaXofewqp6uSmH9KbHHiNXXVn9ZM7IMha0saMERrN1Xs9aWgrYJ0QozVrEwtllhoENO6Z9aRvzqjmaf1CWleDc9ZdNoQIcZWY2pWY2pWHTVb0q1osmSnuqBiCk-a2id6VZle1V1z3Qa8nm8PULTohzuCVXS8dFRS8HOldeFbhwvfOpo7Z2khrWohrWrh2btUgSLT6AClEWFqMWFqMWFqIxIraecymgUAuVLM0FK1hqda8qDmOlTVlFYJOip9KzLDVJkPhpgEfK1VaoTX9wP8wAdL0mHiT0tKWnrNSqToG1biAqgOF0BVfnvh6SE_6SkUqU4spBML6TG56PHBR--RAGkd9ovM2jzw8F4IAVqRYSyBkep8QLXWVWOjYiX6Dr-HOiE7hMjonsb1TdHGwGn5LnNdBDWInPl0__zFZloDH_iU45ouDd0p_nU1OLbHXO0BsOoGQNoHJGGWw58mD0dCdrxR-e1c59hvPw_oMZxvDGItg1jLIG85trfEjnkpH6GnHqGnHuGBzHAO1PSahH3ELnvELnv405S1qP6h8G9d1_CPhH-7xxreWf7emg3vLj94xjECRlZAAFA7R_CE7YS8x1PCHQ_PtmA-oHgC1HbUW4QPJKsrzNcYdd0UZ-gEkzbxIOHKNPdJwJK86ajDdNRh-lOQSY5_kuOfw2nIDAnHDAnHjAnHjKnDjKnD9GctGfoRWOtJzlYT-Hjjmsl5eHcxD3QX80K6sZHX-KRg2b-L-73fbcVqt9nSphMg3lFq075ZN9lSkNDowoGizhxpxW4FtGaCvUuuG12HPZeXnHdsynspFDoI6oQURB4csU6RaYIc8Ty8I9YFWuu8alrWb-qNqzWLyfqruUN-yVwk734kS_J1-PVJkWuZKXItM4WbQK3YL08KnYhWTbDf9x3QEZnFpQl3TDTr06cLezaT9dla1fRdSdSLtNVcJ5a0Wec3SHjYa2rluNe8Hoxpwk11EFjMBpGL_Jgsy2bcAbLMmci8w4w52B34rjlzkbGmi4w1d2Ssy2_Oc767a6YFP96far-dZJ6RU9Che3k9vduRjAbx-817ep3cnqHTuKfXOg1tmg3IefsGm1_3gARNhNjjLFW541qB18-XyGWVxctuHh5ae18DrSl18aemiz81d_yptc75fHs1UIu-ODb56vc77qkFPxfLHLrEHLrEHLpEDTiWLLqPdUWyMnnsTB47-wP2WxKcmObYs2Z_HJ9hkIH0ZvSLeCCiKXAgotDhRH0Hn3Nmk-PEidmEwGXLbAIgZhMQEXIiVgW2o9f3JifErVKkEbLN71pUF09runha08XTmjue1pK84h9J_V5Rin8o9eujGOmFF9ASMoUSMoXimIJIUCE-UjIh5H6LPzzWrngBLTG3KDG3KH6DmmGYgcTt8HN2MzYLbsYINpsr0YzyxBLKE0sovEVcgqphsswOwIXjmi4c16zOYdfjpcOWgh_vymfNrz2AFH0j2DXaD8pseMurIcBSgKLlrcQbKvGGSryheiaARUkm68OuUpsKEvjZ-DKPGNls2UcT62wZSISWhbSZYOC7ryh6oPlLTBttZps_-5zNXXmfzdGK5mjFDtK0JKN5WvFbMW6eVPxWjFt7bZ-l6BsxbiGtaCGtaOGtQF0PRIgvNOILLSYGLaYRLeYL7eFAW3vARKLxPrrxPrrRPrrhPvoSse5PmWe3p8xr0rpz2_122ylfPeqXna7TI6nfj9al0AtplIKfS2Pv6a00StE30tgdh-nT_Q55Qw95Qw9PGHp8wtCJTXRiE53YRCc20T0RwOoZjklG9yQjw-ADYe9PFKA_UYAeP5Se62FKhUYHR_I2CXSePMfD40ZNQNUbsNVczmHoYcKlssduyULnA4TBBhVqDGWGDmqxU6bCxRCphIwYKVSKEduhyt2uPN6nubNTMKD6imuZGZoI1T0rgsAtvLmyvCR_UvBzOzX8paPf26nhrx393k65yHvTRd6bO_Ie5ve2aIQUcoQUUiXeUkgVgm07BlHIQRRyEIUcMYUcnkJiGxteBz4DT5JUHABCITLnTfOOlOp8tYmyJXk5pLtJYKsz01MCXX9bFsgEnVsqMxNDwBAGlptcLoC4HNWdqFQhREn0_LnjBzeXGWrchQA6GApaxCWfFLFWClpoJGtL5lvOM99ynvme88z3nMfFUpwuluLcsRQxv7caM2TsM2bsk_j5JH4-idVPz8GxMoY9kceVLyQehaQDX4Ga0x_pYVl0STMRQhEjv6c_NiNfKfkxZVLK1bevlOY6Z-si03OXyJWxzYXubmaE8H3NcWDPssHg01ommyuaAww2NiabhdoDVLiuAAqbzEEDOejHEeQ7ghlpQRstKDsZi6YJPdHdBq5uNSkVoebEUnMFEFKzu-n94cfvyt6xrLvkx6bxu2tvedZd9oVxvAUIgOyB5oHpgWjXeMs-wdG-8dYeS8u2NCcH--tbN5QQ8jvKWwQM5PemW5Wtzd2yS3i40dy9t0b6nkKWwwCKJHga7AiwDJg1ypFCHVsvVqzJe-6AbjqsBvmuHWo6El58xadJd5K54ir9Syf1T1P48agYjkT7TcWMaU5-w3nXeQRYCzBjwhLtDW8sKMpQ4YKFRxBA1mom2o9qAw6jHTisA_c3B_nMkifYmisE0cUmFHxjRKXkCyOa_GWGD4xo8vcZPjCiyW5X7zUFYHgg2rHeS0pwtGfdApAi2BjH5Letdy4D-Y3rlidv2VK4dd1y6i1ewuMjq_CNDVI6A6nbxyPX3cl7BBlNlMFMSUPzTtpx3phRibM8YjnAWlBfhBnI73Q1V-EGAshaoZM2kTcW5LNmwsyJTN3p6b42G0DGIJz9ASpcvXVdJ32TRRtwGJmcs7971-Yu-fGtkXupACge6B6INpj3GAmO7prsBUxO_06_rbxXx0B-O7or9Ap9htvPvUz7Bb5JEn4MEo9j_CweTvzgmESWKh9v33i5y76y8dkT5XyEC5MPMr6ZCWYOX3-8s1aY0OyeenVA8_VV3evZ8Be0IqmK7hfAxLhINohRJ1wKy5f7VbwyHqtYsRSBj40BvVpxADUvQOuRtcXEIJtsOPDr_oUoZb5ejf2awnn9_3dc6v-c9PWRjI0sOrkewSy83Pcm_QQUCfD6PQGcuEIVNpxdwTLOjUS4zT_WcvhJDmu6PsajNroAprZc8p2CcWtDU8K-rLoa9SVqAz_Up33B7_lpPv0KIrd2X_p1rUW1XF_iwXjdIl9lEa1LxBWSQC8IbSp-D5UQrKlSTSqpUPCGomxQmwg0lFQECioE5XTw9zfowkWr12JLfExZCMGSqaXqFD7XIlEzYTHCmq4QPMWYlYVlYxRqEoxb65oS9qXIiHxfojZW6FejhIJlO3rBuLWVmxQwrOVSnzKzF8N6ObRWqocSytMj1ArKDmaz2MonooLZDLRyNZMLkc265rKrTc7veXJa0mmLFkrfNc0w-Qv7miLMV3TanmspLbsFCGtaO8ExsUXZHU7MdwrGrQ1NCfsie9Tp-xK1ce0PWzF9WVgy-Zpg3NraYQ5qDWoRaVpL1HP1EFwGc7kq5VJhesY6bokwm4FWLhGnts-9G2aSr1cZyJSDYyKF8LOuC1oxV1th0eynTmu0kP16vN06Lo5gZXIthRenD035of7kly5s4-JLKACygivwbG-wqP360MiycSZXmbCopqBAaRtHk6tMXGbMdmNRPmhB1h6LKgQlDdRxWTucVFmoMjQQurQuo_Fa1VtvN458q4uuA2K6DiPnHyRGSpiVW9hW0LtFuUHWsUXA7vIX1k6DTckX9yJrKqWs-lu_-yJ498ZbpmrF6M2omRKG-GAINVOCE-MSSoBd-CBSOlYg1Xqt4XUhwUI6V4jpXNl8FbFhsNsxRXMlJW4CKfoioXLRyyKkEo6YyK7JNqGNcZoN4vdZwPzavn0buR_OEKSs3Vye-dqs1B8_GPRV9vrW03i1qZWyn21q5zUrSaOBXuwgAVRgN2ET2lNCfko4HhKQKtqE8pSQHhLwmz82oT4lnA8J6WnkeE65li2tpywfLvlatm8D-_myadn8Wky13c9ETcRlBfeUD1vt-ZFIqW6baRPbT4n5p8Tjh0S_p7CJ5afE9EOi_5yATaw_JZ4_JPqNuE10MyTLla7r7u9ETMp-JiZJe1V0rfX4xCak3dU7pWsKDsSmFvB1y4KndH3gpPW-zbXB0jgIqwyJn0gpC_bVjjx71JSVO32nmEePktJzZ6wF2GRs1ABbTEAW46KK43tv9Ht9VyGob9zDIYXXtu1LvOSFdK10fda0vJKsVTZ9JFla9jrcub5J-vlErLLpM-N1sZUkkXUvnjMrQhfPsbkyQwdBi2xZqGD1i_uZuhZ0EFQ6Q4WhRND6vK2FrrpU7yTC8KV3RiElYWRS1RWCNlJxCU67lHhpl22a-5wTzxIeMEmd7dq9W4VdX0JcqjyhLYkMi-smUGPoYCgTZJZSIFxKCUB64sSu8Kc1k4WUhJOnbwU2rXk-pWxza_uS9g5Rqm_rdELsJWJqL00-mVHp2py3CaxB5ozj6CnASoB1xkYOsMN0aOzFR7-Xryga76yTlH1lnfJFj95ZJyn7oXVa87CiiFaUX4EyQcZWCFS44MG5EkHGfJREulAS6IKBEurCCmn6fex62wWXlO99uEvuLnkGqZtVGZtTlK0RVBk6CUqTIV6AxAuQeGrhCCethyCiN6KgiKmCmnyiDwWMoVFQk1kUSrDOmCiZxXKAzQBrpkOHtLut85Fg4N-vCAqxEFGuulHfcyQhYydDjaHM0AGmuK7viuOiV_3UOEEnQbYTC2oMZYYO6pfRFIEKQ4nqsgUXVBgyKlazUwWjKJLKWrbM2nocZHyVSSjZ-Z3Vg5Nn-eRZPnmWE89y4llOPMuJZ9l24iD9vsP4eiixvCWWt8TylvCBXFovbvfrUMLyk3pu3cDJa9eRTL30zUIHQaUzVBi69F40QOpqlCsPhipDJ0HnnCBL7RANY2stSR0FcAlSy1enuhckaYA7e_L0nDw9iacn8fSkRBBPhSFC68FQg0WVewOactyLKlZ9vQvd8dmdwdSqm3za2jKV14waq24yixVe2DgCLAdYY2yeQb5pOtS3TCPtWqt4vW3zMe2SsuUN7VpP3GZ7Q7ukbPmIdsmidn4gazBdVJNP5lBe5uVFNZllERbWjwDLAdYYG2eQzyzqeh0ZDZXMkLzZfBxvlnUV9iLxu2VdH0uc882yStn-0bLqVKyQnm3Lt6y3vFFr1hsxXW-TT-s8H5TYZJb1GScrsWA5wBpjVokln1lveY16DzIlydzEb14JJhDhV-J6OHsWgBpDc0NnKTCpgq2LbFpbToi1AJsbkwXWiKlbKvety5T_i1SuwiAdu-z3uv4olaO-l0op-5lUypxKwFGcZokoaqCBM58hW0ltvzVhcn-nyHPC1LDiXD1-37lePFHiE3UstISxzwAbiJWE2GX9w3zr_oZ24H6YUqADWqDPXYkdCd4c_wHT63I4b-Y6ms09uAY5nBI2MOEs6M5HT6lsStgePUOSp6O2BdEWjUCLGqThag1WA0yeut13FVI25dtWhOuo4lMlEkWYXgF_owha9mr3peZLw59pr5Yd8_2A12XKDzu91PSUcLWg56eEojVQZyhZ_TaFGe6hNZDscg_EYqn6vO7hGVoILVTAQjhsIHYmxK7LN2G-NrEDXTvAFuI82vT6CD12WgdVUUoiTV03GKRLciE143Qba7Hnj_OdjOHb1jd2X721vQrejlbReEzBkYhlMSOxmL7W17FutCKaz2LpCdvWxpYP8B5ap_O43Ec5isa8OKT6_IIETiz7xlB8ddO1-3tDca7XcV9ZRmn3nWXUhj8bsFiOdJDpSUQUz0RE8UyToSM0ThLymuAWwzM2ZRp9uhIG6qS4vgUbmTKNJo2mzGIDsY6mLK1ThyDfmdCUJb3iGJmypLfzQZmTHiKSmqf8mDI55cRFne7TA6ZG896AqTFMObyxsbVFBjW1bYbu3LiKFhuM6fPCjJhxAWn61tXgYW6LaY_MXDV4MfnOji9Yix3TqOJo2xLvhVR6LDYD7IjtpSgN4-0Bn7F91fxqX8VkLEF-Z-dWle_snCjQOzsnDb-ycxIf3Vubs8bwiGHayEkFFsKNXMaSkRafT5sKqTpM0ZdqH8qEKXTLzdYW6fb5tI2S2kyKCOLJWxjtlcV4W6OLRMItq8S42_KInJzneyd-lvdOfLX7Uril4VesZc3DywGv9Xq3vcnxLiXHu5TcY5j3SJn3SBn2SOpmc7C9yQ_bm_zT9iYH25scbG9ysL3JwfYmB9ub_NP2Jgfbm_y4vcnlSZc16jwFU9JZnn7rcX9pgPLqu3kZ2pRLkNQX671zsEXKtF2xKThOdcDYD4tZp5yDjUwONjI52Mjkh41MftjI5IeNjNbdtmqt147fbWKk7CtfV2LCXWLCXWYMx2S-8E6g8E6g8K6iwK5CVUKDwVfCmNMX_GKo19-SQShEfy02EDOcviTm9NIxw-mL7hEi_TXvzubkeuwlvjCbFs5c8pb4OzfOksUGY5YzF7idaFuP7EppjynBXsOsfjTK9nNKZAvKI5_HmbGYOULV_hgdLwEZLwFpLwG5Lw8EvTwQ9PJA0MsDQS-OoIu61Jiv1piv1pivVuarlflqRb4qwl7v1yusFlY9Ko-0sA4QQNFCiw2sfKIW1uv1yShfzpM6pgJ9wjDzfh7mEhLL2R6H95P1ZrbWz90zQrVYPc7QstVjDcbPXRcdsbtPrMJidlN7VwHCVgMSXgMSXgMSXh9IeH0g4TV-7nC3t587fBHL18fwZ80vTgS17PHmCF87nV8cJ4pqtZgft5gft5gfN-bHjflxC_hxu195CVxWO-Cdfy1w08caFBjgKHNyLfgzqFYgxbx1oUlGhxtS-YxDMpEFTEqkeC04xddPInB7kXNqTKHNYCPnhK1aTF5zNpi-l9uxPaODLSCvLSCv7YG8tgfy2hx5FdnpMdfrMY_sMY_szPk6nyR35o99suT2-1ROBiAupuORnLgYiw3EDNGTjwUYUtfvs9BAQ_oJpM50zFLWfh_RWmfZ9bJo5Cw7vEpyj8RgA7FqRnJR1jDfNKNr2oFodBXeETcjKXh7BUbh9WFXTynz5xR8b9PWFqY8Usz-SDH3wnrD1A11NljdV3zs6COdN_WIfvWAfPaAfPaAfPaAfPYH8tkfyGd35FN9sfahIXaExLP75y9maNCkiMy4OZDD9UX_SPgH3sIQ4bfYQKyh8I_riWGULx8o_OOmsIHwjz53JWYk9tRi8DWQON_5JPB7HigF2aOVuvuLE0ESnf2qQOKXKyxm3eqeGO887xqCZh-Pjwe91G2rk9AEpvdo_iOINyljPBQkPzTiLdLwF6ZErkdAgUdAlUdAqccDVR4PVHnE59Xrfl3Pb2jxuHwUlP2AFku77Q0rlnbrm-Pq0fr78V5Bc16Od7X7bryr3Q_HK9ZmfYNBrIhYm3mfeqJ9FJ2STwIERmOWx5T0c0qkuzNgwSf0YKVECcGFlEnnuLaZ6En1DM64TUqHlxGaGQ6agsn7ocl3qma8G5vxbmz6O1WixDPg4TPg6zPg9bPHhmE-8Pjpefzyn_mAd47Ef1psCVk-bmbKPjAfGFJOsVXJenBhsTNhxfqGk_X6indTZ4EboqZj8XXUbEKL6-gQ00ruL0dEoxtwpVSxVcmYjBUzivv7Xm50eE1kybbFluPL96cOvCZoSjU18AcwTErgWqUe49d1sTvpkl0V0xMTYkl7N4J8iVsKTI30-IeUMxiLvu1lT9y2jBEWzlX2Kc0UAUOgeQ1E-1JdFwOFG2Wpj-Fwoyz1kmHRjmaD8Ubhlk6D8cZD2_FGRbvFeLyp0EE71vKVPb_14vmojgH83otru2-8uLb7yovndB1Rm61_vkPxR3bMxOQXO2axgViZjB1ofu5PEjizlPTIGstPOCIw-fx5hOiPqUTsQTJn3idWYrQyCOiuKZVSROvvby44rTffXTBYsLuS-sOUpx2ZpEQ2KtXHFD27fypzkt1JtK_R2TAQ7WukIJmNFO5rpAK2Jon3Ndq-xXhfo3WSJUjxvkZ77C1ESo6rf2Ah1v23dxZC2n1lIaTdN_sambaX4x1v9mKi2Hfo_IgInQeEkxbLdELAB7VMBtOK9bw6rDjDAaJiq5I5GavcYzoyFdwcQO68YIVOfO5gTiqkz5ESn087pHw-7bfy_TEHTimPKU9bJEmJjNKeaWdGTtrtaMsGot2OTqW3F2e425EK2IycvNvR9i3Gux2tk8zFGT-duBf__v7lAQL8oSkQlZSyr9R53ch7p85nf31Moe2-Ml_S7ofjFRnMx5MW5IB2L0XMSLutIma8gGMwc5dFMbyVLZgYHuqIUaBj8Ym89i7l-4X-VUQkO-P37lagU8VW10w-MT0m3zZTzUwHals-Yv3JAanPMaHO-b285fZe3qTdV_Im7X4mbzpl5lXjE7F8r_u5fEge_T9XSv36C1XkoIoM663FyynF23_IwuZ495b9Ns10sZgvmble5pH_A-dWtmcYF9X3L4_zP1iveNC8nvetCOziwkVDFtZngA3ESgcvK2L9tSKBW8_ruWBQyYkPF29MA7orsd-4VqwFDm0Vtx-mtlwzGQbBrDY31lx8zpALk-_C5Lsw-S4x-S4x-S4P5LsE5LsE5LsE5LvEDxW0C4yHDxVy8Q8GMHzDf7Empbqt_QfWRNrd1iR9mRdt-b9YE2n3Q-8lDkDXV154ut2ACFjFiyGK3bz5VkhN6TvFJtwn5vZEtuIdE9Ga2iZjRfb6u0lNOdPujBgOW-hIzhuaMVls4Hlkxbs4xrFXvoqjWOda1Zdjvojm1kcCjPMapYR9JNIs2leZGlemxpWpcY2pcY2pcX2gxjWgxjWgxjWgxjU-8NcuMO6p9JTsjjJ8oOT1DxS1eor6gZLXNxRVnmcb2bSYuAQxBjJfZ9pvQHqD0NSPI8Y3Wkxu2LCK3jeIvKrqKthkHW897fqtjkuheuu46ZLRJdMlcz1JsRx30z4NEWw8ddN-x9UWKt4UtceD-UYH83cKD83UFpmJ-2slD7WFKffDqri2MKU9pjzdh9LaohSWKzlibfjxcM2NC2swa-obf5xLxShxvmXkm1kuNIqNH4o0fijS-KFIix-KtJhWt_ihSHt4KNKC_ZN-ZsZgwUORFjwUafGNKu0t4w8PS5p_uLKM24pJ9s4gt_76Koe2-8ogS7vvDLKRXzG-KiMPhrfDybwxO-arNSbzshFBZvPp-NwfT-k7ndLfKfRZCFtbZALuL9Y81BamPD6P6I_PMPrjc4_--MSl8xMXU1vwhLTzZqnzZqnzZqnHm6Ueb5b6w2apB5ulHmyWerBZ6g-bpf6wWerxZql7LvSB2q6Lke_UVtp9pbbS7oebJVGroRfoESukCSY3b3oGfk1Y9HJM3vRIvmDTc33gIt70SCHa9AzetGkKD8CUidRyPG6NxuOG5P7k0lNtUcrjyf94fMIweIMlG7pxb7C-6a19gHov1ta8wRulwRulwRulEW-URrxRGg8bpRFslEawURrBRmk8PEMYDxul4TdWU6Ty_UZJyn6opOILpZ8_bUQmv3WpKUrmnN5NIHSqLrOnXYvVJMl--q3FDPYugrXJnQFObsQ06L-m5MeUx-3CfNwuzMftwnzcLszH7cLk7YIo15w00BMT8LV4wYIblJpinoxooeAN3BNT7HNNk8QXInUyUXEnk_nJZH4ymZ8xmZ8xmZ8xmZ8PZH4GZH4GZH4GZH4GZH4-3HCaD6R9PpD86TYFovjzfHFCIgZHyr4j1RN26xYzr5htaQGDo8scG5ty0LuyS_HLkSgChWQOLg6X4ynOTOGvqdnaAmsgtYUpT9FxCn_nzbYTptD7us0MFBRBR2ggYsBS0CuAFGY4ZsDafDYYM2DtQICRTGsXGA_v6pTr3dh3DlKqfMWAtd03DFjbffNw_2v8rx-PaNkP-ywKmPBBgWJ8Qd_kpvcgFcd3Hr_EajK2WkvEvEs6027T8AWtvDu-UBK_E5Ce-q8pwc3_BJU9p4S1PcWQKfsTfe6ATWcAbxcoNryH12r4BkS5vx643bhixmTi8jYzO2g2ErFvHbWBQvYthRmO2bc2b4xGYvatHQjysRFJ8eML7YNzpF-y9uJ5oiiqlH3lSM0K3Rg-IDtwJX9i6eXEG_eKMb-1uT1zVxyZu2KNdfQsT5xeC3lOX-7PsXn_fg727-dTJJhoZJry9DKulAlTniLhmO5avT35TQnFzgAbjJlDcGkpsh1nfkwJ4vRkaGGfiDQzUFTik5i4FjYQMXGpi9T8DJm41MlwzMS1o0bZT2bi2lWLMRPXdsggnDET11EwHl6N-lLM9-zkfH-TWNt9xU7ONzeJ1Ujd8kWYaIQYLpWuB6OV8W0DxZi6m9xMNjJeyRdzIjUHxGJdCQuJhVTU3ZFe1CVNedyBZI69Y8o8p0RWLfMORDQ9Y8AhsU3ZvCth8vFxoeYefEVyr89OambgaBAy70gy70hyvCPJ8Y4kP-xIcrAjycGOJAc7kvywI8kPO5Ic70hyes_Qc3l9gUnbfaXz0u5nOj9RqOSbdAaaBMn37QxUuWBGqD5B3KJ8ZA9z2eord6JyV-uuS20Yao7FRMMO1KYfCVmBTYcxQOWgsC2l8M5Bc1_vZkuKmCvRzvK4rSlP0YOkoeeUyCyV8pjC2xqTEpmy8rjlKo9brvL0fERri1Lo-Ugz04ZGqfB-p_B-p8T7nRLvd8rDfqcE-50S7HdKsN8p8dMG7QLjbh8kki9fL0WVuT9o6iGjMgI1hlBvpTeNoUmQbbFwvwopvHR1cPWmE-kJagTZFhNXn7iraQ9IjYKKzINBuL8H6egLfjpStV6w7LdPFfYCxkzU61qreWBa8DOVruI-SV_q45aq8i0kIRr4IdE7N7Yq5AO_QWprjWwHfjhTTz4qbtEME6m8W6m8zam8zam8p6nxnqa6PY2qVw12KTXYpdRgl1KDXUp92HVUt-uYplvZfnnVJs4fEv03W21i_Slx_NRmc4koK0Yhzbd2DTQJMtonUGUI1VamGJEWI4WQTvUwIt-ERYgrspky1ZSpWKY-Zq7ooIoOquigiuArsspuUDUtZhlKfYqcYFMsQ6n3A-KHJPvk84R5-yHFcKGBk22-kaZY2S9pmWruyFvWnjTe2TTY2ahOtmAH0nAHMgF6t0v4w4W78ocLd-XVhbtppuq4HZTBewyXGE4bLjjnqAjt9IqwkEIIBhIVKAcYRIo7cKWtG5XyBx8vtOuBkdUGU-j7rSDr1FtlBdIiBeLAifjefXdi3e7bhXTpZo8NtPzOT5h84Ns0N_MWkO4fqH0g1L28F2pp95VQS7sfCvWagkG2SuR00KGtTRgPCcilbEJ5SugPCfi6nunu8rQqY_LlRgPV7R9WjfOWIMMz7i9YOnjE8Al-HiuwEJ8ATJBRCw2CcmKoMNQJKmg8pF8M2YKVq6_cicpdrTyg8QDZ6gd3YnBXB7cIwU-SWY_V1Wvhq3wv70IqIIWQbzsui2tg_FRwvT8OyBgyiCqf7jNQ39DAFnoEGTkTFMTvhFbMkaUWgGOMiTgEV5ACGQuMoCrcXGLtFqKtsXZ6EISyVO8PR3oI119zda6-IlQ4V-G6CrdYuF_9CeoMFYLkXaa5oOP71mKZL3xIlY8yzhc-RNq9Xmb81IdIuzW9IEY1HSRlkpCfEg6SqJQZaiRk6aAFuL_SB1B7ypU4V-Lqg1ydICNk-s04qiuAbF15Q2phUoUDD4udjJmXlRRrAZYD7AC7uKBOSHHW6f5ooLVOCXmBMTYJiYGkFEwxCzV5juB1Qe0D8oApUH6tdavsh5ojLufc9EOmTb6NRsh-vGfhCQM7bwZs5vysMTwsrKt74sNHi9UAQ5t9EpeoJ3EJ7U-nghFUuKCBiCUo1AkyQnASHdOuGgjomCz3mZyB_v2lGCn7oaHUsn8Q0fONiF47n5o3KRJgc5O8erY-udX2rOUUCpz_xJvC3cID21liKIQHoe_QKh6rB2fL6b9hNcBQqDOzjsy0ILN3z8wBMrOOzHQlM6nJzE1yJ6HOzL8y86_cSajzH4Q6vxFqsX-lOekq2UvX-pBT24avwB0LCzUnWeUIBe7-4JaD_d2vjLgVndJYdArcaDLDK9TT9R78cnDy4as09bmILG9htlKYrRRmK4VJR2HSUZh0FCYdhUlHAdIh3rsEPjphij_IUtyfe9UCe3fYuiueKT9zwcJcsAAXPI8DRebccacm5n1lZst1V-8d_17tvuPfq913bkUiLvxmvCkcdJ3vB137rwedwpGvxj8c-TpDrG3vuxXpt7qeIwFkMlGcQlPa3jrESvSrFZL7MtUTkeUbDHQVbAx92xyxUqb09yGqPiM7oR35clFmqDCEBdXZ5gArhIkDhtoMIgWnDGT-TfBWBW-ERxas74dFisATpd4X1BiCp1zjAKggAp4N8xhkOzsVlX48QEYq-35edkONoUnjoxfudEj3-3ZLeszgGZ4WNsNZYm5mwUIm-gX2VC_m4YAclgPseMSMwi1soi7pd64M1gJsde_wOiddZDw_4IfF05woGavNjCsi-_OCw980R55ObHwrRz_cbuEDQrfKvnMnPf96lxJrda8vmKiWzb_e5sTuZDX-4ciNqDs1zQqJ5gx4Uic6uT4ug6-HagI8MpQUaWrsJxSKnDFiujNOMhGjPkC24D6TMOMgw7C_fyQwDtF8Mcn00hqDMQKsPmKVh6jXfQbOjdFo_VKRwQYZCOkKOVtphhRZurQV3wwpgfdbART-oCfD87YP9GQ1_gc9WY1_SruWjMzkhUm-loJIJ-R-w8sI3P6sjoW7hXXdVi2GT60aLMSsSypsXhCmZ12mIcCz6W9Fqz6LVRq16jcOgjT3acwNFYY6Q9drgzCj7TiqR4DXLM1th7IfvbmEFqodQkNs4lJtLQqdkPzFQGQ1BeoGAiaDvfcL3-7vtDh4Ohj7_w2bCy-2VWN0mvnUihmWYRvSP5QhyTYNxGRDVyHAyA5pbxjPD_jhxFJHQKxTqgCT9ZXpb4S9HZ4b_N5kSePvTZY0_orUfE3_30iNVPCq92aFYIOhckIIanRCuiFQZWj4_YxCiFTfgVRJaBIxDMllofEAcddJk1ON4WFhW69V18RcoqWTVTgxb9AxW2w8YayA0jbjp8OzmYUT_IX2azp_sfEtd-n9sa6UfcWFtSvVI7jA62sCaNPXdwdgv9rO-7aF9y5n9y2chRBylgIZ0Ts3-7ghTz8kk-e7ktHz3ea_62E4b7s_dOFHY0XyxKcTpq8OS4-YEefzjstlRe8sFhcuq3UwXm43dkN9ezZBDsdOP7WY5_n6VFEa_4OvWI1_6ivM3KPw5MNLes6EMPfKMfcSkcn-xEmrQMSfQUnDBmHXn-nAScpZiGlaJpp2f0vC6s39HQcHTwcP7OKM1SnTqVPLB0NwcmRmx0LzCRpccPDQDVnXfCdPpMOORww3sjoP3WukTDLj7QGfP-Y_J-fFo6lmYvkXHOsOYKxOCb_icde5T-LsENCCyKPM9xYk5_cWJKc_WpDV-EsLkveeSBSjMI8rzOMK87hCPK4QjytnjHApYyUKc73CXK9srmfH4nW_uFuzovsFXzE3el_gqMdCgzpuoYATFvNmOI4mxE6eCH1Zf2B95Dxl8KR6MkzGgYO6vCcoS2l_O02S_r5TltX4H5RlNf5SWQrtHCoRy0rEsj4Sy0rEshKxrEwsKxPLuonADTHXrMQ199ctjILU-_GiUZD7AxakIDWxnNeAaNovTxis8nisS6qbDN5Qf4B411MfKGZ1lFTkpLZfX42NpbzO1_djpfE_SPlq_NWZaWvEIBsxyNZApOdBCdk_sG4N_LjITCNW2YhVNmKVjVhlY1bZmFU2ZpWNzwMbE802H6Dl_ByWA6xVP7t0ptDcVVWFp4MHjMU847DDtArW-AmlYEZzGp8ZyvRfH8PWtpU3U2Vb51S3mj8azGYSzElDu4m3JXU3jgM6s5OcDqTFQJ2QoBjWfUdN30PuN4uQl70OyXpF_oHPyWn-KlVIfoWH6kbBNe5nuPS9xvCwsA6k0mCJe3X_JE97REgJJsQItoZeDzCjFH08YR1JuvYDmYdUNybl20fKN35aXKzgWp_f2fAW2fCe3zOV1fjvbHiLbPhq_J0NH2nPu0KdofIAkXkaMR8YFHpExGgkL1iDiM0g8jOSF9lRCKFrU1LMGOhRGOoMBaRlBARl8JUqHaE5iNXg6AYrsWnU2Ooo3qM_5E3OjArspfNT8R71_a51Nf4H8V6NvxPveZDgTrxNtaZ-wnGAQuyQp3fImFMf1xllmPFzv5ktLAI7Dy_mkxjMpAcuMxPSYsTUQ4xqMnmZB0N0xUsqDyBrzGfAhGbAhObBajUDqjKD466Jj0ZFrTR2OaqPhjg3WGPPMt2DzBt3Dz7NFKYtMv04vRT1ozI0GApdvRRmOHT1_fC8RvtzIFQZohMbgTohaF619mww9v9af4Zl0pH5Kws6Ed6Ra1vo9LUtizkiYCeC54GngfIUmtBCeZY6SBc-eOoQWd-vRl5bX2n8vfWVxl9Z357gXeNlLbvGCraMtyflHA6-aUPl3JHJ7cnRElmV5AmDICjRyVMI7SwidIdFB5kD7DBYecK6gYh9CFS4AWP9dOiDixqLqPm8VdOxoVXU8pzXEQ0zqXA94wv5_SWFUPhTfn3zUhr_g_Cvxl9dz-jJH-t8PPL5dl8gunD685l-ZkIaIf5xmdRDXufMMRweUEi15KNOT3qkWoN4-iKVGTU86SBHarIQHeRIXQU15KQjD6nLQo2hGXsxmRPGc-zdVj34LEoX4c4qQnL6B1K_v0shZV-9ptIzXRnuGVy9Qsxssmc2WFijPBkRyeSrM_nqTJ45jxjphBgxynRBWSBjkTM9UtI-GfOe-VBDMUN-MjxPubG6-eiNjU1wbFkWoewJlcAf3IQPrVJ-fxNeGv-DPc5vbsLLq0P9jpx8YTnACmEinXfsVoDgHqyIdsF3gw8o2Akx5LqUJygj1J-gxtBBUCekcDHuAfeckEKlGDGqUgLGVDAsjWLlEZsGg2_UqVpIrNuEJlSWrBisB9gqO71KSXnGu8VFWMv4_YOhUNVWBX8gABiaUyG46SGSjBH84EFMr_BtBjiyk1qN1NX8BKG41vYEobhitEYDdc7ENXEPKvWcShWquVCeTjUzYjt9PEGZoE5IocqNT6r8rEP7YLH2iBm1qvMRM55KJKqgBxKRsthk9RM5KwFGKiV1Tsd2qj-n_4DtrLLv2E7TcxynIhpCknB1_CGOb-nKEjc6qml0stCI4zTiQY3OLBo9uFIoB5ihKi2gNI2ZT6OL0wIZTt3gwObGAo7T-N6yThvRZwnFSNIjsRK9QW4rENWYbw2yrMCu4APu03w0qk-5z2q8ft9__lh-u8Zks-LYxVt7mB7WmPzmRV1JoDj14m46n5d0uoAikM1F9KPzJRHt6eCSIVYNxjeZFcNLU9KG2eZ1ejtLSmaG9u0pW5quPOtg3MOb3kf-m9SuCt5InqzuoM-raoL6eL_sYztaRTIhjRA6Px_u_qbNmN1ZwqALtgpVhOjOrUJI3EdjaD5Ay72aghai-7tafdCJwbkG12Wke_BNXV0B4_hHQBAGMLcbCwjCQIJwrA3V_Y7wHTof5cXognSneJmXcKlzS-tYkQY_s7GiKqvshyZSy47fO4foGpdU8ErNllTPM5TqeZJA3ZFfCaoMGVGZ_EAKIo4qUgkZD3m8Zs4Rj6HyGAZDlYdVeVh0d7ZjbFULVYaMmszKoj7h0EoZhYSQReoq85YcRZUntK_kd5V9J7-z_N5NhPK7Kngjv1pBe6-5q-yn5OaSuQEBchXphBRCkhPggRkzXJjTFD1s8VdaB4bPNRCK8jjKE1QR6k_Q4OorQ4OrRwMwDj5n0TFH2DRYf8IMY5I2UFkkm4WS1Z9T4Fz_JMdSwXs5_nJV860cS9lPNXgdRm7Zu7AzwAphIs3JP8YZyT_GkTz-FQfJ6F9vGgkeFQniX80XhGt0r1eJOCZ_QqNNI0J8TSEU7ERUSSFTkNjTSJMhYmKaC3cGghUD0bMk6UUAJQPRNWQd5MklHcYvb2l3jQqm_WxK9S3RMyXtm3-_Q9elO9WUt2Xfq-aq4A-qmcp71Vxl37mY8_TKdFZC_DtFUorU46wxPEKtOf3TLy2PiD_1kVJG0E_iTeMk3qQFKxesXNBoCIQIvqHKED1f0yZPLumwwXK-8hmhlsnwwnvWv_EjqeAPwnv21_xIyr4T3py8YObkJHBNZpZHNeYOzpC4rPYuuuQlIc49FOLsHx0JYkQvM6nJTJAyE6RMr66PzIdFI9NZlI6CISOgmU6stK8nl3RYQI-k0Q4vDavs5h3j54bgXSZBxvybJGcfmPPDneqAcLiK-MOcUfxhjpZqTmR8lFxbnmA4WRqINH-mLglGLArTi8LEodBx0Ch0HCQdjKDKBYPqB-caXJeR4BKwhMIHPTrTFuMnQTrH-OBUyhqmXiZDB0Or0WL3w6MkZ3c_sHqr7Kv98JA3MN9rCb3_-amWVH-MMqo_ahnVH6xoHi_3tcawi8IvwlPpgEgqsNB4gipDg-sKIFOQqUTlIykdsBHMWgNssLDWfSxzQ6eKIVnXVYMRWqnUC2093vPMVfad0Nb-R9O-KviD0DY6TGnJSd6a5-bfXh7NB8yTPP7tecmokTOM4DbmAI2ZQmOH3wKHz4GTR2MO0JgDtMTi1wKP3wKP3_hR02jAWVTymgt8IwvYjvfmsh3vzaXM1XvJaz744qf0uM33Otfmq2OXEwVgHX_JUYzFCmEi4BirWRB_w3Z0OojpdFizIyAb6x4HOh4-0LFp2sc_HhC7WRF_MWZQWOhBYaFHZzrFoZUHh1YeQWjl0fk4hSPVSv0Womu-Iwi1PIKQzCMI3TxMiGfxIDt-MnmQzmcoPT9AFEBgUMBmgfvfnjxJBX9QPVmNV6q3yr7bmQ46VoHIv4rQsUoc5XdQlF8sT1pBoYkHhS8eg45VBh-rDOZCQajgMfh0hEMaDw6PPEzsYFPSYYMleezTEQsZ4ZaFHO2PYrgq-IMYDk_ZPxDDMd6I4ZqTSS88j0lnJhPitwzIY7_gajL7Fz-lIf9y0ph0OgLRcxVh4jLpTaExmfFMPs-YfJ4xOVzfmInFeAaPlCZQLzV1c1-asRBbxenDsAg8-9_EcVXwngrPg94dnnfgYYAyQ5OhMJjH3KF-LRy-Nzx9vOAlGvPwDn4e3sHPwzv4CVGPTR6D-KuoE4K5GgQFc0Jc5Buil4TnkRmiF47ncZDATRPZ2DTgMH4deAZxjHUCLZatVIq0ToyNfGPudeAsM3M6g_p7cyZlX3nVybF0J0fcnRhe1-QisYuD204f3FbkAUKqKuLvrU6Iq2uQRKUMQjdZJ8fsnRzZd3L835n4IGIGEXh1kGjbtK9GBDAq743VyLDN9MEt1MiwSQXv_awuyCvBTG9uocpOy4ih7LQcVp7E9Y656yH_cvA89W0a8xhjahjfCPWkcZ4llO3TO-oJYVUVoeOEeZLvnhzed0Lk4RuieCXzJA8_T_bT82Q_PYNwvtOE8xXTd3LAEm2DzCHE2LWQ3-XM01vDT8X-PP8o9jLnr8R-lX1njyG6rSL-acmEKLkimRru1jx2mzk8KJh3_FYHh-cHM7vzA5Fcjk87OYrtzLTBn5k3-JMD-M7M-_SJAXBvjPfpM_M-XfLh4a9m8xxTmsbnatqKF9Pst8KfimmW_fBvxLRGtLP47e0a2h3jkw5NJwcYnSXwZYV3khOjiWr7Pmj9kaE4fprQJLQHPI07QfuG71fimFEhILSr6VdCT1zoVqYO0h2rzpJeHy5K2Xe6f0e7tBp4B7d0cI9hfyY_IfinKWpmp9KdO6nJQvSFvoIV4qmow07C1mf1bDZp5a_Wv35g_Wtk_esfrH_9g_VvZOshLqXodqPT0AkBLtW4LQjfOpGqrvU0lZMI3YEdHeyjRmKfHyoxMibNNXv-MX3sQRknBFrUjLpXcW-QzDtuoHvnZN4hE31rFgrjJ00fKlEsEsU7nBTvcHK8w3mHaWTsYMzQuM67FAjDeEN0HDgxDmXCWcVTPh2nDwqgvcMISTMInzjjMIlTvoL3XovpS3afanH3b2l8oMV9vNhTq5nr6DzFHFqsECZCNxKJ56BHSXN0hmLnMWLnMXoo2xQqcUIURIPgq8oTIz4qRKEFJkVmnBS9cVJkxgkxF0093MlEeYzKDAiJcGMlwDDggKiI5DNiL_UFGKnC-OPz0kkfq_tUFcb756VS9p1DmwcJ6jxC0cPgixYipBNirN6EF-e1zczdaAzF7nB6B7dk4g76aDddc8bDyzw8CmcwITyiIj6awaRojHP6x6-TYjhOig45ITqjQQrVY2eXQzPOIDTjxDCZN5ZZtWZjNZJ8FvPRkgT-q2rNv6rW_INqzdeqdR4QZfGSyC_kficQjq2-4Js2Wdht3bTaGiiB1u3EWuEtMncnEKmE-FBKChUDeY6jUDeQP6-9u3Bw_Xg2e3cMzmF39-8rtF_Q_NNlcK3g9S7_a3_qHjB-IZ2QQog_8sSMDEePG7USXDwf51CLmuWkKIRazkL-2eLd6cElHUaxCjXfxOVM_shRWwhyuWcy3znf3sHXsu8024fiU4QW5_TnZgoVhuAI5QvK1EKOW_AvJClUGLKLcbpnYtrOpJZbjJCEntETTc2N8kgB_xSynfa-7W54cEk3Nv_ClGaz0L7sc2qmP22JtII_WJIcG_0cPbX7gv0zvgEwhPPTCoBzaVMQv0ihjgh5Cx-uT5FC9TBilpzj6d2VB5hxIBTET6HCkJWWOzIhdcRKEAbsU3nRMRjMPRbMOm1vd7Za9p1lwmBsCvn3S7-g_pCLWEXxZ5gDmvnujTu-vntQPVIIQTErhZAeI4VKJcqTKI-RC4yqd2MUz_ceSoBZWSnu0FVlQ7sHBqb87eRUK3jNhu_5eCWZ5fXJ6ZeL85s-hYiy-rhuihRCjAHACG26MlrVhJKZ6nJP9G7E9LUxNBmKNqhamGG3E71HklnuMLTejfnYeloUtcbHu9NijBQqxUiiUsnMd4s1QQuj5awuBK5MCoYpU8hfbtFc5A-b956rWy3edzXnPXVeW-A3WuA32giwYGvT2FtR4DWtDZelke_1IeIUKVTKIOSNIbCcLl0L9lotuvPyBXsT8qkRa2JHXhux1t5u6bXsOyPW_b2VL8h_TEchEs7ut3tr4nsULF_h_eBYxYSDpN0NWozuh9xYrVRf5-o6lwwglKpOjr6XGElUyiCdpLPDZ3Ms5m-x3BWe4Ha7jzTwqcT28vs3sUKJ7f3tm0Ba9tWjDhCvC8sBVggTgR20zR20LR20ORy0gRzRNRetjOHomotWy_ow6B7KXbPF6B7KXafFeH8-eEM9eG86yIkOIjIQN08RcquD3erwsenv4RyOfVPQvHsyBjVqIdxFL7Ebf3s-qBX8QWkoitsHSqMB3N6Y-ckcZDIHmTEHmTEHmbwnvwO_wS5KpYnDvd0NWizgIBg9zmKFoc4tdG4AackkWjKJlkCIOZOnUJ6Cgj4pXOw9ajLz090NknX_4_MGreAPUvv-eYOWfSW16SBykg4iJ-kID53T4TnLWDCeFCBk9_4iLOlgcpI4btldh8WIiEjRziUDCLdBUlchJCHimUg6_OGBIIVKXYgIog7QHaSmRBvelGhfmRJtIlOirWZK0YUfrY_hcLuZUrzd1I7ilkV7ajHegmr3ce6T36yn5LfcKXk_qJ0gpFMpg_jNbUp-45qS98sp-Q2wDo3yoBe-M03E3I2ppcba5msjJBW8MULC-7YQbN7nsEKYCMrpL2V9QXgpaw39hOCjKg4n734VywarAUbH0en0W9l0ei-iVVEpRjqVMoj3c9qhg6DCmbhDeA6lXRooOhIUy4U717wBPiwuknK-vgurZd95m-zvQX1BFHnyzmZWOvvPodzZDoI6IYWLIeJ3nCn7o-WUyfLnwmuWC69Zdt8SEVXIGEz07DhL6j_voOwGP2K8x3B5qOWG0wnwWTaX1wpASnL5Wutva9J-YY6y50TfhmgVH9WU92wo5fdsSMq-CYFxphUh6Qp2tMve1vP8Tw6DrM92FW7Xl-IuCfmqYCPi95cYQ_SRseQAY0qMSpCaZsTqwdlyQmzFLclYVGKUZOwHxvHXHkKwiBvKDE2Gtj_UVnZQC23lTmmPKZNSRoZu2HFI12aAtQDLAXYw1hkqQcm6F38Uv_gLMaRvXKZz7rUfJ609Qrr2iMnam2wZs8FXfLS7A-Irrf5-bXVdfwXB_p5Hy8uCfAu9mgtNu7K3KWkrjkBJkDbLf5ZpXC2uuxMJ60_rWy67v5q1c9ZrNq9z5loWMmiiDPb9miJhy3TbsrlxPmn1PLRVmbazdbvwghSYyIV0HPu5TcMNTRrjeX3tcdvxG7_CcvdwGVZjM1wG6exp12F9QM00nM97chUZfrozKppMN2I63QaT6TZlZboNJl0ryc9uKTFSCOmwAlLqwDGWK0IKOM874cySEEyv1HTa-V3ae97vY93aayCdJcSW9hpIFktf11qLpaAMf4FJ0HoFecKA0Ba7813qixGMLCbDqMB-qkd0EADJGLAYViQnhJUgW9XCTF0L0s5fUC526G0t1sQhIXbnuz54outQCJMew1s30mF8D0f6C5B0F4tN6MNCmkdkJ4WQVgTFpEfrdR7TJYDuPgGmdQGks7A81yEGQOahX-ZEQpFJTsRkSPLwAYYEiA4JIOkGFltDkldYDhgSQjokxLQugLSjC2p2SOOSfiM_8s6BOCHpyiBRHyzqg0R9kKgLkrmmHFSVua5Vknt_j-i6XZo7ZgNITB_cCDdIISQodTCUGWoMTYKWUlmogkGe-OxgidckjZmsMZM0BpC2Flpuzl-lOiAJ9sHnLGl1AOiupFyRcOWAi1PXC8aH2n-Nl5SxbXxFbc1BhrvQBimEdEJwYRTKDDWGJkGyMBWhuicvy01vAKqbTcmSEkJ1Qwdmynvq8nHFgvrecrlJ1X50N6lpNKjriyDcumUg0S2D1YOgtDYNtmjGUZtNg2JIqPO-en93UqZJ7jgDUDzQ_UTKpWucSLl1baC-hXdgJqBnOV0BRuXgfc-vTGBaM1_6nkCEdAIRkwlESCfQFJXJWpidLMGy7ZMrYaY8Ja89d0rOsV6dfuOV4eKyQTohRq_OTHp1ZtKrM5NenbAJtRAYvIx3p0Ug5GYyANMDhxcZuQqO8iEXow207Z2ooyCojmd6sID5rD9YQBkJWUCVtRNDYFeCVNYQqwdBKmumaMZpMrv5e3wGU6_mzw0096HPMGTZ4E6yQQohnRAjS3hN2kKTICM4Gc7MRXDuO9tbLOReNACnl5PMNlruHYMTzDk_WeTcQ3uiqTW01zrx98VxUmO9Gb4TFrvJBfi-DKp4i1q8RS1kUQtb1MIWtZBF1W95fyFrN2o6pKKNX_wW0UZIRRsxEW2EVLRN0YxjtmJs7g9nbKLb1REZqnAdxUInQUYe8Xu_FsoMHQR1QgoXA8G-763uBa2HW2G5ZgtA80te2SLKzVcDNb_k-rVkEOvanyzi6uujzcMPaItgIKSCgZgIBkIqGKZoxomwNg_vF98Y27yMjWOKTGnzjK9VDww_6Y3NS6sMjf_f17UlBwyCwCvV-EDvf7F2AugumP5uFMiq-MCRSLoVQtJHuU42xuxIJyoEObMjnagQ5MxS1Yp_y8yOjsyiCuWP3d1I7s54k-jBJHowSR5MsgeT7MEkeTDPtgvcYeJa445y2Rp3iBl3CDl3VLWi-cydSOZO5IOhmRY48wxn8xyz7g0eIZIQckrTV4wJGhlaCaJ9kkE9V-zQk2f5mk1n_282tat1X7Pp_InkeQNTptmeIG9gxKyBEXJpCzq9SUPIpSFm0hAyaQ3u4WwIIl09FnIELu2qwit06qkJNwSqKZMN7wYEzKLNFbFF5Xz9Fh1re2DvOBWByLAC2ZaTbinKO1mU9Cb3rkGZFitiI620WoVhqEbA62sG1ABAeh5XgIl0yMrm95KS_a2eL7cqi87VQy3L_8DhqrZz0qRamMCGsXGxbH39DJ7a0OhzafCfyhWkbTGgRwDeJivvKrcNsTAEMrDUBoGLXho_dQhGvvqj5ikGfPvixTU0kSTvBmlUfC8IaBHZ4ePRoPbLc8IoLhzPCRFK1qWVSVFX-if1daVFQP3ZJ5BVm7-Az9DlSqmodIxjHb77QlA0eD-Ac7ix4rGDEy7Ug10WLr70aoeZPq9kzUSW60C6TcJiCQ-gqdHn6sygBtqaPWtfW4LGC4lp10nvPQ1S7fYKixTo5msH0F4lv7Y86ovdkQIA:5:1:6:842519:8:10:9:10:10:6898698:12:14:13:21:14:274923:17:1:43:3:25::18:10:19:700:42:0:45:7092:15:3:30:897837:31:0:28:9 years:29:3 years:35:0:36:0_167_67_0_0_0_0_207_0_0_89_88_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0:37:0:38:0:39:0:46:113:47:0:40:0:27:AwYDBQUCBw==#bf337d690d496f0d4744f4987f33879545168285#f153089c6726a8faef998600b2645dfbbb76eb74 \ No newline at end of file diff --git a/tests/level.rs b/tests/level.rs index ebf80af..eb212f9 100644 --- a/tests/level.rs +++ b/tests/level.rs @@ -1,12 +1,7 @@ -use std::borrow::Cow; - use dash_rs::{ model::{ - level::{DemonRating, Featured, LevelLength, LevelRating}, - song::MainSong, - GameVersion, + level::LevelLength, }, - Base64Decoded, Thunk, }; use dash_rs::model::level::DemonRating::Extreme; use dash_rs::model::level::LevelRating::Demon; diff --git a/tests/misc.rs b/tests/misc.rs deleted file mode 100644 index 96566c8..0000000 --- a/tests/misc.rs +++ /dev/null @@ -1,109 +0,0 @@ -use dash_rs::{ - model::{ - creator::Creator, - level::{DemonRating, Featured, Level, LevelData, LevelLength, LevelRating, Password}, - song::{MainSong, NewgroundsSong}, - GameVersion, - }, - Base64Decoded, Thunk, -}; -use std::borrow::Cow; - -mod helper; - -const CREO_DUNE_DATA_TOO_MANY_FIELDS: &str = "1~|~771277~|~54~|~should be ignored~|~2~|~Creo - \ - Dune~|~3~|~50531~|~4~|~CreoMusic~|~5~|~8.\ - 03~|~6~|~~|~7~|~UCsCWA3Y3JppL6feQiMRgm6Q~|~8~|~1~|~10~|~https%3A%2F%2Faudio.ngfiles.com%\ - 2F771000%2F771277_Creo---Dune.mp3%3Ff1508708604~|~9~|~should be ignored"; - -const CREATOR_REGISTERED_DATA_TOO_MANY_FIELDS: &str = "4170784:Serponge:119741:34:fda:32:asd:3"; - -const TIME_PRESSURE: Level = Level { - level_id: 897837, - name: Cow::Borrowed("time pressure"), - description: Some(Thunk::Processed(Base64Decoded(Cow::Borrowed( - "please rate and like 8-9 stars mabye?", - )))), - version: 2, - creator: 842519, - difficulty: LevelRating::Demon(DemonRating::Easy), - downloads: 3189574, - main_song: Some(MainSong { - main_song_id: 14, - name: "Electrodynamix", - artist: "DJ-Nate", - }), - gd_version: GameVersion::Unknown, - likes: 198542, - length: LevelLength::Long, - stars: 10, - featured: Featured::Featured(700), - copy_of: None, - two_player: false, - custom_song: None, - coin_amount: 0, - coins_verified: false, - stars_requested: None, - is_epic: false, - object_amount: None, - index_46: None, - index_47: None, - level_data: LevelData { - level_data: Thunk::Unprocessed("REMOVED"), - password: Password::PasswordCopy(3101), - time_since_upload: Cow::Borrowed("5 years"), - time_since_update: Cow::Borrowed("5 years"), - index_36: None, - }, -}; - -impl helper::ThunkProcessor for Level<'_, LevelData<'_>, S, U> { - fn process_all_thunks(&mut self) { - if let Some(ref mut hunk) = self.description { - assert!(hunk.process().is_ok()) - } - let objects = self.level_data.level_data.process(); - assert!(objects.is_ok(), "{:?}", objects.unwrap_err()); - } -} - -impl helper::ThunkProcessor for Level<'_, (), S, U> { - fn process_all_thunks(&mut self) { - if let Some(ref mut hunk) = self.description { - assert!(hunk.process().is_ok()) - } - } -} - -#[test] -fn deserialize_too_many_fields() { - init_log(); - - helper::load::(CREO_DUNE_DATA_TOO_MANY_FIELDS); - helper::load::(CREATOR_REGISTERED_DATA_TOO_MANY_FIELDS); -} - -#[test] -fn deserialize_level() { - init_log(); - - let mut level = helper::load_processed::(include_str!("data/11774780_dark_realm_gjdownload_response")); -} - -#[test] -fn deserialize_level2() { - init_log(); - - let mut level = helper::load_processed::(include_str!("data/897837_time_pressure_gjdownload_response")); - - level.level_data.level_data = Thunk::Unprocessed("REMOVED"); - - assert_eq!(level, TIME_PRESSURE); -} - -fn init_log() { - if let Err(err) = env_logger::builder().is_test(true).try_init() { - // nothing to make the tests fail over - eprintln!("Error setting up env_logger: {:?}", err) - } -} diff --git a/tests/response.rs b/tests/response.rs index c9c1e0e..bca83dc 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -1,5 +1,3 @@ -use dash_rs::request; - // This string has been manually edited to ensure that the cases of duplicate and missing // songs/creators are also covered by the unit test const GET_GJ_LEVELS_RESPONSE: &str = "1:62953227:2:Noice:5:1:6:14098234:8:10:9:30:10:329795:12:0:13:21:14:16024:17::43:0:25::18:5:19:24981:42:1:45:30320:3:Tm9pY2UgbGV2ZWwsIGhvcGUgeW91IGxpa2UgaXQ=:15:3:30:0:31:0:37:0:38:0:39:5:46:1:47:2:35:778510|1:63362544:2:Happy Day Gd:5:4:6:14098234:8:10:9:30:10:16162:12:0:13:21:14:2772:17::43:0:25::18:4:19:24979:42:0:45:38476:3:NyB5ZWFycyBvZiBqb3lzIGFuZCBzb3Jyb3dzLCB0aGUgYmVzdCBnYW1lIEkga25ldyBhbmQgSSB3aWxsIHN0YXksIGNvbW1lbnQgaG93IG1hbnkgeWVhcnMgeW91IGhhdmUgYmVlbiBhbmQgd2hhdCBtb3RpdmF0ZWQgeW91IHRvIHN0YXk=:15:3:30:0:31:0:37:3:38:1:39:4:46:1:47:2:35:936243|1:63336521:2:Sound Visualization:5:1:6:4123296:8:10:9:50:10:47521:12:0:13:21:14:2951:17::43:6:25::18:8:19:24979:42:1:45:26229:3:dmlzdWFsIGVmZmVjdHM=:15:3:30:0:31:0:37:3:38:1:39:8:46:1:47:2:35:778510|1:63335504:2:Mind Control:5:2:6:10130943:8:10:9:30:10:51434:12:0:13:21:14:4120:17::43:0:25::18:5:19:24979:42:1:45:49951:3:SXQncyBkb25lIHlhYWF5:15:3:30:0:31:0:37:2:38:1:39:5:46:1:47:2:35:763439|1:63333766:2:An Ode to Time:5:8:6:7226087:8:10:9:10:10:10795:12:0:13:21:14:716:17:1:43:3:25::18:10:19:24979:42:0:45:65535:3:QW5kIGl0IGFsbCBjb21lcyBjcmFzaGluZyBkb3duLiAwOC8xMC8yMC4=:15:4:30:0:31:0:37:3:38:1:39:10:46:1:47:2:35:896364|1:63292359:2:AnnoZone:5:2:6:5897998:8:10:9:50:10:7890:12:0:13:21:14:636:17::43:6:25::18:8:19:24979:42:1:45:51592:3:VGhlIEFubm8gU2VyaWVzIGhhcyByZXR1cm5lZCBhZnRlciAyIHllYXJzIHdpdGggYSAzcmQgbGV2ZWwhIERlZGljYXRlZCB0byB0aGUgQnJveXMuIE1vcmUgQW5ubyBTZXJpZXMgbGV2ZWxzIHRvIGNvbWUuLi4_:15:3:30:0:31:0:37:0:38:1:39:7:46:1:47:2:35:638150|1:63260507:2:Trouble:5:3:6:14221993:8:10:9:50:10:4512:12:0:13:21:14:323:17::43:6:25::18:8:19:24979:42:1:45:58854:3:SSBzaG91bGQgb2Yga25vd24geW91IHdlcmUgdHJvdWJsZS4uLi4uLiAgICAgICAgICAgICBoaQ==:15:3:30:0:31:0:37:1:38:1:39:8:46:1:47:2:35:939885|1:63254272:2:AdrenaLines:5:2:6:116033399:8:10:9:40:10:34848:12:0:13:21:14:2419:17::43:5:25::18:6:19:24979:42:0:45:32956:3:ZGVjbyBsdmw_ISBlbmpveSB1d3U=:15:3:30:0:31:0:37:2:38:1:39:6:46:1:47:2:35:887253|1:63232525:2:Metropolis:5:3:6:1647052:8:10:9:10:10:99385:12:0:13:21:14:5506:17:1:43:3:25::18:10:19:24979:42:1:45:65535:3:V2VsY29tZS4uLg==:15:3:30:0:31:0:37:1:38:1:39:10:46:1:47:2:35:674039|1:61865319:2:Utopia:5:4:6:5570844:8:10:9:50:10:148912:12:0:13:21:14:6961:17::43:6:25::18:8:19:24979:42:0:45:13533:3:dXBkYXRlZCwgc2Vjb25kIHBhcnQgZG9lc250IGxvb2sgc28gdWdseSBub3c=:15:3:30:0:31:0:37:3:38:1:39:8:46:1:47:2:35:761926#1647052:DesTicY:95952|4123296:Cdpre:1478680|5570844:Axils:1341135|7226087:Pauze:1705254|8908442:Nikce:2517174|10130943:FaekI:1727914|14098234:AleXins:4322668|14221993:IFuse:5633975|116033399:KumoriGD:11439344#1~|~638150~|~2~|~-ThunderZone v2-~|~3~|~30~|~4~|~Waterflame~|~5~|~8.78~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F638000%2F638150_-ThunderZone-v2-.mp3~|~7~|~UCVuv5iaVR55QXIc_BHQLakA~|~8~|~1~:~1~|~674039~|~2~|~Crystal Tokyo~|~3~|~746~|~4~|~Fantomenk~|~5~|~10.54~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F674000%2F674039_Crystal-Tokyo.mp3~|~7~|~UCMSBjXolfz29kxnXpBa7LJA~|~8~|~1~:~1~|~761926~|~2~|~mistmurk + 3MBER - Utopia~|~3~|~49123~|~4~|~mistmurk~|~5~|~7.76~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F761000%2F761926_mistmurk--3mber---Utopia.mp3~|~7~|~~|~8~|~1~:~1~|~763439~|~2~|~ColBreakz - Mind Control~|~3~|~47795~|~4~|~ColBreakz~|~5~|~10~|~6~|~~|~10~|~http%3A%2F%2Faudio.ngfiles.com%2F763000%2F763439_ColBreakz---Mind-Control.mp3~|~7~|~~|~8~|~1~:~1~|~778510~|~2~|~Hazmat~|~3~|~23384~|~4~|~CricketSaysChill~|~5~|~1.8~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F778000%2F778510_Hazmat.mp3%3Ff1512785304~|~7~|~~|~8~|~1~:~1~|~852209~|~2~|~Fried Sushi~|~3~|~28916~|~4~|~lchavasse~|~5~|~5.88~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F852000%2F852209_Fried-Sushi.mp3%3Ff1552100587~|~7~|~~|~8~|~1~:~1~|~887253~|~2~|~Adrenaline~|~3~|~51089~|~4~|~PsoGnar~|~5~|~10.35~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F887000%2F887253_Adrenaline.mp3%3Ff1570984144~|~7~|~~|~8~|~1~:~1~|~896364~|~2~|~Beethoven - Moonlight Sonata 3rd Movement (meganeko remix)~|~3~|~48917~|~4~|~meganeko~|~5~|~9.45~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F896000%2F896364_Beethoven---Moonlight-Sona.mp3%3Ff1575491260~|~7~|~UCP3M2myndqXuAEKKnqm_7SQ~|~8~|~1~:~1~|~936243~|~2~|~Phaera - Ignition~|~3~|~50872~|~4~|~TheArcadium~|~5~|~5.76~|~6~|~~|~10~|~https%3A%2F%2Faudio.ngfiles.com%2F936000%2F936243_Phaera---Ignition.mp3%3Ff1590147327~|~7~|~~|~8~|~1#11389:0:10#f687963dcfd37f857633563ee28b0cfadc727c97"; From 56ec8fb2129653e98283e76643b90343091178af Mon Sep 17 00:00:00 2001 From: Ryder <36455274+zRyder@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:03:15 -0800 Subject: [PATCH 22/29] Added SuggestGJStars request models and tests (#11) * Added SuggestGJStars request models and tests * Miscellaneous refactos --------- Co-authored-by: Ryder --- rustfmt.toml | 5 + src/lib.rs | 1 - src/model/level/mod.rs | 2 +- src/model/user/mod.rs | 26 +++--- src/request/account.rs | 12 +-- src/request/comment.rs | 76 ++++++++-------- src/request/level.rs | 61 ++++--------- src/request/mod.rs | 50 +++++----- src/request/moderator.rs | 192 +++++++++++++++++++++++++++++++++++++++ src/request/user.rs | 10 +- tests/moderator.rs | 37 ++++++++ 11 files changed, 337 insertions(+), 135 deletions(-) create mode 100644 src/request/moderator.rs create mode 100644 tests/moderator.rs diff --git a/rustfmt.toml b/rustfmt.toml index db2d5e5..2b6230a 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -21,3 +21,8 @@ trailing_semicolon = false use_field_init_shorthand = true use_try_shorthand = true wrap_comments = true +fn_single_line = true +hard_tabs = true +normalize_doc_attributes = true +group_imports = "StdExternalCrate" +trailing_comma = "Never" diff --git a/src/lib.rs b/src/lib.rs index bf74a01..121d7a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ #![forbid(unsafe_code)] #![deny( - const_err, illegal_floating_point_literal_pattern, late_bound_lifetime_arguments, non_camel_case_types, diff --git a/src/model/level/mod.rs b/src/model/level/mod.rs index f47fbb7..63f2827 100644 --- a/src/model/level/mod.rs +++ b/src/model/level/mod.rs @@ -1,4 +1,4 @@ -//! Module containing structs modelling Geometry Dash levels as they are returned from the boomlings +//! Module containing structs modelling Geometry Dash levels as they are returned from the Boomlings //! servers use base64::URL_SAFE; diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index 24bd9ad..bbb8166 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -91,15 +91,15 @@ impl From for u8 { pub enum MessageState { /// Messages are open to all users - /// This is provided by value 0 by boomlings response for messageState + /// This is provided by value 0 by Boomlings response for messageState Anyone, /// Messages are only open to friends - /// This is provided by value 1 by boomlings response for messageState + /// This is provided by value 1 by Boomlings response for messageState FriendsOnly, /// Messages cannot be sent to this user - /// This is provided by value 2 by boomlings response for messageState + /// This is provided by value 2 by Boomlings response for messageState NoOne, /// Unknown or invalid value @@ -132,10 +132,10 @@ impl From for MessageState { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum FriendsState { - /// Friend requests are open to all users, this is provided by value 0 by boomlings response for friendsState + /// Friend requests are open to all users, this is provided by value 0 by Boomlings response for friendsState Anyone, - /// Friend requests cannot be sent to this user, this is provided by value 1 by boomlings response for friendsState + /// Friend requests cannot be sent to this user, this is provided by value 1 by Boomlings response for friendsState NoOne, /// Unknown or invalid value @@ -167,19 +167,19 @@ impl From for FriendsState { pub enum FriendState { /// The [`AuthenticatedUser`] has not sent a friend request to this profile - /// This is provided by value 0 by boomlings response for friendsState + /// This is provided by value 0 by Boomlings response for friendsState NoRequest, /// The [`AuthenticatedUser`] is already friends with this profile - /// This is provided by value 1 by boomlings response for friendsState + /// This is provided by value 1 by Boomlings response for friendsState AlreadyFriends, /// The [`AuthenticatedUser`] has sent a request to this profile but the profile has not responded - /// This is provided by value 3 by boomlings response for friendsState + /// This is provided by value 3 by Boomlings response for friendsState RequestPendingByProfile, /// The profile has sent a request to the [`AuthenticatedUser`] but the [`AuthenticatedUser`] has not responded - /// This is provided by value 4 by boomlings response for friendsState + /// This is provided by value 4 by Boomlings response for friendsState RequestPendingByUser, /// Unknown or invalid value @@ -215,15 +215,15 @@ impl From for FriendState { pub enum CommentHistoryState { /// Messages are open to all users - /// This is provided by value 0 by boomlings response for messageState + /// This is provided by value 0 by Boomlings response for messageState Anyone, /// Messages are only open to friends - /// This is provided by value 1 by boomlings response for messageState + /// This is provided by value 1 by Boomlings response for messageState FriendsOnly, /// Messages cannot be sent to this user - /// This is provided by value 2 by boomlings response for messageState + /// This is provided by value 2 by Boomlings response for messageState NoOne, /// Unknown or invalid value @@ -258,7 +258,7 @@ pub enum Color { /// A color whose index was known to dash-rs which could be converted to RGB values Known(u8, u8, u8), - /// The index of some unknown colors. This variant will be constructed if robtop ever adds more + /// The index of some unknown colors. This variant will be constructed if RobTop ever adds more /// colors and while dash-rs hasn't updated yet Unknown(u8), } diff --git a/src/request/account.rs b/src/request/account.rs index 171ac2e..96b593d 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -20,7 +20,7 @@ pub struct LoginRequest<'a> { /// The Unique Device IDentifier (UDID) of the user to authenticate /// /// ## GD Internals: - /// This field is called `udid` in the boomlings API + /// This field is called `udid` in the Boomlings API /// The value of this value can be randomly generated /// The digits must be between 100,000 and 100,000,000 /// This will succeed as long as these conditions are met @@ -29,22 +29,18 @@ pub struct LoginRequest<'a> { /// The username of the user to authenticate /// /// ## GD Internals: - /// This field is called `userName` in the boomlings API + /// This field is called `userName` in the Boomlings API #[serde(rename = "userName")] pub user_name: &'a str, /// The unencrypted password of the user to authenticate /// /// ## GD Internals: - /// This field is called `password` in the boomlings API - pub password: &'a str, + /// This field is called `password` in the Boomlings API + password: &'a str, /// The secret token to call /database/accounts routes pub secret: &'a str, - - //// The base request data - // #[serde(borrow)] - // pub base: BaseRequest<'a>, } impl<'a> LoginRequest<'a> { diff --git a/src/request/comment.rs b/src/request/comment.rs index fc3bc34..dbc5de0 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -6,7 +6,6 @@ use crate::{ util }; use serde::Serialize; -use reqwest::Error; pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php"; pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php"; @@ -24,13 +23,13 @@ pub enum SortMode { /// Sort the comments by likes, in descending order /// /// ## GD Internals: - /// This variant is represented by the numeric value `1` in the boomlings API + /// This variant is represented by the numeric value `1` in the Boomlings API Liked, /// Sort the comments from newest to oldest /// /// ## GD Internals: - /// This variant is represented by the numeric value `0` in the boomlings APII + /// This variant is represented by the numeric value `0` in the Boomlings API Recent, } @@ -51,26 +50,26 @@ pub struct LevelCommentsRequest<'a> { /// Unknown, probably related to pagination /// /// ## GD Internals: - /// This field is called `total` in the boomlings API + /// This field is called `total` in the Boomlings API pub total: u32, /// The page of users to retrieve. The first page is page `0` /// /// ## GD Internals: - /// This field is called `page` in the boomlings API + /// This field is called `page` in the Boomlings API pub page: u32, /// What to sort by comments by /// /// ## GD Internals: - /// This field is called `mode` in the boomlings API. + /// This field is called `mode` in the Boomlings API. #[serde(rename = "mode")] pub sort_mode: SortMode, /// The id of the level to retrieve the comments of /// /// ## GD Internals: - /// This field is called `levelID` in the boomlings API + /// This field is called `levelID` in the Boomlings API #[serde(rename = "levelID")] pub level_id: u64, @@ -78,7 +77,7 @@ pub struct LevelCommentsRequest<'a> { /// (via the "load more comments option), the API accepts any value. The max value for this is 100 /// /// ## GD Internals: - /// This field is called `count` in the boomlings API + /// This field is called `count` in the Boomlings API #[serde(rename = "count")] pub limit: u32, } @@ -90,10 +89,6 @@ impl<'a> LevelCommentsRequest<'a> { const_setter!(page: u32); - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, LEVEL_COMMENTS_ENDPOINT) - } - pub const fn new(level: u64) -> Self { Self::with_base(GD_21, level) } @@ -119,6 +114,10 @@ impl<'a> LevelCommentsRequest<'a> { self } + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, LEVEL_COMMENTS_ENDPOINT) + } + pub fn to_string(&self) -> String { super::to_string(&self) } @@ -132,19 +131,19 @@ pub struct ProfileCommentsRequest<'a> { /// Unknown, probably related to pagination /// /// ## GD Internals: - /// This field is called `total` in the boomlings API + /// This field is called `total` in the bBomlings API pub total: u32, /// The page of users to retrieve. The first page is page `0` /// /// ## GD Internals: - /// This field is called `page` in the boomlings API + /// This field is called `page` in the Boomlings API pub page: u32, /// The account id of the user to retrieve the comments of /// /// ## GD Internals: - /// This field is called `accountID` in the boomlings API + /// This field is called `accountID` in the Boomlings API #[serde(rename = "accountID")] pub account_id: u64, } @@ -156,15 +155,11 @@ impl<'a> ProfileCommentsRequest<'a> { const_setter!(account_id: u64); - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, PROFILE_COMMENT_ENDPOINT) - } - pub const fn new(account: u64) -> Self { Self::with_base(GD_21, account) } - pub const fn with_base(base: BaseRequest<'a>, account: u64) -> Self { + const fn with_base(base: BaseRequest<'a>, account: u64) -> Self { ProfileCommentsRequest { account_id: account, base, @@ -173,6 +168,10 @@ impl<'a> ProfileCommentsRequest<'a> { } } + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, PROFILE_COMMENT_ENDPOINT) + } + pub fn to_string(&self) -> String { super::to_string(&self) } @@ -187,37 +186,38 @@ pub struct CommentHistoryRequest<'a> { /// Unknown, probably related to pagination /// /// ## GD Internals: - /// This field is called `total` in the boomlings API + /// This field is called `total` in the Boomlings API pub total: u32, /// The page of comments to retrieve. The first page is page `0` and pages will contain `total` number of comments /// /// ## GD Internals: - /// This field is called `page` in the boomlings API + /// This field is called `page` in the Boomlings API pub page: u32, /// The amount of comments to retrieve. Note that while in-game this can only be set to 20 or 40 however, a max of 100 comments can be returned /// ## GD Internals: - /// This field is called `count` in the boomlings API + /// This field is called `count` in the Boomlings API pub count: u32, /// What to sort by comments by /// /// ## GD Internals: - /// This field is called `mode` in the boomlings API. + /// This field is called `mode` in the Boomlings API. #[serde(rename = "mode")] pub sort_mode: SortMode, /// The id of the player to retrieve comments, this is `not` the account ID /// /// ## GD Internals: - /// This field is called `userID` in the boomlings API. + /// This field is called `userID` in the Boomlings API. #[serde(rename = "userID")] pub player_id: u64, } impl<'a> CommentHistoryRequest<'a> { const_setter!(page: u32); + const_setter!(count: u32); pub const fn with_base(base: BaseRequest<'a>, player: u64) -> Self { @@ -255,14 +255,14 @@ pub struct UploadCommentRequest<'a> { pub base: BaseRequest<'a>, /// The authenticated user data - pub authenticated_user: AuthenticatedUser<'a>, + authenticated_user: AuthenticatedUser<'a>, /// The content of the comment, this value will be base64 url encoded pub comment: Cow<'a, str>, /// The id of the level the comment to upload is posted to /// ## GD Internals: - /// This field is called `levelID` in the boomlings API + /// This field is called `levelID` in the Boomlings API #[serde(rename = "levelID")] pub level_id: u64, @@ -272,6 +272,7 @@ pub struct UploadCommentRequest<'a> { impl<'a> UploadCommentRequest<'a> { const_setter!(level_id: u64); + const_setter!(percent: u8); pub fn to_url(&self) -> String { @@ -283,7 +284,7 @@ impl<'a> UploadCommentRequest<'a> { Self::with_base(GD_21, authenticated_user, level_id) } - pub const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { UploadCommentRequest{ base, authenticated_user, @@ -317,28 +318,25 @@ pub struct DeleteCommentRequest<'a> { pub base: BaseRequest<'a>, /// The authenticated user data - pub authenticated_user: AuthenticatedUser<'a>, + authenticated_user: AuthenticatedUser<'a>, /// The id of the level comment to delete /// ## GD Internals: - /// This field is called `commentID` in the boomlings API + /// This field is called `commentID` in the Boomlings API #[serde(rename = "commentID")] pub comment_id: u64, /// The id of the level the comment to delete is posted to /// ## GD Internals: - /// This field is called `levelID` in the boomlings API + /// This field is called `levelID` in the Boomlings API #[serde(rename = "levelID")] pub level_id: u64, } impl<'a> DeleteCommentRequest<'a> { const_setter!(comment_id: u64); - const_setter!(level_id: u64); - pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, DELETE_COMMENT_ENDPOINT) - } + const_setter!(level_id: u64); pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64, comment_id: u64) -> Self { Self::with_base(GD_21, authenticated_user, level_id, comment_id) @@ -353,12 +351,12 @@ impl<'a> DeleteCommentRequest<'a> { } } - pub fn to_string(&self) -> String { - super::to_string(&self) + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, DELETE_COMMENT_ENDPOINT) } - pub async fn get_response_body(&self) -> Result { - super::execute(&self, &self.to_url()).await + pub fn to_string(&self) -> String { + super::to_string(&self) } } diff --git a/src/request/level.rs b/src/request/level.rs index 1d45f9b..5db2d0e 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -24,20 +24,20 @@ pub struct LevelRequest<'a> { /// The ID of the level to download /// /// ## GD Internals: - /// This field is called `levelID` in the boomlings API + /// This field is called `levelID` in the Boomlings API #[serde(rename = "levelID")] pub level_id: u64, /// Some weird field the Geometry Dash Client sends along /// /// ## GD Internals: - /// This value needs to be converted to an integer for the boomlings API + /// This value needs to be converted to an integer for the Boomlings API pub inc: bool, /// Some weird field the Geometry Dash Client sends along /// /// ## GD Internals: - /// This field is called `extras` in the boomlings API and needs to be + /// This field is called `extras` in the Boomlings API and needs to be /// converted to an integer pub extra: bool, } @@ -49,36 +49,15 @@ impl From for LevelRequest<'_> { } impl<'a> LevelRequest<'a> { - const_setter! { - /// Sets the [`BaseRequest`] to be used - /// - /// Allows builder-style creation of requests - base[with_base]: BaseRequest<'a> - } + const_setter! { base[with_base]: BaseRequest<'a> } - const_setter! { - level_id: u64 - } + const_setter! { level_id: u64 } - const_setter! { - /// Sets the value of the `inc` field - /// - /// Allows builder-style creation of requests - inc: bool - } + const_setter! { inc: bool } + + const_setter! { extra: bool } - const_setter! { - /// Sets the value of the `extra` field - /// - /// Allows builder-style creation of requests - extra: bool - } - /// Constructs a new `LevelRequest` to retrieve the level with the given id - /// - /// Uses a default [`BaseRequest`], and sets the - /// `inc` field to `true` and `extra` to `false`, as are the default - /// values set the by the Geometry Dash Client pub const fn new(level_id: u64) -> LevelRequest<'a> { LevelRequest { base: GD_21, @@ -154,19 +133,19 @@ pub struct SearchFilters { /// Only retrieve featured levels /// /// ## GD Internals: - /// This value needs to be converted to an integer for the boomlings API + /// This value needs to be converted to an integer for the Boomlings API pub featured: bool, /// Only retrieve original (uncopied) levels /// /// ## GD Internals: - /// This value needs to be converted to an integer for the boomlings API + /// This value needs to be converted to an integer for the Boomlings API pub original: bool, /// Only retrieve two-player levels /// /// ## GD Internals: - /// This field is called `twoPlayer` in the boomlings API and needs to be + /// This field is called `twoPlayer` in the Boomlings API and needs to be /// converted to an integer #[serde(rename = "twoPlayer")] pub two_player: bool, @@ -174,19 +153,19 @@ pub struct SearchFilters { /// Only retrieve levels with coins /// /// ## GD Internals: - /// This value needs to be converted to an integer for the boomlings API + /// This value needs to be converted to an integer for the Boomlings API pub coins: bool, /// Only retrieve epic levels /// /// ## GD Internals: - /// This value needs to be converted to an integer for the boomlings API + /// This value needs to be converted to an integer for the Boomlings API pub epic: bool, /// Only retrieve star rated levels /// /// ## GD Internals: - /// This field is called `star` in the boomlings API and needs to be + /// This field is called `star` in the Boomlings API and needs to be /// converted to an integer #[serde(rename = "star")] pub rated: bool, @@ -195,7 +174,7 @@ pub struct SearchFilters { /// /// ## GD Internals: /// This field composes both the `customSong` and `song` fields of the - /// boomlings API. To filter by main song, set the `song` field to the + /// Boomlings API. To filter by main song, set the `song` field to the /// id of the main song, and omit the `customSong` field from the /// request. To filter /// by a newgrounds @@ -463,7 +442,7 @@ pub struct LevelsRequest<'a> { /// The type of level list to retrieve /// /// ## GD Internals: - /// This field is called `type` in the boomlings API and needs to be + /// This field is called `type` in the Boomlings API and needs to be /// converted to an integer #[serde(rename = "type")] pub request_type: LevelRequestType, @@ -474,7 +453,7 @@ pub struct LevelsRequest<'a> { /// [`LevelRequestType::Search`] or [`LevelRequestType::User`] /// /// ## GD Internals: - /// This field is called `str` in the boomlings API + /// This field is called `str` in the Boomlings API #[serde(rename = "str")] pub search_string: &'a str, @@ -484,7 +463,7 @@ pub struct LevelsRequest<'a> { /// [`LevelRequestType::Search`] /// /// ## GD Internals: - /// This field is called `len` in the boomlings API and needs to be + /// This field is called `len` in the Boomlings API and needs to be /// converted to a comma separated list of integers, or a single dash /// (`-`) if filtering by level length isn't wanted. #[serde(rename = "len")] @@ -500,7 +479,7 @@ pub struct LevelsRequest<'a> { /// [`LevelRequestType::Search`] /// /// ## GD Internals: - /// This field is called `diff` in the boomlings API and needs to be + /// This field is called `diff` in the Boomlings API and needs to be /// converted to a comma separated list of integers, or a single dash /// (`-`) if filtering by level rating isn't wanted. #[serde(rename = "diff")] @@ -515,7 +494,7 @@ pub struct LevelsRequest<'a> { /// [`LevelRequestType::Search`] /// /// ## GD Internals: - /// This field is called `demonFilter` in the boomlings API and needs to be + /// This field is called `demonFilter` in the Boomlings API and needs to be /// converted to an integer. If filtering by demon rating isn't wanted, /// the value has to be omitted from the request. #[serde(rename = "demonFilter")] diff --git a/src/request/mod.rs b/src/request/mod.rs index 1d7c14c..2b5cce8 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -1,9 +1,9 @@ -//! Module containing all structs modelling requests to the boomlings APIs. +//! Module containing all structs modelling requests to the Boomlings APIs. //! //! These directly implement (de)serialization into RobTop's data format, unlike models where //! RobTop's eccentricities are hidden. This is since directly re-using these structs outside of -//! making/proxying requests for the boomlings servers seems rather useless to me, as they already -//! contain a lot of boomlings-specific fields. +//! making/proxying requests for the Boomlings servers seems rather useless to me, as they already +//! contain a lot of Boomlings-specific fields. use std::borrow::Cow; use crate::{ @@ -13,7 +13,6 @@ use crate::{ serde::RequestSerializer, }; use serde::{Deserialize, Serialize}; -use reqwest::Error; macro_rules! const_setter { ($name: ident, $field: ident, $t: ty) => { @@ -51,10 +50,15 @@ pub mod comment; pub mod level; pub mod user; pub mod account; +pub mod moderator; pub const REQUEST_BASE_URL: &str = "http://www.boomlings.com/database/"; + +pub const SECRET: &str = "Wmfd2893gb7"; pub const ACCOUNT_SECRET: &str = "Wmfv3899gc9"; +pub const MODERATOR_SECRET: &str = "Wmfp3879gc3"; + pub const CONTENT_TYPE: &str = "Content-Type"; pub const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; @@ -63,20 +67,26 @@ pub const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; pub const GD_21: BaseRequest = BaseRequest::new( GameVersion::Version { major: 2, minor: 1 }, GameVersion::Version { major: 3, minor: 3 }, - "Wmfd2893gb7", + SECRET, +); + +pub const MODERATOR_GD_21: BaseRequest = BaseRequest::new( + GameVersion::Version { major: 2, minor: 1 }, + GameVersion::Version { major: 3, minor: 3 }, + MODERATOR_SECRET, ); /// Base data included in every request made /// /// The fields in this struct are only relevant when making a request to the -/// `boomlings` servers. When using GDCF with a custom Geometry Dash API, they +/// `Boomlings` servers. When using GDCF with a custom Geometry Dash API, they /// can safely be ignored. #[derive(Debug, Clone, Hash, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct BaseRequest<'a> { /// The version of the game client we're pretending to be /// /// ## GD Internals: - /// This field is called `gameVersion` in the boomlings API and needs to be + /// This field is called `gameVersion` in the Boomlings API and needs to be /// converted to a string response /// The value of this field doesn't matter, and the request will succeed /// regardless of what it's been set to @@ -86,7 +96,7 @@ pub struct BaseRequest<'a> { /// Internal version of the game client we're pretending to be /// /// ## GD Internals: - /// This field is called `binaryVersion` in the boomlings API and needs to + /// This field is called `binaryVersion` in the Boomlings API and needs to /// be converted to a string /// /// The value of this field doesn't matter, and the request will succeed @@ -118,28 +128,28 @@ impl Default for BaseRequest<'static> { } } -#[derive(Debug, Serialize, Default, Clone, Hash)] +#[derive(Debug, Serialize, Default, PartialEq, Eq, Clone, Hash)] pub struct AuthenticatedUser<'a> { /// The user name of the authenticated user /// /// ## GD Internals: - /// This field is called `userName` in the boomlings API + /// This field is called `userName` in the Boomlings API #[serde(rename = "userName")] pub user_name: &'a str, /// The account ID of the authenticated user /// /// ## GD Internals: - /// This field is called `accountID` in the boomlings API + /// This field is called `accountID` in the Boomlings API #[serde(rename = "accountID")] pub account_id: u64, /// The encrypted password of the authenticated user, this is sensitive data as it can be used to act as a user on endpoints requiring `gjp` /// /// ## GD Internals: - /// This field is called `gjp` in the boomlings API + /// This field is called `gjp` in the Boomlings API #[serde(rename = "gjp")] - pub password_hash: Cow<'a, str> + password_hash: Cow<'a, str> } pub(crate) fn to_string(request: S) -> String { @@ -149,18 +159,4 @@ pub(crate) fn to_string(request: S) -> String { request.serialize(&mut serializer).unwrap(); String::from_utf8(output).unwrap() -} - -async fn execute(request: S, url: &str) -> Result { - let reqwest_client = reqwest::Client::new(); - println!("{:?}", to_string(&request)); - println!("{:?}", url); - reqwest_client - .post(url) - .body(to_string(request)) - .header(CONTENT_TYPE, URL_FORM_ENCODED) - .send() - .await? - .text() - .await } \ No newline at end of file diff --git a/src/request/moderator.rs b/src/request/moderator.rs new file mode 100644 index 0000000..4ff2c12 --- /dev/null +++ b/src/request/moderator.rs @@ -0,0 +1,192 @@ +use serde::Serialize; +use crate::request::{AuthenticatedUser, BaseRequest, MODERATOR_GD_21, REQUEST_BASE_URL}; + +pub const SUGGEST_STARS_ENDPOINT: &str = "suggestGJStars20.php"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct SuggestStarsRequest<'a> { + /// The base request data + pub base: BaseRequest<'a>, + + /// The authenticated Moderator data + authenticated_user: AuthenticatedUser<'a>, + + /// The id of the level the Moderator is suggesting + /// ## GD Internals: + /// This field is called `levelID` in the Boomlings API + #[serde(rename = "levelID")] + pub level_id: u64, + + /// The star rating the Moderator is suggesting + /// ## GD Internals: + /// This field is called `stars` in the Boomlings API + pub stars: SuggestedStars, + + /// The feature score the Moderator is suggesting + /// ## GD Internals: + /// This field is called `feature` in the Boomlings API + pub feature: SuggestedFeatureScore, + + /// The Geometry Dash World flag, should not be used but is required for this request + /// ## GD Internals: + /// This field is called `gdw` in the Boomlings API + pub gdw: u8 +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(into = "u8")] +pub enum SuggestedStars { + /// Suggest the level with a one star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `1` in the Boomlings API + One, + + /// Suggest the level with a two star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `2` in the Boomlings API + Two, + + /// Suggest the level with a three star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `3` in the Boomlings API + Three, + + /// Suggest the level with a four star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `4` in the Boomlings API + Four, + + /// Suggest the level with a five star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `5` in the Boomlings API + Five, + + /// Suggest the level with a six star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `6` in the Boomlings API + Six, + + /// Suggest the level with a seven star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `7` in the Boomlings API + Seven, + + /// Suggest the level with an eight star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `8` in the Boomlings API + Eight, + + /// Suggest the level with a nine star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `9` in the Boomlings API + Nine, + + /// Suggest the level with a ten star rating + /// + /// ## GD Internals: + /// This variant is represented by the numeric value `10` in the Boomlings API + Ten +} + +impl From for u8 { + fn from(stars: SuggestedStars) -> Self { + match stars { + SuggestedStars::One => 1, + SuggestedStars::Two => 2, + SuggestedStars::Three => 3, + SuggestedStars::Four => 4, + SuggestedStars::Five => 5, + SuggestedStars::Six => 6, + SuggestedStars::Seven => 7, + SuggestedStars::Eight => 8, + SuggestedStars::Nine => 9, + SuggestedStars::Ten => 10 + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(into = "u8")] +pub enum SuggestedFeatureScore { + /// Suggest the level with a Rate feature score + /// ## GD Internals: + /// This variant is represented by the numeric value `0` in the Boomlings API + Rate, + + /// Suggest the level with a Featured feature score + /// ## GD Internals: + /// This variant is represented by the numeric value `1` in the Boomlings API + Featured +} + +impl From for u8 { + fn from(feature_score: SuggestedFeatureScore) -> Self { + match feature_score { + SuggestedFeatureScore::Rate => 0, + SuggestedFeatureScore::Featured => 1 + } + } +} + +impl<'a> SuggestStarsRequest<'a> { + const_setter!(stars: SuggestedStars); + + const_setter!(feature: SuggestedFeatureScore); + + pub const fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + Self::with_base(MODERATOR_GD_21, authenticated_user, level_id) + } + + const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { + SuggestStarsRequest{ + base, + authenticated_user, + level_id, + stars: SuggestedStars::One, + feature: SuggestedFeatureScore::Rate, + gdw: 0 + } + } + + pub fn to_url(&self) -> String { + format!("{}{}", REQUEST_BASE_URL, SUGGEST_STARS_ENDPOINT) + } + + pub fn to_string(&self) -> String { + super::to_string(&self) + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use crate::request::AuthenticatedUser; + use crate::request::moderator::{SuggestedFeatureScore, SuggestedStars, SuggestStarsRequest}; + + const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser { + user_name: "TestUser", + account_id: 472634, + password_hash: Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") + }; + + #[test] + fn serialize_suggest_stars_request() { + let request = SuggestStarsRequest::new(TEST_AUTHENTICATED_USER, 96457938) + .stars(SuggestedStars::Ten) + .feature(SuggestedFeatureScore::Featured); + + assert_eq!( + request.to_string(), + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&levelID=96457938&stars=10&feature=1&gdw=0" + ); + } +} \ No newline at end of file diff --git a/src/request/user.rs b/src/request/user.rs index defb409..a71fe0e 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -19,12 +19,12 @@ pub struct UserRequest<'a> { pub base: BaseRequest<'a>, /// The authenticated user data - pub authenticated_user: Option>, + authenticated_user: Option>, /// The **account ID** (_not_ user ID) of the users whose data to retrieve. /// /// ## GD Internals: - /// This field is called `targetAccountID` in the boomlings API + /// This field is called `targetAccountID` in the Boomlings API #[serde(rename = "targetAccountID")] pub user: u64, } @@ -65,7 +65,7 @@ pub struct UserSearchRequest<'a> { /// Unknown, probably related to pagination /// /// ## GD Internals: - /// This field is called `total` in the boomlings API + /// This field is called `total` in the Boomlings API pub total: u32, /// The page of users to retrieve @@ -76,13 +76,13 @@ pub struct UserSearchRequest<'a> { /// parameters useless. /// /// ## GD Internals: - /// This field is called `page` in the boomlings API + /// This field is called `page` in the Boomlings API pub page: u32, /// The name of the user being searched for /// /// ## GD Internals: - /// This field is called `str` in the boomlings API + /// This field is called `str` in the Boomlings API #[serde(rename = "str")] pub search_string: &'a str, } diff --git a/tests/moderator.rs b/tests/moderator.rs new file mode 100644 index 0000000..c6b633a --- /dev/null +++ b/tests/moderator.rs @@ -0,0 +1,37 @@ +use dash_rs::request::account::LoginRequest; +use dash_rs::request::{CONTENT_TYPE, URL_FORM_ENCODED}; +use dash_rs::request::moderator::{SuggestedFeatureScore, SuggestedStars, SuggestStarsRequest}; + +#[tokio::test] +async fn suggest_level() { + dotenv::from_filename("test_env.env").expect("test_env.env file not found"); + + let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); + let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); + let client = reqwest::Client::new(); + + let request = LoginRequest::default() + .user_name(&user_name) + .password(&password); + + let login_response = request.to_authenticated_user() + .await + .unwrap(); + + let suggest_level_request = SuggestStarsRequest::new(login_response, 95524621) + .feature(SuggestedFeatureScore::Featured) + .stars(SuggestedStars::Ten); + + println!("{}", &suggest_level_request.to_string()); + let suggest_level_response = client.post(suggest_level_request.to_url()) + .body(suggest_level_request.to_string()) + .header(CONTENT_TYPE, URL_FORM_ENCODED) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!(suggest_level_response, "1") +} \ No newline at end of file From 5df3ce44aa9c90d3966176f9a26a29ebe37ff33f Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 11 Jan 2024 16:32:39 -0800 Subject: [PATCH 23/29] Added Platformer length to models --- src/model/level/mod.rs | 9 +++++++++ src/request/level.rs | 1 + 2 files changed, 10 insertions(+) diff --git a/src/model/level/mod.rs b/src/model/level/mod.rs index 63f2827..c3e2fc6 100644 --- a/src/model/level/mod.rs +++ b/src/model/level/mod.rs @@ -61,6 +61,13 @@ pub enum LevelLength { /// This variant is represented by the value `4` in both requests and /// responses ExtraLong, + + /// Platformer, referred to as `Plat.` in game + /// + /// ## GD Internals: + /// This variant is represented by the value `5` in both requests and + /// responses + Platformer, } impl From for LevelLength { @@ -71,6 +78,7 @@ impl From for LevelLength { 2 => LevelLength::Medium, 3 => LevelLength::Long, 4 => LevelLength::ExtraLong, + 5 => LevelLength::Platformer, _ => LevelLength::Unknown(i) } } @@ -84,6 +92,7 @@ impl From for u8 { LevelLength::Medium => 2, LevelLength::Long => 3, LevelLength::ExtraLong => 4, + LevelLength::Platformer => 5, LevelLength::Unknown(inner) => inner } } diff --git a/src/request/level.rs b/src/request/level.rs index 5db2d0e..252a7ae 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -617,6 +617,7 @@ impl Serialize for LengthFilter { LevelLength::Medium => 2, LevelLength::Long => 3, LevelLength::ExtraLong => 4, + LevelLength::Platformer => 5 }; serializer.serialize_u8(numerical_value) From ac55aded9184d5d14cc87fbda539481b25dbdff8 Mon Sep 17 00:00:00 2001 From: Ryder Date: Thu, 11 Jan 2024 22:38:06 -0800 Subject: [PATCH 24/29] Added Epic, Legendary, and Mythic sends --- src/request/moderator.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/request/moderator.rs b/src/request/moderator.rs index 4ff2c12..c645337 100644 --- a/src/request/moderator.rs +++ b/src/request/moderator.rs @@ -125,14 +125,33 @@ pub enum SuggestedFeatureScore { /// Suggest the level with a Featured feature score /// ## GD Internals: /// This variant is represented by the numeric value `1` in the Boomlings API - Featured + Featured, + + /// Suggest the level with a Epic feature score + /// ## GD Internals: + /// This variant is represented by the numeric value `2` in the Boomlings API + Epic, + + /// Suggest the level with a Legendary feature score + /// ## GD Internals: + /// This variant is represented by the numeric value `3` in the Boomlings API + Legendary, + + /// Suggest the level with a Mythic feature score + /// ## GD Internals: + /// This variant is represented by the numeric value `4` in the Boomlings API + Mythic } impl From for u8 { fn from(feature_score: SuggestedFeatureScore) -> Self { match feature_score { SuggestedFeatureScore::Rate => 0, - SuggestedFeatureScore::Featured => 1 + SuggestedFeatureScore::Featured => 1, + SuggestedFeatureScore::Epic => 2, + SuggestedFeatureScore::Legendary => 3, + SuggestedFeatureScore::Mythic => 4, + } } } From 16654feb492468801958309db64f38d2a144e620 Mon Sep 17 00:00:00 2001 From: Ryder Date: Sat, 12 Oct 2024 09:52:01 -0700 Subject: [PATCH 25/29] Remove login auth user calls --- src/request/account.rs | 60 +++++++++++++++++++++------------------- src/request/comment.rs | 15 +++++----- src/request/mod.rs | 25 ----------------- src/request/moderator.rs | 14 ++++++---- src/request/user.rs | 14 +++++----- tests/account.rs | 24 ---------------- tests/comment.rs | 30 ++++++++------------ tests/moderator.rs | 19 +++++-------- tests/profile.rs | 19 +++++-------- 9 files changed, 80 insertions(+), 140 deletions(-) delete mode 100644 tests/account.rs diff --git a/src/request/account.rs b/src/request/account.rs index 96b593d..c70a1e1 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -1,13 +1,11 @@ +use std::borrow::Cow; use std::fmt; use std::fmt::Formatter; -use reqwest::{Error, Response}; use serde::{Serialize}; use crate::{ - util, request::{REQUEST_BASE_URL} }; -use crate::request::{AuthenticatedUser}; pub const ACCOUNT_LOGIN_ENPOINT: &str = "accounts/loginGJAccount.php"; pub const XOR_KEY: &str = "37526"; @@ -63,34 +61,38 @@ impl<'a> LoginRequest<'a> { fn to_string(&self) -> String { super::to_string(&self) } +} - async fn execute(&self) -> Result { - let reqwest_client = reqwest::Client::new(); - reqwest_client - .post(self.to_url()) - .body(self.to_string()) - .header(CONTENT_TYPE, URL_FORM_ENCODED) - .send() - .await - } +#[derive(Debug, Serialize, Default, PartialEq, Eq, Clone, Hash)] +pub struct AuthenticatedUser<'a> { + /// The username of the authenticated user + /// + /// ## GD Internals: + /// This field is called `userName` in the Boomlings API + #[serde(rename = "userName")] + pub user_name: &'a str, + + /// The account ID of the authenticated user + /// + /// ## GD Internals: + /// This field is called `accountID` in the Boomlings API + #[serde(rename = "accountID")] + pub account_id: u64, + + /// The encrypted password of the authenticated user, this is sensitive data as it can be used to act as a user on endpoints requiring `gjp` + /// + /// ## GD Internals: + /// This field is called `gjp` in the Boomlings API + #[serde(rename = "gjp")] + password_hash: Cow<'a, str> +} - pub async fn to_authenticated_user(&self) -> Result { - match self.execute().await { - Ok(login_result) => { - let response_body = login_result.text().await.unwrap(); - if response_body.eq("-1") { - return Err(AuthenticationError("invalid credentials".into())) - } - - Ok(AuthenticatedUser { - user_name: self.user_name, - account_id: response_body.splitn(2, ",").next().unwrap().parse::().unwrap(), - password_hash: base64::encode_config(&util::xor(self.password.as_bytes().to_vec(), XOR_KEY.as_bytes()), base64::URL_SAFE).into() - }) - } - Err(login_error) => { - Err(AuthenticationError(login_error.to_string())) - } +impl<'a> AuthenticatedUser<'a> { + pub fn new(user_name: &'a str, account_id: u64, password_hash: Cow<'a, str>) -> Self { + AuthenticatedUser{ + user_name, + account_id, + password_hash, } } } diff --git a/src/request/comment.rs b/src/request/comment.rs index dbc5de0..a3dfd4a 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -2,10 +2,11 @@ use std::borrow::Cow; use crate::{ - request::{BaseRequest, GD_21, REQUEST_BASE_URL, AuthenticatedUser}, + request::{BaseRequest, GD_21, REQUEST_BASE_URL}, util }; use serde::Serialize; +use crate::request::account::AuthenticatedUser; pub const LEVEL_COMMENTS_ENDPOINT: &str = "getGJComments21.php"; pub const PROFILE_COMMENT_ENDPOINT: &str = "getGJAccountComments20.php"; @@ -279,7 +280,6 @@ impl<'a> UploadCommentRequest<'a> { format!("{}{}", REQUEST_BASE_URL, UPLOAD_COMMENT_ENDPOINT) } - pub fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { Self::with_base(GD_21, authenticated_user, level_id) } @@ -365,12 +365,13 @@ mod tests { use std::borrow::Cow; use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest, UploadCommentRequest, DeleteCommentRequest, SortMode}; use crate::request::{AuthenticatedUser}; + use crate::request::account::AuthenticatedUser; - const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser { - user_name: "TestUser", - account_id: 472634, - password_hash: Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") - }; + const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") + ); #[test] fn serialize_level_comments() { diff --git a/src/request/mod.rs b/src/request/mod.rs index 2b5cce8..daa23ad 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -5,7 +5,6 @@ //! making/proxying requests for the Boomlings servers seems rather useless to me, as they already //! contain a lot of Boomlings-specific fields. -use std::borrow::Cow; use crate::{ model::{ GameVersion @@ -128,30 +127,6 @@ impl Default for BaseRequest<'static> { } } -#[derive(Debug, Serialize, Default, PartialEq, Eq, Clone, Hash)] -pub struct AuthenticatedUser<'a> { - /// The user name of the authenticated user - /// - /// ## GD Internals: - /// This field is called `userName` in the Boomlings API - #[serde(rename = "userName")] - pub user_name: &'a str, - - /// The account ID of the authenticated user - /// - /// ## GD Internals: - /// This field is called `accountID` in the Boomlings API - #[serde(rename = "accountID")] - pub account_id: u64, - - /// The encrypted password of the authenticated user, this is sensitive data as it can be used to act as a user on endpoints requiring `gjp` - /// - /// ## GD Internals: - /// This field is called `gjp` in the Boomlings API - #[serde(rename = "gjp")] - password_hash: Cow<'a, str> -} - pub(crate) fn to_string(request: S) -> String { let mut output = Vec::new(); let mut serializer = RequestSerializer::new(&mut output); diff --git a/src/request/moderator.rs b/src/request/moderator.rs index c645337..0e78ab9 100644 --- a/src/request/moderator.rs +++ b/src/request/moderator.rs @@ -1,5 +1,6 @@ use serde::Serialize; -use crate::request::{AuthenticatedUser, BaseRequest, MODERATOR_GD_21, REQUEST_BASE_URL}; +use crate::request::{BaseRequest, MODERATOR_GD_21, REQUEST_BASE_URL}; +use crate::request::account::AuthenticatedUser; pub const SUGGEST_STARS_ENDPOINT: &str = "suggestGJStars20.php"; @@ -188,14 +189,15 @@ impl<'a> SuggestStarsRequest<'a> { #[cfg(test)] mod tests { use std::borrow::Cow; + use crate::request::account::AuthenticatedUser; use crate::request::AuthenticatedUser; use crate::request::moderator::{SuggestedFeatureScore, SuggestedStars, SuggestStarsRequest}; - const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser { - user_name: "TestUser", - account_id: 472634, - password_hash: Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") - }; + const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") + ); #[test] fn serialize_suggest_stars_request() { diff --git a/src/request/user.rs b/src/request/user.rs index a71fe0e..45af33a 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -4,7 +4,7 @@ use crate::{ request::{BaseRequest, GD_21, REQUEST_BASE_URL}, }; use serde::Serialize; -use crate::request::AuthenticatedUser; +use crate::request::account::AuthenticatedUser; pub const GET_USER_ENDPOINT: &str = "getGJUserInfo20.php"; pub const SEARCH_USER_ENDPOINT: &str = "getGJUsers20.php"; @@ -111,14 +111,14 @@ impl<'a> UserSearchRequest<'a> { #[cfg(test)] mod tests { use std::borrow::Cow; - use crate::request::AuthenticatedUser; + use crate::request::account::AuthenticatedUser; use crate::request::user::{UserRequest, UserSearchRequest}; - const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser { - user_name: "TestUser", - account_id: 472634, - password_hash: Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") - }; + const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") + ); #[test] fn serialize_user_request() { diff --git a/tests/account.rs b/tests/account.rs deleted file mode 100644 index 7e57625..0000000 --- a/tests/account.rs +++ /dev/null @@ -1,24 +0,0 @@ -use dash_rs::request; - -#[tokio::test] -async fn login_gj_account_test() { - dotenv::from_filename("test_env.env").expect("test_env.env file not found"); - - let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); - let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); - - let request = request::account::LoginRequest::default() - .user_name(&user_name) - .password(&password); - - let result = request.to_authenticated_user().await.unwrap(); - - assert_eq!( - result.user_name, - "Ryder" - ); - assert_eq!( - result.account_id, - 57903 - ); -} \ No newline at end of file diff --git a/tests/comment.rs b/tests/comment.rs index fa5929e..df180cd 100644 --- a/tests/comment.rs +++ b/tests/comment.rs @@ -1,9 +1,11 @@ +use std::borrow::Cow; use dash_rs::{ request::{ account::LoginRequest, comment::{UploadCommentRequest, DeleteCommentRequest, CommentHistoryRequest, LevelCommentsRequest, ProfileCommentsRequest, SortMode}, }, }; +use dash_rs::request::account::AuthenticatedUser; use dash_rs::response::{parse_get_gj_acccount_comments_response, parse_get_gj_comments_response}; const CONTENT_TYPE: &str = "Content-Type"; @@ -90,13 +92,11 @@ async fn upload_comment() { let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); let client = reqwest::Client::new(); - let login_request = LoginRequest::default() - .user_name(&user_name) - .password(&password); - - let login_response = login_request.to_authenticated_user() - .await - .unwrap(); + let login_response = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") + ); let comment_upload_request = UploadCommentRequest::new(login_response, 76298358) .comment("More tests still ignore me") @@ -117,19 +117,13 @@ async fn upload_comment() { #[tokio::test] async fn delete_comment() { - dotenv::from_filename("test_env.env").expect("test_env.env file not found"); - - let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); - let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); let client = reqwest::Client::new(); - let request = LoginRequest::default() - .user_name(&user_name) - .password(&password); - - let login_response = request.to_authenticated_user() - .await - .unwrap(); + let login_response = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") + ); let comment_history_request = CommentHistoryRequest::new(3713125) .sort_mode(SortMode::Recent) diff --git a/tests/moderator.rs b/tests/moderator.rs index c6b633a..56b3823 100644 --- a/tests/moderator.rs +++ b/tests/moderator.rs @@ -1,22 +1,17 @@ -use dash_rs::request::account::LoginRequest; +use std::borrow::Cow; +use dash_rs::request::account::{AuthenticatedUser, LoginRequest}; use dash_rs::request::{CONTENT_TYPE, URL_FORM_ENCODED}; use dash_rs::request::moderator::{SuggestedFeatureScore, SuggestedStars, SuggestStarsRequest}; #[tokio::test] async fn suggest_level() { - dotenv::from_filename("test_env.env").expect("test_env.env file not found"); - - let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); - let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); let client = reqwest::Client::new(); - let request = LoginRequest::default() - .user_name(&user_name) - .password(&password); - - let login_response = request.to_authenticated_user() - .await - .unwrap(); + let login_response = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") + ); let suggest_level_request = SuggestStarsRequest::new(login_response, 95524621) .feature(SuggestedFeatureScore::Featured) diff --git a/tests/profile.rs b/tests/profile.rs index 7042acd..0906395 100644 --- a/tests/profile.rs +++ b/tests/profile.rs @@ -54,8 +54,9 @@ // save_load_roundtrip!(Profile, PROFILE_STARDUST1971); // load_save_roundtrip!(Profile, PROFILE_STARDUST1971_DATA, PROFILE_STARDUST1971, ":", true); +use std::borrow::Cow; use dash_rs::model::user::ModLevel; -use dash_rs::request::account::LoginRequest; +use dash_rs::request::account::{AuthenticatedUser, LoginRequest}; use dash_rs::request::user::UserRequest; use dash_rs::response::parse_get_gj_user_info_response; @@ -64,19 +65,13 @@ const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; #[tokio::test] async fn test_get_user_profile_with_auth() { - dotenv::from_filename("test_env.env").expect("test_env.env file not found"); - - let user_name = dotenv::var("GJ_ACCOUNT_USERNAME").unwrap(); - let password = dotenv::var("GJ_ACCOUNT_PASSWORD").unwrap(); let client = reqwest::Client::new(); - let login_request = LoginRequest::default() - .user_name(&user_name) - .password(&password); - - let login_response = login_request.to_authenticated_user() - .await - .unwrap(); + let login_response = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") + ); let request = UserRequest::with_authenticated_user(login_response, 57903); From 747e2f6c5bc385b8cb5f24a62dbb0bcedca4c33f Mon Sep 17 00:00:00 2001 From: Ryder Date: Sat, 14 Mar 2026 20:02:58 -0700 Subject: [PATCH 26/29] Merge fixed --- src/model/comment/level.rs | 9 +- src/model/level/internal.rs | 105 +++----- src/model/level/local_level.rs | 368 -------------------------- src/model/level/mod.rs | 75 +++--- src/model/level/online_level.rs | 439 -------------------------------- src/model/user/mod.rs | 4 + src/model/user/profile.rs | 86 ++----- src/request/account.rs | 73 ------ src/request/comment.rs | 44 +++- src/request/level.rs | 6 +- src/request/mod.rs | 13 +- src/request/moderator.rs | 56 ++-- src/request/user.rs | 21 +- src/response.rs | 33 ++- src/serde/thunk.rs | 8 +- 15 files changed, 212 insertions(+), 1128 deletions(-) delete mode 100644 src/model/level/local_level.rs delete mode 100644 src/model/level/online_level.rs diff --git a/src/model/comment/level.rs b/src/model/comment/level.rs index a756c57..2317fc4 100644 --- a/src/model/comment/level.rs +++ b/src/model/comment/level.rs @@ -9,7 +9,6 @@ use crate::{ serde::{Base64Decoder, Thunk}, GJFormat, ProcessError, ThunkProcessor, }; -use crate::model::RobtopForm; #[derive(Debug, Eq, VariantPartialEq, Clone, Deserialize, Serialize, Dash)] pub struct LevelComment<'a> { @@ -18,10 +17,8 @@ pub struct LevelComment<'a> { #[dash(no_index)] pub user: Option>, - /// The id of the the level that this [`CommentEntry`] was posted to - /// - /// ## GD Internals - /// This value is provided at index `1` + /// The id of the level that this [`CommentEntry`] was posted to + #[dash(index = 1)] pub level_id: Option, /// The actual content of the [`LevelComment`] made. @@ -79,7 +76,7 @@ impl ThunkProcessor for Color { type Error = ProcessError; type Output<'a> = Color; - fn from_unprocessed(unprocessed: Cow) -> Result, Self::Error> { + fn from_unprocessed(unprocessed: Cow<'_, str>) -> Result, Self::Error> { let mut split = unprocessed.split(','); let r = split.next(); diff --git a/src/model/level/internal.rs b/src/model/level/internal.rs index b9f92b3..d358fad 100644 --- a/src/model/level/internal.rs +++ b/src/model/level/internal.rs @@ -69,9 +69,9 @@ struct InternalLevel<'src> { #[serde(rename = "45")] index_45: Option, #[serde(rename = "46")] - index_46: Option<&'src str>, + index_46: u32, #[serde(rename = "47")] - index_47: Option<&'src str>, + index_47: Option, // Only present sometimes #[serde(skip_serializing_if = "Option::is_none")] @@ -91,7 +91,7 @@ struct InternalLevel<'src> { index_36: Option<&'src str>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "40")] - index_40: Option<&'src str>, + index_40: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "52")] index_52: Option<&'src str>, @@ -127,8 +127,8 @@ impl<'de> Dash<'de> for Level<'de, (), Option, u64> { stars_requested: InternalProxy::from_deserialize_proxy(internal.index_39), is_epic: InternalProxy::from_deserialize_proxy(internal.index_42), object_amount: InternalProxy::from_deserialize_proxy(internal.index_45), - index_46: InternalProxy::from_deserialize_proxy(internal.index_46), - index_47: InternalProxy::from_deserialize_proxy(internal.index_47), + editor_time: InternalProxy::from_deserialize_proxy(internal.index_46), + editor_time_copies: InternalProxy::from_deserialize_proxy(internal.index_47), main_song: if internal.index_35.is_some() { None @@ -172,8 +172,8 @@ impl<'de> Dash<'de> for Level<'de, (), Option, u64> { index_39: self.stars_requested.to_serialize_proxy(), index_42: self.is_epic.to_serialize_proxy(), index_45: self.object_amount.to_serialize_proxy(), - index_46: self.index_46.to_serialize_proxy(), - index_47: self.index_47.to_serialize_proxy(), + index_46: self.editor_time.to_serialize_proxy(), + index_47: self.editor_time_copies.to_serialize_proxy(), index_12: self.main_song.map(|song| song.main_song_id).unwrap_or(0), index_25: self.difficulty == LevelRating::Auto, @@ -212,11 +212,11 @@ impl<'de> Dash<'de> for Level<'de, LevelData<'de>, Option, u64> { password: InternalProxy::from_deserialize_proxy(idx27), time_since_upload: InternalProxy::from_deserialize_proxy(idx28), time_since_update: InternalProxy::from_deserialize_proxy(idx29), - index_36: InternalProxy::from_deserialize_proxy(internal.index_36.unwrap_or_default()), - index_40: InternalProxy::from_deserialize_proxy(internal.index_40.unwrap_or_default()), - index_52: InternalProxy::from_deserialize_proxy(internal.index_52.unwrap_or_default()), - index_53: InternalProxy::from_deserialize_proxy(internal.index_53.unwrap_or_default()), - index_57: InternalProxy::from_deserialize_proxy(internal.index_57.unwrap_or_default()), + extra_string: InternalProxy::from_deserialize_proxy(internal.index_36.unwrap_or_default()), + low_detail_mode: InternalProxy::from_deserialize_proxy(internal.index_40.unwrap_or_default()), + song_ids: InternalProxy::from_deserialize_proxy(internal.index_52.unwrap_or_default()), + sfx_ids: InternalProxy::from_deserialize_proxy(internal.index_53.unwrap_or_default()), + verification_time: InternalProxy::from_deserialize_proxy(internal.index_57.unwrap_or_default()), }, _ => return Err(D::Error::custom("Missing indices for level data!")), }; @@ -241,8 +241,8 @@ impl<'de> Dash<'de> for Level<'de, LevelData<'de>, Option, u64> { stars_requested: InternalProxy::from_deserialize_proxy(internal.index_39), is_epic: InternalProxy::from_deserialize_proxy(internal.index_42), object_amount: InternalProxy::from_deserialize_proxy(internal.index_45), - index_46: InternalProxy::from_deserialize_proxy(internal.index_46), - index_47: InternalProxy::from_deserialize_proxy(internal.index_47), + editor_time: InternalProxy::from_deserialize_proxy(internal.index_46), + editor_time_copies: InternalProxy::from_deserialize_proxy(internal.index_47), main_song: if internal.index_35.is_some() { None @@ -289,8 +289,8 @@ impl<'de> Dash<'de> for Level<'de, LevelData<'de>, Option, u64> { index_39: self.stars_requested.to_serialize_proxy(), index_42: self.is_epic.to_serialize_proxy(), index_45: self.object_amount.to_serialize_proxy(), - index_46: self.index_46.to_serialize_proxy(), - index_47: self.index_47.to_serialize_proxy(), + index_46: self.editor_time.to_serialize_proxy(), + index_47: self.editor_time_copies.to_serialize_proxy(), index_12: self.main_song.map(|song| song.main_song_id).unwrap_or(0), index_25: self.difficulty == LevelRating::Auto, @@ -310,11 +310,11 @@ impl<'de> Dash<'de> for Level<'de, LevelData<'de>, Option, u64> { index_27: Some(index_27.borrow()), index_28: Some(self.level_data.time_since_upload.to_serialize_proxy()), index_29: Some(self.level_data.time_since_update.to_serialize_proxy()), - index_36: Some(self.level_data.index_36.to_serialize_proxy()), - index_40: Some(self.level_data.index_40.to_serialize_proxy()), - index_52: Some(self.level_data.index_52.to_serialize_proxy()), - index_53: Some(self.level_data.index_53.to_serialize_proxy()), - index_57: Some(self.level_data.index_57.to_serialize_proxy()), + index_36: Some(self.level_data.extra_string.to_serialize_proxy()), + index_40: Some(self.level_data.low_detail_mode.to_serialize_proxy()), + index_52: Some(self.level_data.song_ids.to_serialize_proxy()), + index_53: Some(self.level_data.sfx_ids.to_serialize_proxy()), + index_57: Some(self.level_data.verification_time.to_serialize_proxy()), }; internal.serialize(serializer) } @@ -352,57 +352,18 @@ impl InternalProxy for LevelLength { } } -// impl LevelRating { -// fn from_response_value(value: i8) -> LevelRating { -// match value { -// 0 => LevelRating::NotAvailable, -// 10 => LevelRating::Easy, -// 20 => LevelRating::Normal, -// 30 => LevelRating::Hard, -// 40 => LevelRating::Harder, -// 50 => LevelRating::Insane, -// _ => LevelRating::Unknown(value), -// } -// } -// -// fn into_response_value(self) -> i8 { -// match self { -// LevelRating::Unknown(value) => value, -// LevelRating::NotAvailable => 0, -// LevelRating::Easy => 10, -// LevelRating::Normal => 20, -// LevelRating::Hard => 30, -// LevelRating::Harder => 40, -// LevelRating::Insane => 50, -// LevelRating::Demon(demon_rating) => demon_rating.into_response_value(), -// _ => panic!("got {:?}, please handle before calling this function", self), -// } -// } -// } -// -// impl DemonRating { -// fn from_response_value(value: i32) -> DemonRating { -// match value { -// 10 => DemonRating::Easy, -// 20 => DemonRating::Medium, -// 30 => DemonRating::Hard, -// 40 => DemonRating::Insane, -// 50 => DemonRating::Extreme, -// _ => DemonRating::Unknown(value), -// } -// } -// -// fn into_response_value(self) -> i32 { -// match self { -// DemonRating::Unknown(value) => value, -// DemonRating::Easy => 10, -// DemonRating::Medium => 20, -// DemonRating::Hard => 30, -// DemonRating::Insane => 40, -// DemonRating::Extreme => 50, -// } -// } -// } +impl LevelRating { + fn from_response_value(value: i32) -> LevelRating { + match value { + 0 => LevelRating::NotAvailable, + 10 => LevelRating::Easy, + 20 => LevelRating::Normal, + 30 => LevelRating::Hard, + 40 => LevelRating::Harder, + 50 => LevelRating::Insane, + _ => LevelRating::Unknown(value), + } + } fn into_response_value(self) -> i32 { match self { diff --git a/src/model/level/local_level.rs b/src/model/level/local_level.rs deleted file mode 100644 index 4082251..0000000 --- a/src/model/level/local_level.rs +++ /dev/null @@ -1,368 +0,0 @@ -use crate::model::level::object::speed::Speed; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use flate2::read::{GzDecoder, GzEncoder, ZlibDecoder}; -use flate2::Compression; -use std::io::Read; -use crate::model::level::{LevelProcessError, Password}; -use crate::{HasRobtopFormat, Thunk, ThunkContent}; -use crate::model::level::object::{LevelObject, ObjectData}; - -#[derive(Debug, PartialEq, Clone, Default, Copy, Serialize, Deserialize)] -pub struct LevelMetadata { - pub starting_speed: Speed, - pub song_offset: f64, - pub song_fade_in: bool, - pub song_fade_out: bool, - pub dual_start: bool, - pub two_player_controls: bool, - pub start_gravity_inverted: bool, - // ... other fields in the metadata section ... -} - -/// Struct encapsulating the additional level data returned when actually downloading a level -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct LevelData<'a> { - /// The level's password - /// - /// ## GD Internals: - /// This value is provided at index `27`. For encoding details, see the documentation on the - /// [`Password`] variants - pub password: Password, - - /// The time passed since the `Level` was uploaded, as a string. Note that these strings are - /// very imprecise, as they are only of the form "x months ago", or similar. - /// - /// TODO: Parse these into an enum - /// - /// ## GD Internals: - /// This value is provided at index `28` - pub time_since_upload: Cow<'a, str>, - - /// The time passed since the `Level` was last updated, as a string. Note that these strings are - /// very imprecise, as they are only of the form "x months ago", or similar. - /// - /// ## GD Internals: - /// This value is provided at index `29` - pub time_since_update: Cow<'a, str>, - - /// According to the GDPS source, this is a value called `extraString` - /// - /// ## GD Internals: - /// This value is provided at index `36` - pub extra_string: Option>, - - /// If this [`Level`] has a low detail option. - /// - /// ## GD Internals - /// This value is provided at index `40` - pub has_low_detail_mode: bool, - - /// The number of seconds this [`Level`] has been opened in the level editor. - /// - /// ## GD Internals - /// This value is provided at index `46` - pub editor_time: Option, - - /// The number of seconds this [`Level`] has been opened in the level editor include the amount from copies. - /// - /// ## GD Internals - /// This value is provided at index `47` - pub copy_editor_time: Option, - - /// The level's actual data. - /// - /// ## GD Internals: - /// This value is provided at index `4`, and is urlsafe base64 encoded and `DEFLATE` compressed - #[serde(borrow)] - pub level_data: Thunk<'a, Objects>, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct Objects { - pub meta: LevelMetadata, - pub objects: Vec, -} - -impl<'a> ThunkContent<'a> for Objects { - type Error = LevelProcessError; - - fn from_unprocessed(unprocessed: &'a str) -> Result { - // Doing the entire base64 in one go is actually faster than using base64::read::DecoderReader and - // having the two readers go back and forth. - let decoded = base64::decode_config(unprocessed, base64::URL_SAFE).map_err(LevelProcessError::Base64)?; - - // Here's the deal: Robtop decompresses all levels by calling the zlib function 'inflateInit2_' with - // the second argument set to 47. This basically tells zlib "this data might be compressed using - // zlib or gzip format, with window size at most 15, but you gotta figure it out yourself". - // However, flate2 doesnt expose this option, so we have to manually determine whether we - // have gzip or zlib compression. - - let mut decompressed = String::new(); - - match &decoded[..2] { - // gz magic bytes - [0x1f, 0x8b] => { - let mut decoder = GzDecoder::new(&decoded[..]); - - decoder.read_to_string(&mut decompressed).map_err(LevelProcessError::Compression)?; - }, - // There's no such thing as "zlib magic bytes", but the first byte stores some information about how the data is compressed. - // '0x78' is the first byte for the compression method robtop used (note: this is only used for very old levels, as he switched - // to gz for newer levels) - [0x78, _] => { - let mut decoder = ZlibDecoder::new(&decoded[..]); - - decoder.read_to_string(&mut decompressed).map_err(LevelProcessError::Compression)?; - }, - _ => return Err(LevelProcessError::UnknownCompression), - } - - let mut iter = decompressed.split_terminator(';'); - - let metadata_string = match iter.next() { - Some(meta) => meta, - None => return Err(LevelProcessError::MissingMetadata), - }; - - let meta = LevelMetadata::from_robtop_str(metadata_string).map_err(|err| LevelProcessError::Deserialize(err.to_string()))?; - - iter.map(|object_string| LevelObject::from_robtop_str(object_string)) - .collect::>() - .map(|objects| Objects { meta, objects }) - .map_err(|err| LevelProcessError::Deserialize(err.to_string())) - } - - fn as_unprocessed(&self) -> Result, LevelProcessError> { - let mut bytes = Vec::new(); - - self.meta.write_robtop_data(&mut bytes).map_err(LevelProcessError::Serialize)?; - - bytes.push(b';'); - - for object in &self.objects { - object.write_robtop_data(&mut bytes).map_err(LevelProcessError::Serialize)?; - bytes.push(b';'); - } - - // FIXME(game specific): Should we remember the compression scheme (zlib or gz) from above, or just - // always re-compress using gz? Since the game dyncamially detects the compression method, we're - // compatible either way. - - let mut encoder = GzEncoder::new(&bytes[..], Compression::new(9)); // TODO: idk what these values mean - let mut compressed = Vec::new(); - - encoder.read_to_end(&mut compressed).map_err(LevelProcessError::Compression)?; - - Ok(Cow::Owned(base64::encode_config(&compressed, base64::URL_SAFE))) - } -} - -impl Objects { - pub fn length_in_seconds(&self) -> f32 { - let mut portals = Vec::new(); - let mut furthest_x = 0.0; - - for object in &self.objects { - if let ObjectData::SpeedPortal { checked: true, speed } = object.metadata { - portals.push((object.x, speed)) - } - - furthest_x = f32::max(furthest_x, object.x); - } - - portals.sort_by(|(x1, _), (x2, _)| x1.partial_cmp(x2).unwrap()); - - get_seconds_from_x_pos(furthest_x, self.meta.starting_speed, &portals) - } -} - -fn get_seconds_from_x_pos(pos: f32, start_speed: Speed, portals: &[(f32, Speed)]) -> f32 { - let mut speed: f32 = start_speed.into(); - - if portals.is_empty() { - return pos / speed - } - - let mut last_obj_pos = 0.0; - let mut total_time = 0.0; - - for (x, portal_speed) in portals { - // distance between last portal and this one - let current_segment = x - last_obj_pos; - - // break if we're past the position we want to calculate the position to - if pos <= current_segment { - break - } - - // Calculate time spent in this segment and add to total time - total_time += current_segment / speed; - - speed = (*portal_speed).into(); - - last_obj_pos = *x; - } - - // add the time spent between end and last portal to total time and return - (pos - last_obj_pos) / speed + total_time -} - -mod internal { - use std::borrow::Cow; - use crate::{DeError, model::level::{ - object::speed::Speed, - local_level::{LevelMetadata, LevelData}, - }, serde::{HasRobtopFormat, IndexedDeserializer, IndexedSerializer, SerError}, Thunk}; - use serde::{Deserialize, Serialize}; - use std::io::Write; - use crate::model::level::local_level::Objects; - use crate::model::level::Password; - use crate::serde::{Internal, RefThunk}; - - impl <'a> HasRobtopFormat<'a> for LevelData<'a> { - fn from_robtop_str(input: &'a str) -> Result { - let internal = InternalLevelData::deserialize(&mut IndexedDeserializer::new(input, ":", true))?; - - Ok(Self { - level_data: match internal.index_4 { - RefThunk::Unprocessed(unproc) => Thunk::Unprocessed(unproc), - _ => unreachable!() - }, - password: internal.index_27.0, - time_since_upload: Cow::Borrowed(internal.index_28), - time_since_update: Cow::Borrowed(internal.index_29), - extra_string: internal.index_36.map(Cow::Borrowed), - has_low_detail_mode: internal.index_40, - editor_time: internal.index_46, - copy_editor_time: internal.index_47 - }) - } - - fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { - let internal = InternalLevelData { - index_4: self.level_data.as_ref_thunk(), - index_27: Internal(self.password), - index_28: self.time_since_upload.as_ref(), - index_29: self.time_since_update.as_ref(), - index_36: self.extra_string.as_deref(), - index_40: self.has_low_detail_mode, - index_46: self.editor_time, - index_47: self.copy_editor_time - }; - - internal.serialize(&mut IndexedSerializer::new(":", writer, true)) - } - } - - impl<'a> HasRobtopFormat<'a> for LevelMetadata { - fn from_robtop_str(input: &'a str) -> Result { - let int = InternalLevelMetadata::deserialize(&mut IndexedDeserializer::new(input, ",", true))?; - - Ok(LevelMetadata { - starting_speed: match int.starting_speed { - 0 => Speed::Slow, - 1 => Speed::Normal, - 2 => Speed::Medium, - 3 => Speed::Fast, - 4 => Speed::VeryFast, - unknown => Speed::Unknown(unknown), - }, - song_offset: int.song_offset, - song_fade_in: int.song_fade_in, - song_fade_out: int.song_fade_out, - dual_start: int.dual_start, - two_player_controls: int.two_player_controls, - start_gravity_inverted: int.start_gravity_inverted, - }) - } - - fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { - let internal = InternalLevelMetadata { - starting_speed: match self.starting_speed { - Speed::Slow => 0, - Speed::Normal => 1, - Speed::Medium => 2, - Speed::Fast => 3, - Speed::VeryFast => 4, - Speed::Unknown(unknown) => unknown, - }, - song_offset: self.song_offset, - song_fade_in: self.song_fade_in, - song_fade_out: self.song_fade_out, - dual_start: self.dual_start, - two_player_controls: self.two_player_controls, - start_gravity_inverted: self.start_gravity_inverted, - }; - - internal.serialize(&mut IndexedSerializer::new(",", writer, true)) - } - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct InternalLevelData<'src, 'bor> { - #[serde(rename = "4")] - index_4: RefThunk<'src, 'bor, Objects>, - #[serde(rename = "27")] - index_27: Internal, - #[serde(rename = "28")] - index_28: &'src str, - #[serde(rename = "29")] - index_29: &'src str, - #[serde(rename = "36")] - index_36: Option<&'src str>, - #[serde(rename = "40")] - index_40: bool, - #[serde(rename = "46")] - index_46: Option, - #[serde(rename = "47")] - index_47: Option, - } - - #[derive(Debug, Serialize, Deserialize, Clone, Copy)] - pub struct InternalLevelMetadata { - #[serde(rename = "kA4", default = "one")] - starting_speed: u8, - - #[serde(rename = "kA13", default)] - song_offset: f64, - - #[serde(rename = "kA15", default)] - song_fade_in: bool, - - #[serde(rename = "kA16", default)] - song_fade_out: bool, - - #[serde(rename = "kA8", default)] - dual_start: bool, - - #[serde(rename = "kA10", default)] - two_player_controls: bool, - - #[serde(rename = "kA11", default)] - start_gravity_inverted: bool, //.. other fields - } - - fn one() -> u8 { - 1 - } -} - -// starting_speed(index = kA4), -// song_offset(index = kA13), -// fade_in(index = kA15), -// fade_out(index = kA16), -// song guidelines: kA16 -// background texture index: kA6 -// ground texture index: kA7 -// ground line index: kA17 -// font: kA18 -// color page (???): kS39 -// starting game mode: kA2 -// starting size: kA3 -// dual_start(index = kA8), -// level/start pos (???): kA9 -// two_player_controls(index = kA10), -// start_gravity_inverted(index = kA11, optional), - - diff --git a/src/model/level/mod.rs b/src/model/level/mod.rs index a73f918..3e8f39f 100644 --- a/src/model/level/mod.rs +++ b/src/model/level/mod.rs @@ -28,21 +28,17 @@ use crate::{ util, Dash, GJFormat, SerError, }; use flate2::Compression; -use crate::model::level::local_level::LevelMetadata; -// use flate2::read::GzDecoder; -// use std::io::Read; mod internal; -pub mod local_level; pub mod object; -pub mod online_level; +pub mod metadata; /// Enum representing the possible level lengths known to dash-rs #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum LevelLength { /// Enum variant that's used by the [`From`](From) impl for when an /// unrecognized value is passed - Unknown(u8), + Unknown(i32), /// Tiny /// @@ -92,7 +88,7 @@ pub enum LevelLength { pub enum LevelRating { /// Enum variant that's used by the [`From`](From) impl for when an /// unrecognized value is passed - Unknown(i8), + Unknown(i32), /// Not Available, sometimes referred to as `N/A` or `NA` /// @@ -160,8 +156,8 @@ impl LevelRating { } } -impl From for LevelRating { - fn from(i: i8) -> Self { +impl From for LevelRating { + fn from(i: i32) -> Self { match i { -3 => LevelRating::Auto, -2 => LevelRating::Demon(DemonRating::Hard), @@ -176,7 +172,7 @@ impl From for LevelRating { } } -impl From for i8 { +impl From for i32 { fn from(rating: LevelRating) -> Self { match rating { LevelRating::Auto => -3, @@ -667,19 +663,17 @@ pub struct Level<'a, Data = LevelData<'a>, Song = Option, User = u64> { /// in version 2.1 or later. For all older levels this is always `None` pub object_amount: Option, - /// According to the GDPS source this is always `1`, although that is - /// evidently wrong + /// The total number of seconds spent on this copy of a level /// /// ## GD Internals: - /// This value is provided at index `46` and seems to be an integer - pub index_46: Option>, + /// This value is provided at index `46` and is an integer + pub editor_time: u32, - /// According to the GDPS source, this is always `2`, although that is - /// evidently wrong + /// The cumulative total of seconds spent on previous copies of the level /// /// ## GD Internals: - /// This value is provided at index `47` and seems to be an integer - pub index_47: Option>, + /// This value is provided at index `47` and is an integer + pub editor_time_copies: Option, /// Additional data about this level that can be retrieved by downloading the level. /// @@ -718,8 +712,8 @@ impl<'a, Data, Song, User> Level<'a, Data, Song, User> { stars_requested: self.stars_requested, is_epic: self.is_epic, object_amount: self.object_amount, - index_46: self.index_46, - index_47: self.index_47, + editor_time: self.editor_time, + editor_time_copies: self.editor_time_copies, } } @@ -747,8 +741,8 @@ impl<'a, Data, Song, User> Level<'a, Data, Song, User> { stars_requested: self.stars_requested, is_epic: self.is_epic, object_amount: self.object_amount, - index_46: self.index_46, - index_47: self.index_47, + editor_time: self.editor_time, + editor_time_copies: self.editor_time_copies, level_data: self.level_data, } } @@ -777,8 +771,8 @@ impl<'a, Data, Song, User> Level<'a, Data, Song, User> { stars_requested: self.stars_requested, is_epic: self.is_epic, object_amount: self.object_amount, - index_46: self.index_46, - index_47: self.index_47, + editor_time: self.editor_time, + editor_time_copies: self.editor_time_copies, level_data: self.level_data, } } @@ -825,19 +819,38 @@ pub struct LevelData<'a> { /// This value is provided at index `29` pub time_since_update: Cow<'a, str>, - /// According to the GDPS source, this is a value called `extraString` + /// The extraString passed when uploading the level. Its use is currently unknown /// /// ## GD Internals: /// This value is provided at index `36` - pub index_36: Cow<'a, str>, + pub extra_string: Cow<'a, str>, - pub index_40: Cow<'a, str>, + /// Whether this level has a low detail mode option + /// + /// ## GD Internals: + /// This value is provided at index `40` + pub low_detail_mode: bool, - pub index_52: Cow<'a, str>, + /// The comma separated list of all song IDs used in the level + /// + /// ## GD Internals: + /// This value is provided at index `52` + /// TODO: Parse into a List + pub song_ids: Cow<'a, str>, - pub index_53: Cow<'a, str>, + /// The comma separated list of all sfx IDs used in the level + /// + /// ## GD Internals: + /// This value is provided at index `52` + /// TODO: Parse into a List + pub sfx_ids: Cow<'a, str>, - pub index_57: Cow<'a, str>, + /// The amount of time spent verifying the level in frames assuming 240FPS + /// + /// ## GD Internals: + /// This value is provided at index `52` + /// TODO: Figure out how to parse thise + pub verification_time: Cow<'a, str>, } #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -920,7 +933,7 @@ impl ThunkProcessor for Objects { .map_err(|err| LevelProcessError::Deserialize(err.to_string())) } - fn as_unprocessed(processed: &Objects) -> Result, LevelProcessError> { + fn as_unprocessed(processed: &Objects) -> Result, LevelProcessError> { let mut bytes = Vec::new(); processed.meta.write_gj(&mut bytes)?; diff --git a/src/model/level/online_level.rs b/src/model/level/online_level.rs deleted file mode 100644 index b9ba195..0000000 --- a/src/model/level/online_level.rs +++ /dev/null @@ -1,439 +0,0 @@ -use std::{ - borrow::Cow, -}; -use serde::{ - Deserialize, Serialize, -}; -use crate::{ - Base64Decoded, Thunk, - model::{ - GameVersion, - creator::Creator, - song::{MainSong, NewgroundsSong}, - level::{ - Featured, LevelLength, LevelRating, - local_level::LevelData, - }, - }, -}; - -pub type ListedLevel<'a> = Level<'a, Option>, Option>, Option>>; - -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] -pub struct Level<'a, Data = Option>, Song = Option, User = u64>{ - /// The ID of the [`Level`]. - /// - /// ## GD Internals - /// This value is provided at index `1` - pub level_id: u64, - - /// The name of the [`Level`]. - /// - /// ## GD Internals - /// This value is provided at index `2` - pub name: Cow<'a, str>, - - /// The description of the [`Level`]. - /// - /// ## GD Internals - /// This value is provided at index `3` - #[serde(borrow = "'a")] - pub description: Option>>, - - /// The version of the [`Level`]. - /// - /// ## GD Internals - /// This value is provided at index `5` - pub version: u8, - - /// The [`User`] ID of the [`Creator`] who uploaded this [`Level`]. - /// - /// ## GD Internals - /// This value is provided at index `6` - pub creator: User, - - /// The amount of times this [`Level`] has been downloaded. - /// - /// ## GD Internals - /// This value is provided at index `10` - pub downloads: u64, - - /// The amount of people who have completed this [`Level`]. - /// This is removed in update 2.1 - /// - /// ## GD Internals - /// This value is provided at index `11` - pub set_completes: Option, - - /// The main song used by this [`Level`]. - /// If this level does not use a main song, then the value is none. - /// - /// ## GD Internals - /// This value is provided at index `12` - pub main_song: Option, - - /// The game version this [`Level`] was updated in. - /// - /// ## GD Internals - /// This value is provided at index `13` - pub game_version: GameVersion, - - /// The number of likes this [`Level`] has. - /// - /// ## GD Internals - /// This value is provided at index `14` - pub likes: i32, - - /// The length of this [`Level`] has. - /// - /// ## GD Internals - /// This value is provided at index `15` - pub length: LevelLength, - - /// The amount of stars this [`Level`] awards upon completion. - /// - /// ## GD Internals - /// This value is provided at index `18` - pub stars: u8, - - /// The feature score this [`Level`] has. - /// If the [`Level`] is not featured, then this value is set to 0. - /// - /// ## GD Internals - /// This value is provided at index `19` - pub feature_score: Featured, - - /// The ID this [`Level`] was copied from. - /// If the [`Level`] is not copied, this value is `None` - /// - /// ## GD Internals - /// This value is provided at index `30` - pub copy_of: Option, - - /// If this [`Level`] has two player mode enabled. - /// - /// ## GD Internals - /// This value is provided at index `31` - pub is_two_player: bool, - - /// The custom song used by this [`Level`]. - /// If this level does not use a custom song, then the value is 0. - /// - /// ## GD Internals - /// This value is provided at index `35` - pub custom_song: Song, - - /// The number of coins this [`Level`] has. - /// - /// ## GD Internals - /// This value is provided at index `37` - pub coin_amount: u8, - - /// If this [`Level`] has verified coins. - /// - /// ## GD Internals - /// This value is provided at index `38` - pub has_verified_coins: bool, - - /// The amount of starts requested for this [`Level`]. - /// - /// ## GD Internals - /// This value is provided at index `39` - pub stars_requested: Option, - - /// The number that this daily or weekly [`Level`] was. - /// - /// ## GD Internals - /// This value is provided at index `41` - pub daily_number: Option, - - /// If this [`Level`] is rated Epic. - /// - /// ## GD Internals - /// This value is provided at index `42` - pub is_epic: bool, - - /// The difficulty of this [`Level`] - /// - /// ## GD Internals: - /// This value is a construct from the value at the indices `9`, `17` and - /// `25`, whereas index 9 is an integer representation of either the - /// [`LevelRating`] or the [`DemonRating`] - /// struct, depending on the value of index 17. - /// - /// If index 25 is set to true, the level is an auto level and the value at - /// index 9 is some nonsense, in which case it is ignored. - pub difficulty: LevelRating, - - /// If this [`Level`] is in a Gauntlet. - /// - /// ## GD Internals - /// This value is provided at index `44` - pub in_gauntlet: Option, - - /// The number of objects this [`Level`] has. - /// This value will be `None` for levels uploaded before update 2.1 - /// - /// ## GD Internals - /// This value is provided at index `45` - pub object_count: Option, - - /// The encoded data of the [`Level`]. - /// This data is only returned when downloading a level - /// - /// ## GD Internals - /// This value is provided at index `4` - pub level_data: Data, -} - - -#[cfg(test)] -mod tests { - -} - -mod internal { - use crate::model::{ - song::MainSong, - level::{ - DemonRating, Featured, LevelRating, Password, - online_level::Level, - local_level::{LevelData, Objects}, - } - }; - use crate::serde::Internal; - - #[allow(non_upper_case_globals, unused_imports)] - const _level: () = { - use crate::{ - serde::{DeError, HasRobtopFormat, IndexedDeserializer, IndexedSerializer, PercentDecoded, SerError, Thunk, RefThunk, Base64Decoded}, - }; - use serde::{Deserialize, Serialize}; - use std::{borrow::{Cow, Borrow}, io::Write}; - #[derive(Serialize, Deserialize)] - struct InternalLevel<'src, 'bor> { - #[serde(rename = "1")] - index_1: u64, - #[serde(rename = "2")] - index_2: &'src str, - #[serde(rename = "3")] - index_3: Option>>, - #[serde(rename = "4")] - index_4: Option>, - #[serde(rename = "5")] - index_5: u8, - #[serde(rename = "6")] - index_6: u64, - #[serde(serialize_with = "crate::util::true_to_ten")] - #[serde(rename = "8")] - index_8: bool, - #[serde(rename = "9")] - index_9: i8, - #[serde(rename = "10")] - index_10: u64, - #[serde(rename = "11")] - index_11: Option, - #[serde(rename = "12")] - index_12: u8, - #[serde(rename = "13")] - index_13: u8, - #[serde(rename = "14")] - index_14: i32, - #[serde(rename = "15")] - index_15: u8, - #[serde(rename = "17")] - index_17: bool, - #[serde(rename = "18")] - index_18: u8, - #[serde(rename = "19")] - index_19: Featured, - #[serde(serialize_with = "crate::util::false_to_empty_string")] - #[serde(rename = "25")] - index_25: bool, - #[serde(rename = "27")] - index_27: Option>, - #[serde(rename = "28")] - index_28: Option<&'src str>, - #[serde(rename = "29")] - index_29: Option<&'src str>, - // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] - #[serde(rename = "30")] - index_30: Option, - #[serde(rename = "31")] - index_31: bool, - // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] - #[serde(rename = "35")] - index_35: Option, - #[serde(rename = "36")] - index_36: Option<&'src str>, - #[serde(rename = "37")] - index_37: u8, - #[serde(rename = "38")] - index_38: bool, - #[serde(rename = "39")] - index_39: Option, - #[serde(rename = "40")] - index_40: Option, - // #[serde(deserialize_with = "crate::util::negative_or_zero_u32_to_none")] - #[serde(rename = "41")] - index_41: Option, - #[serde(rename = "42")] - index_42: bool, - #[serde(rename = "43")] - index_43: u8, - #[serde(rename = "44")] - index_44: Option, - #[serde(rename = "45")] - index_45: Option, - // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] - #[serde(rename = "46")] - index_46: Option, - // #[serde(deserialize_with = "crate::util::negative_or_zero_u64_to_none")] - #[serde(rename = "47")] - index_47: Option, - } - - impl<'src> HasRobtopFormat<'src> for Level<'src> { - fn from_robtop_str(input: &'src str) -> Result { - let internal = InternalLevel::deserialize(&mut IndexedDeserializer::new(input, ":", true))?; - Ok(Self { - level_id: internal.index_1, - name: Cow::Borrowed(internal.index_2), - description: match internal.index_3 { - None => None, - Some(RefThunk::Unprocessed(unproc)) => Some(Thunk::Unprocessed(unproc)), - _ => unreachable!() - }, - level_data: match internal.index_4 { - None => None, - Some(_data) => { - Some(LevelData::from_robtop_str(input)?) - } - }, - version: internal.index_5, - creator: internal.index_6, - downloads: internal.index_10, - set_completes: match internal.index_11 { - None => None, - Some(set_complete) => Some(set_complete) - }, - main_song: match internal.index_35 { - // Main song - Some(0) => Some(MainSong::from(internal.index_12)), - Some(_custom_song) => { - None - } - None => unreachable!(), - }, - game_version: internal.index_13.into(), - likes: internal.index_14, - length: internal.index_15.into(), - stars: internal.index_18, - feature_score: internal.index_19, - copy_of: internal.index_30, - is_two_player: internal.index_31, - custom_song: internal.index_35, - coin_amount: internal.index_37, - has_verified_coins: internal.index_38, - stars_requested: internal.index_39, - daily_number: internal.index_41, - is_epic: internal.index_42, - difficulty: if !internal.index_8 { - // has_difficulty_rating - LevelRating::NotAvailable - } - else if internal.index_25 { - // is_auto - LevelRating::Auto - } - else if internal.index_17 { - // is_demon - LevelRating::Demon(internal.index_9.into()) - } - else { - // rating - internal.index_9.into() - }, - in_gauntlet: match internal.index_44 { - None => None, - Some(flag) => Some(flag), - }, - object_count: internal.index_45, - }) - } - fn write_robtop_data(&self, writer: W) -> Result<(), SerError> { - let internal = InternalLevel { - index_1: self.level_id, - index_2: self.name.as_ref(), - index_3: self.description.as_ref().map(|t| t.as_ref_thunk()), - index_4: None, - index_5: self.version, - index_6: self.creator, - index_25: self.difficulty == LevelRating::Auto, - index_8: self.difficulty != LevelRating::NotAvailable, - index_9: self.difficulty.into(), - index_17: self.difficulty.is_demon(), - index_10: self.downloads, - index_11: self.set_completes, - index_12: self.main_song.map(|song| song.main_song_id).unwrap_or(0), - index_13: self.game_version.into(), - index_14: self.likes, - index_15: self.length.into(), - index_18: self.stars, - index_19: self.feature_score, - index_27: None, - index_28: None, - index_29: None, - index_30: match self.copy_of { - Some(0) => None, - Some(id) => Some(id), - None => None - }, - index_31: self.is_two_player, - index_35: match self.custom_song { - Some(0) => None, - Some(id) => Some(id), - None => None - }, - index_36: None, - index_37: self.coin_amount, - index_38: self.has_verified_coins, - index_39: match self.stars_requested { - Some(0) => None, - Some(id) => Some(id), - None => None - }, - index_40: None, - index_41: match self.daily_number { - Some(daily_number) => Some(daily_number), - None => None - }, - index_42: self.is_epic, - index_43: match self.difficulty { - LevelRating::Demon(DemonRating::Easy) => 3, - LevelRating::Demon(DemonRating::Medium) => 4, - LevelRating::Demon(DemonRating::Hard) => 0, - LevelRating::Demon(DemonRating::Insane) => 5, - LevelRating::Demon(DemonRating::Extreme) => 6, - _ => 5, // this seems to be the default for non-demons - }, - index_44: self.in_gauntlet, - index_45: match self.object_count { - Some(0) => None, - Some(id) => Some(id), - None => None - }, - index_46: None, - index_47: None - }; - - if let Some(level_data) = &self.level_data { - let bytes = Vec::new(); - level_data.write_robtop_data(bytes)?; - } - - internal.serialize(&mut IndexedSerializer::new(":", writer, true)) - } - } - }; -} \ No newline at end of file diff --git a/src/model/user/mod.rs b/src/model/user/mod.rs index f0b4d58..bd19896 100644 --- a/src/model/user/mod.rs +++ b/src/model/user/mod.rs @@ -370,3 +370,7 @@ impl From for u8 { crate::into_conversion!(Color, u8); crate::into_conversion!(IconType, u8); +crate::into_conversion!(MessageState, u8); +crate::into_conversion!(FriendsState, u8); +crate::into_conversion!(FriendState, u8); +crate::into_conversion!(CommentHistoryState, u8); diff --git a/src/model/user/profile.rs b/src/model/user/profile.rs index 377c307..34fd4c3 100644 --- a/src/model/user/profile.rs +++ b/src/model/user/profile.rs @@ -62,17 +62,12 @@ pub struct Profile<'a> { /// The global leaderboard position for this [`Profile`]. /// This data is returned when requesting leaderboard data, otherwise `None`. - /// - /// ## GD Internals: - /// This value is provided at index `6` + #[dash(index = 6)] pub leaderboard_position: Option, /// The account ID for this [`Profile`], just used to highlight leaderboard ranking. /// This data is returned when requesting leaderboard data, otherwise `None`. - /// - /// - /// ## GD Internals: - /// This value is provided at index `7` + #[dash(index = 7)] pub account_highlight: Option, /// The amount of creator points this [`Profile`] was awarded. @@ -81,10 +76,8 @@ pub struct Profile<'a> { /// The icon this [`Profile`] has displayed for leaderboards and comments. /// This data is returned when requesting leaderboard or comment data, otherwise `None`. - /// - /// ## GD Internals: - /// This value is provided at index `9` - pub icon_id: Option, + #[dash(index = 9)] + pub icon_id: IconType, /// This [`Profile`]'s primary color /// @@ -107,13 +100,6 @@ pub struct Profile<'a> { #[dash(index = 13)] pub secret_coins: u8, - /// The icon type this [`Profile`] has displayed for leaderboards and comments. - /// This data is returned when requesting leaderboard or comment data, otherwise `None`. - /// - /// ## GD Internals: - /// This value is provided at index `14` - pub icon_type: Option, - /// The [`Profile`]'s unique account ID #[dash(index = 16)] pub account_id: u64, @@ -123,13 +109,11 @@ pub struct Profile<'a> { pub user_coins: u16, /// The privacy option for messages that this ['Profile'] has set - /// ## GD Internals: - /// This value is provided at index `18` + #[dash(index = 18)] pub message_state: MessageState, /// The privacy option for friends that this ['Profile'] has set - /// ## GD Internals: - /// This value is provided at index `19` + #[dash(index = 19)] pub friends_state: FriendsState, /// The link to the [`Profile`]'s [YouTube](https://youtube.com) channel, if provided @@ -139,32 +123,32 @@ pub struct Profile<'a> { #[dash(index = 20)] pub youtube_url: Option>, - /// The 1-based index of the cube this [`Profile`] currently uses. Indexing of icons starts at + /// The 1-based index of the cube this [`Profile`] currently uses. Indexing of icons starts in /// the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 21)] pub cube_index: u16, - /// The 1-based index of the ship this [`Profile`] currently uses. Indexing of icons starts at + /// The 1-based index of the ship this [`Profile`] currently uses. Indexing of icons starts in /// the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 22)] pub ship_index: u8, - /// The 1-based index of the ball this [`Profile`] currently uses. Indexing of icons starts at + /// The 1-based index of the ball this [`Profile`] currently uses. Indexing of icons starts in /// the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 23)] pub ball_index: u8, - /// The 1-based index of the UFO this [`Profile`] currently uses. Indexing of icons starts at + /// The 1-based index of the UFO this [`Profile`] currently uses. Indexing of icons starts in /// the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 24)] pub ufo_index: u8, - /// The 1-based index of the wave this [`Profile`] currently uses. Indexing of icons starts at + /// The 1-based index of the wave this [`Profile`] currently uses. Indexing of icons starts in /// the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 25)] pub wave_index: u8, - /// The 1-based index of the robot this [`Profile`] currently uses. Indexing of icons starts at + /// The 1-based index of the robot this [`Profile`] currently uses. Indexing of icons starts in /// the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 26)] pub robot_index: u8, @@ -174,60 +158,43 @@ pub struct Profile<'a> { pub has_glow: bool, /// Flag for if this [`Profile`] is registered or not. - /// - /// ## GD Internals: - /// This value is provided at index `29`, 0 if not registered, 1 if registed + #[dash(index = 29)] pub is_registered: bool, /// This [`Profile`]'s global rank. [`None`] if he is banned or not ranked. - /// - /// ## GD Internals: - /// For unranked/banned users it's `0`. TODO: Why is this an option? #[dash(index = 30)] pub global_rank: Option, /// Holds the status of the friend request sent to this [`Profile`] by the [`AuthenticatedUser`]. - /// - /// ## GD Internals: - /// This value is provided at index `31` + #[dash(index = 31)] pub friend_state: FriendState, /// Number of unread messages this [`Profile`] has. - /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. - /// - /// ## GD Internals: - /// This value is provided at index `38` + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser`]. + #[dash(index = 38)] pub unread_messages_count: Option, /// Number of unread friend requests this [`Profile`] has. - /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. - /// - /// ## GD Internals: - /// This value is provided at index `39` + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser`]. + #[dash(index = 39)] pub unread_friend_request_count: Option, /// Number of new friend this [`Profile`] has. - /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. - /// - /// ## GD Internals: - /// This value is provided at index `40` + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser`]. + #[dash(index = 40)] pub new_friends_count: Option, /// Flag for if a friend request is new. - /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser']. - /// - /// ## GD Internals: - /// This value is provided at index `41` + /// This data is only returned when requesting a [`Profile`] as your [`AuthenticatedUser`]. + #[dash(index = 41)] pub new_friend_request: Option, /// The time since this [`Profile`] has submitted a new level score. /// This data is only returned when requesting leaderboard data - /// - /// ## GD Internals: - /// This value is provided at index `42` + #[dash(index = 42)] pub time_since_score_update: Option>, - /// The 1-based index of the spider this [`Profile`] currently uses. Indexing of icons starts at + /// The 1-based index of the spider this [`Profile`] currently uses. Indexing of icons starts in /// the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 43)] pub spider_index: u8, @@ -251,7 +218,7 @@ pub struct Profile<'a> { pub diamonds: u16, /// The 1-based index of the death-effect this [`Profile`] currently uses. Indexing of icons - /// starts at the top left corner and then goes left-to-right and top-to-bottom + /// starts in the top left corner and then goes left-to-right and top-to-bottom #[dash(index = 48)] pub death_effect_index: u8, @@ -260,8 +227,7 @@ pub struct Profile<'a> { pub mod_level: ModLevel, /// The privacy option this for viewing comment history [`Profile`] has set. - /// ## GD Internals: - /// This value is provided at index `50` + #[dash(index = 50)] pub comment_history_state: CommentHistoryState, } diff --git a/src/request/account.rs b/src/request/account.rs index c70a1e1..88b8549 100644 --- a/src/request/account.rs +++ b/src/request/account.rs @@ -3,66 +3,10 @@ use std::fmt; use std::fmt::Formatter; use serde::{Serialize}; -use crate::{ - request::{REQUEST_BASE_URL} -}; - -pub const ACCOUNT_LOGIN_ENPOINT: &str = "accounts/loginGJAccount.php"; pub const XOR_KEY: &str = "37526"; pub const CONTENT_TYPE: &str = "Content-Type"; pub const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; -#[derive(Debug, Clone, Serialize, Hash)] -pub struct LoginRequest<'a> { - - /// The Unique Device IDentifier (UDID) of the user to authenticate - /// - /// ## GD Internals: - /// This field is called `udid` in the Boomlings API - /// The value of this value can be randomly generated - /// The digits must be between 100,000 and 100,000,000 - /// This will succeed as long as these conditions are met - pub udid: &'a str, - - /// The username of the user to authenticate - /// - /// ## GD Internals: - /// This field is called `userName` in the Boomlings API - #[serde(rename = "userName")] - pub user_name: &'a str, - - /// The unencrypted password of the user to authenticate - /// - /// ## GD Internals: - /// This field is called `password` in the Boomlings API - password: &'a str, - - /// The secret token to call /database/accounts routes - pub secret: &'a str, -} - -impl<'a> LoginRequest<'a> { - const_setter!(user_name: &'a str); - const_setter!(password: &'a str); - - pub fn default() -> Self { - LoginRequest{ - udid: "100000", - user_name: "", - password: "", - secret: super::ACCOUNT_SECRET - } - } - - fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, ACCOUNT_LOGIN_ENPOINT) - } - - fn to_string(&self) -> String { - super::to_string(&self) - } -} - #[derive(Debug, Serialize, Default, PartialEq, Eq, Clone, Hash)] pub struct AuthenticatedUser<'a> { /// The username of the authenticated user @@ -107,20 +51,3 @@ impl fmt::Display for AuthenticationError { write!(f, "{}", self.0) } } - -#[cfg(test)] -mod tests { - use crate::request::account::LoginRequest; - - #[tokio::test] - async fn serialize_login_request() { - let request = LoginRequest::default() - .user_name("TestUser") - .password("PLAIN_TEXT_PASS_HERE"); - - assert_eq!( - request.to_string(), - "udid=100000&userName=TestUser&password=PLAIN_TEXT_PASS_HERE&secret=Wmfv3899gc9" - ); - } -} diff --git a/src/request/comment.rs b/src/request/comment.rs index 703e5f9..a7d0612 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -75,7 +75,7 @@ pub struct LevelCommentsRequest<'a> { pub level_id: u64, /// The amount of comments to retrieve. Note that while in-game this can only be set to 20 or 40 - /// (via the "load more comments option), the API accepts any value. The max value for this is 100 + /// (via the "load more comments" option), the API accepts any value. The max value for this is 100 /// /// ## GD Internals: /// This field is called `count` in the Boomlings API @@ -243,6 +243,12 @@ impl<'a> CommentHistoryRequest<'a> { } } +impl Display for CommentHistoryRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", super::to_string(self)) + } +} + #[derive(Debug, Clone, Serialize, Hash)] pub struct UploadCommentRequest<'a> { /// The base request data @@ -313,6 +319,12 @@ impl<'a> UploadCommentRequest<'a> { } } +impl Display for UploadCommentRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", super::to_string(self)) + } +} + #[derive(Debug, Clone, Serialize, Hash)] pub struct DeleteCommentRequest<'a> { /// The base request data @@ -334,6 +346,12 @@ pub struct DeleteCommentRequest<'a> { pub level_id: u64, } +impl Display for ProfileCommentsRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", super::to_string(self)) + } +} + impl<'a> DeleteCommentRequest<'a> { const_setter!(comment_id: u64); @@ -357,7 +375,7 @@ impl<'a> DeleteCommentRequest<'a> { } } -impl Display for ProfileCommentsRequest<'_> { +impl Display for DeleteCommentRequest<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", super::to_string(self)) } @@ -369,12 +387,6 @@ mod tests { use crate::request::comment::{LevelCommentsRequest, ProfileCommentsRequest, CommentHistoryRequest, UploadCommentRequest, DeleteCommentRequest, SortMode}; use crate::request::account::AuthenticatedUser; - const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser::new( - "Ryder", - 57903, - Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") - ); - #[test] fn serialize_level_comments() { if let Err(err) = env_logger::builder().is_test(true).try_init() { @@ -430,13 +442,18 @@ mod tests { eprintln!("Error setting up env_logger: {:?}", err) } - let request = UploadCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632) + let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") + ); + let request = UploadCommentRequest::new(test_authenticated_user, 85179632) .comment("This is a test comment") .percent(56); assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&comment=VGhpcyBpcyBhIHRlc3QgY29tbWVudA==&levelID=85179632&percent=56&chk=UQsGAAEACgQBVQBaAwoGVwtSDQIEWAYOUFEAVQoIBVtWDwEHDQEJVA==" + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=Ryder&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&comment=VGhpcyBpcyBhIHRlc3QgY29tbWVudA==&levelID=85179632&percent=56&chk=UQsGAAEACgQBVQBaAwoGVwtSDQIEWAYOUFEAVQoIBVtWDwEHDQEJVA==" ); } @@ -447,7 +464,12 @@ mod tests { eprintln!("Error setting up env_logger: {:?}", err) } - let request = DeleteCommentRequest::new(TEST_AUTHENTICATED_USER, 85179632, 7000000); + let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") + ); + let request = DeleteCommentRequest::new(test_authenticated_user, 85179632, 7000000); assert_eq!( request.to_string(), diff --git a/src/request/level.rs b/src/request/level.rs index d3e462c..a8411d5 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -598,7 +598,7 @@ impl Serialize for DemonFilter { DemonRating::Extreme => 5, }; - serializer.serialize_i8(numerical_value) + serializer.serialize_i32(numerical_value) } } @@ -621,7 +621,7 @@ impl Serialize for LengthFilter { LevelLength::Platformer => 5, }; - serializer.serialize_u8(numerical_value) + serializer.serialize_i32(numerical_value) } } @@ -647,7 +647,7 @@ impl Serialize for RatingFilter { * -2 means "search for any demon, regardless of difficulty" */ }; - serializer.serialize_i8(numerical_value) + serializer.serialize_i32(numerical_value) } } diff --git a/src/request/mod.rs b/src/request/mod.rs index 5e35cff..8813577 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -72,15 +72,10 @@ pub const URL_FORM_ENCODED: &str = "application/x-www-form-urlencoded"; /// A `BaseRequest` instance that has all its fields set to the /// same values a Geometry Dash 2.1 client would use -pub const GD_21: BaseRequest = BaseRequest::new( - GameVersion::Version { major: 2, minor: 1 }, - GameVersion::Version { major: 3, minor: 3 }, - SECRET, -); -pub const MODERATOR_GD_21: BaseRequest = BaseRequest::new( - GameVersion::Version { major: 2, minor: 1 }, - GameVersion::Version { major: 3, minor: 3 }, +pub const MODERATOR_GD_22: BaseRequest = BaseRequest::new( + GameVersion::Version { major: 2, minor: 2 }, + GameVersion::Version { major: 3, minor: 8 }, MODERATOR_SECRET, ); @@ -89,7 +84,7 @@ pub const MODERATOR_GD_21: BaseRequest = BaseRequest::new( pub const GD_22: BaseRequest = BaseRequest::new( GameVersion::Version { major: 2, minor: 2 }, GameVersion::Version { major: 3, minor: 8 }, - "Wmfd2893gb7", + SECRET, ); /// Base data included in every request made diff --git a/src/request/moderator.rs b/src/request/moderator.rs index 0e78ab9..fd53140 100644 --- a/src/request/moderator.rs +++ b/src/request/moderator.rs @@ -1,10 +1,11 @@ +use std::fmt::Display; use serde::Serialize; -use crate::request::{BaseRequest, MODERATOR_GD_21, REQUEST_BASE_URL}; +use crate::request::{endpoint_base_url, BaseRequest, MODERATOR_GD_22}; use crate::request::account::AuthenticatedUser; pub const SUGGEST_STARS_ENDPOINT: &str = "suggestGJStars20.php"; -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] pub struct SuggestStarsRequest<'a> { /// The base request data pub base: BaseRequest<'a>, @@ -34,64 +35,64 @@ pub struct SuggestStarsRequest<'a> { pub gdw: u8 } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Hash)] #[serde(into = "u8")] pub enum SuggestedStars { - /// Suggest the level with a one star rating + /// Suggest the level with a one-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `1` in the Boomlings API One, - /// Suggest the level with a two star rating + /// Suggest the level with a two-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `2` in the Boomlings API Two, - /// Suggest the level with a three star rating + /// Suggest the level with a three-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `3` in the Boomlings API Three, - /// Suggest the level with a four star rating + /// Suggest the level with a four-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `4` in the Boomlings API Four, - /// Suggest the level with a five star rating + /// Suggest the level with a five-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `5` in the Boomlings API Five, - /// Suggest the level with a six star rating + /// Suggest the level with a six-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `6` in the Boomlings API Six, - /// Suggest the level with a seven star rating + /// Suggest the level with a seven-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `7` in the Boomlings API Seven, - /// Suggest the level with an eight star rating + /// Suggest the level with an eight-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `8` in the Boomlings API Eight, - /// Suggest the level with a nine star rating + /// Suggest the level with a nine-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `9` in the Boomlings API Nine, - /// Suggest the level with a ten star rating + /// Suggest the level with a ten-star rating /// /// ## GD Internals: /// This variant is represented by the numeric value `10` in the Boomlings API @@ -115,7 +116,7 @@ impl From for u8 { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Hash)] #[serde(into = "u8")] pub enum SuggestedFeatureScore { /// Suggest the level with a Rate feature score @@ -163,7 +164,7 @@ impl<'a> SuggestStarsRequest<'a> { const_setter!(feature: SuggestedFeatureScore); pub const fn new(authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { - Self::with_base(MODERATOR_GD_21, authenticated_user, level_id) + Self::with_base(MODERATOR_GD_22, authenticated_user, level_id) } const fn with_base(base: BaseRequest<'a>, authenticated_user: AuthenticatedUser<'a>, level_id: u64) -> Self { @@ -178,11 +179,13 @@ impl<'a> SuggestStarsRequest<'a> { } pub fn to_url(&self) -> String { - format!("{}{}", REQUEST_BASE_URL, SUGGEST_STARS_ENDPOINT) + format!("{}{}", endpoint_base_url(), SUGGEST_STARS_ENDPOINT) } +} - pub fn to_string(&self) -> String { - super::to_string(&self) +impl Display for SuggestStarsRequest<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", super::to_string(self)) } } @@ -190,24 +193,23 @@ impl<'a> SuggestStarsRequest<'a> { mod tests { use std::borrow::Cow; use crate::request::account::AuthenticatedUser; - use crate::request::AuthenticatedUser; use crate::request::moderator::{SuggestedFeatureScore, SuggestedStars, SuggestStarsRequest}; - const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser::new( - "Ryder", - 57903, - Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") - ); - #[test] fn serialize_suggest_stars_request() { - let request = SuggestStarsRequest::new(TEST_AUTHENTICATED_USER, 96457938) + let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") + ); + + let request = SuggestStarsRequest::new(test_authenticated_user, 96457938) .stars(SuggestedStars::Ten) .feature(SuggestedFeatureScore::Featured); assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&levelID=96457938&stars=10&feature=1&gdw=0" + "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=Ryder&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&levelID=96457938&stars=10&feature=1&gdw=0" ); } } \ No newline at end of file diff --git a/src/request/user.rs b/src/request/user.rs index f14892e..707da34 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -57,6 +57,13 @@ impl<'a> UserRequest<'a> { super::to_string(&self) } } + +impl From for UserRequest<'_> { + fn from(user_id: u64) -> Self { + UserRequest::new(user_id) + } +} + impl From> for UserRequest<'_> { fn from(creator: Creator<'_>) -> Self { UserRequest::from(creator.user_id) @@ -131,15 +138,15 @@ mod tests { use crate::request::account::AuthenticatedUser; use crate::request::user::{UserRequest, UserSearchRequest}; - const TEST_AUTHENTICATED_USER: AuthenticatedUser = AuthenticatedUser::new( - "Ryder", - 57903, - Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") - ); - #[test] fn serialize_user_request() { - let request = UserRequest::with_authenticated_user(TEST_AUTHENTICATED_USER, 57903); + let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( + "Ryder", + 57903, + Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") + ); + + let request = UserRequest::with_authenticated_user(test_authenticated_user, 57903); assert_eq!( request.to_string(), diff --git a/src/response.rs b/src/response.rs index 8fc6420..4d8d167 100644 --- a/src/response.rs +++ b/src/response.rs @@ -10,15 +10,13 @@ use crate::{ profile::ProfileComment, }, creator::Creator, - level::{ - online_level::{Level, ListedLevel} - }, song::NewgroundsSong, user::{profile::Profile, searched::SearchedUser}, }, serde::GJFormat, DeError, }; +use crate::model::level::{Level, ListedLevel}; // Since NoneError is not stabilized, we cannot do `impl From for ResponseError<'_>`, so // this is the next best thing @@ -55,7 +53,7 @@ impl<'a> From> for ResponseError<'a> { } } -pub fn parse_get_gj_levels_response(response: &str) -> Result, ResponseError> { +pub fn parse_get_gj_levels_response(response: &str) -> Result>, ResponseError<'_>> { check_response_errors(response)?; let mut sections = response.split('#'); @@ -93,29 +91,28 @@ pub fn parse_get_gj_levels_response(response: &str) -> Result, creator, difficulty: level.difficulty, downloads: level.downloads, - set_completes: level.set_completes, main_song: level.main_song, - game_version: level.game_version, + gd_version: level.gd_version, likes: level.likes, length: level.length, stars: level.stars, - feature_score: level.feature_score, + featured: level.featured, copy_of: level.copy_of, - is_two_player: level.is_two_player, + two_player: false, custom_song: song, coin_amount: level.coin_amount, - has_verified_coins: level.has_verified_coins, + coins_verified: false, stars_requested: level.stars_requested, - daily_number: level.daily_number, is_epic: level.is_epic, - object_count: level.object_count, - in_gauntlet: level.in_gauntlet + object_amount: level.object_amount, + editor_time: level.editor_time, + editor_time_copies: level.editor_time_copies, }) }) .collect::>() } -pub fn parse_download_gj_level_response(response: &str) -> Result { +pub fn parse_download_gj_level_response(response: &str) -> Result, ResponseError<'_>> { check_response_errors(response)?; let mut sections = response.split('#'); @@ -123,13 +120,13 @@ pub fn parse_download_gj_level_response(response: &str) -> Result Result { +pub fn parse_get_gj_user_info_response(response: &str) -> Result, ResponseError<'_>> { check_response_errors(response)?; Ok(Profile::from_gj_str(response)?) } -pub fn parse_get_gj_users_response(response: &str) -> Result { +pub fn parse_get_gj_users_response(response: &str) -> Result, ResponseError<'_>> { check_response_errors(response)?; let mut sections = response.split('#'); @@ -140,7 +137,7 @@ pub fn parse_get_gj_users_response(response: &str) -> Result Result, ResponseError> { +pub fn parse_get_gj_comments_response(response: &str) -> Result>, ResponseError<'_>> { check_response_errors(response)?; let mut sections = response.split('#'); @@ -170,7 +167,7 @@ pub fn parse_get_gj_comments_response(response: &str) -> Result Result, ResponseError> { +pub fn parse_get_gj_acccount_comments_response(response: &str) -> Result>, ResponseError<'_>> { check_response_errors(response)?; let mut sections = response.split('#'); @@ -181,7 +178,7 @@ pub fn parse_get_gj_acccount_comments_response(response: &str) -> Result Result<(), ResponseError> { +fn check_response_errors(response: &str) -> Result<(), ResponseError<'_>> { if response == "-1" { return Err(ResponseError::NotFound); } diff --git a/src/serde/thunk.rs b/src/serde/thunk.rs index 075adbc..4a60505 100644 --- a/src/serde/thunk.rs +++ b/src/serde/thunk.rs @@ -119,7 +119,7 @@ pub trait ThunkProcessor { /// /// This function is *not* called automatically during deserialization from a RobTop data /// format. - fn from_unprocessed(unprocessed: Cow) -> Result, Self::Error>; + fn from_unprocessed(unprocessed: Cow<'_, str>) -> Result, Self::Error>; /// Takes some processed thunk value and converts it into RobTop-representation fn as_unprocessed<'b>(processed: &'b Self::Output<'_>) -> Result, Self::Error>; @@ -153,7 +153,7 @@ impl<'a, C: ThunkProcessor> Thunk<'a, C> { } } - pub fn as_unprocessed(&self) -> Result, C::Error> { + pub fn as_unprocessed(&self) -> Result, C::Error> { match self { Thunk::Unprocessed(unprocessed) => Ok(Cow::Borrowed(unprocessed)), Thunk::Processed(content) => C::as_unprocessed(content), @@ -197,7 +197,7 @@ impl ThunkProcessor for PercentDecoder { type Error = ProcessError; type Output<'a> = Cow<'a, str>; - fn from_unprocessed(unprocessed: Cow) -> Result, Self::Error> { + fn from_unprocessed(unprocessed: Cow<'_, str>) -> Result, Self::Error> { match unprocessed { Cow::Borrowed(unprocessed) => percent_decode_str(unprocessed).decode_utf8().map_err(ProcessError::Utf8), Cow::Owned(unprocessed) => match percent_decode_str(&unprocessed).decode_utf8().map_err(ProcessError::Utf8)? { @@ -236,7 +236,7 @@ impl ThunkProcessor for Base64Decoder { type Error = ProcessError; type Output<'a> = Cow<'a, str>; - fn from_unprocessed(unprocessed: Cow) -> Result, Self::Error> { + fn from_unprocessed(unprocessed: Cow<'_, str>) -> Result, Self::Error> { let vec = ROBTOP_BASE64_CONFIG.decode(&*unprocessed)?; let string = String::from_utf8(vec).map_err(ProcessError::FromUtf8)?; From c1881ac78abd13205f52c4ebefb2fee87b579e6b Mon Sep 17 00:00:00 2001 From: Ryder Date: Sat, 28 Mar 2026 17:21:58 -0700 Subject: [PATCH 27/29] delete unnecessary test --- examples/refresh_test_artifacts.rs | 2 +- tests/moderator.rs | 32 ------------------------------ 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 tests/moderator.rs diff --git a/examples/refresh_test_artifacts.rs b/examples/refresh_test_artifacts.rs index 97b80cc..6c21ef1 100644 --- a/examples/refresh_test_artifacts.rs +++ b/examples/refresh_test_artifacts.rs @@ -1,6 +1,6 @@ use std::{ fs::OpenOptions, - path::{Path, PathBuf}, + path::Path, }; use dash_rs::{ diff --git a/tests/moderator.rs b/tests/moderator.rs deleted file mode 100644 index 56b3823..0000000 --- a/tests/moderator.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::borrow::Cow; -use dash_rs::request::account::{AuthenticatedUser, LoginRequest}; -use dash_rs::request::{CONTENT_TYPE, URL_FORM_ENCODED}; -use dash_rs::request::moderator::{SuggestedFeatureScore, SuggestedStars, SuggestStarsRequest}; - -#[tokio::test] -async fn suggest_level() { - let client = reqwest::Client::new(); - - let login_response = AuthenticatedUser::new( - "Ryder", - 57903, - Cow::Borrowed("UmVkaXNuZU1FQXJFREdlTnRJQw==") - ); - - let suggest_level_request = SuggestStarsRequest::new(login_response, 95524621) - .feature(SuggestedFeatureScore::Featured) - .stars(SuggestedStars::Ten); - - println!("{}", &suggest_level_request.to_string()); - let suggest_level_response = client.post(suggest_level_request.to_url()) - .body(suggest_level_request.to_string()) - .header(CONTENT_TYPE, URL_FORM_ENCODED) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - - assert_eq!(suggest_level_response, "1") -} \ No newline at end of file From 89e92f9a037e7f114b34bd536dd6eb78aa948873 Mon Sep 17 00:00:00 2001 From: Ryder Date: Tue, 9 Jun 2026 11:55:11 -1000 Subject: [PATCH 28/29] Updated for 2.2 and added unknown indexes --- Cargo.toml | 2 - dash-rs-derive/src/field.rs | 18 +++-- dash-rs-derive/src/utils.rs | 8 +++ rustfmt.toml | 6 -- src/model/comment/level.rs | 8 ++- src/model/level/internal.rs | 27 +++++--- src/model/level/mod.rs | 68 ++++++++++++++++--- src/request/comment.rs | 14 ++-- src/request/level.rs | 2 +- src/request/moderator.rs | 6 +- src/request/user.rs | 10 +-- src/response.rs | 4 +- tests/artifacts/level/11774780/processed | 16 ++--- tests/artifacts/level/11774780/raw | 2 +- tests/artifacts/level/897837/processed | 16 ++--- tests/artifacts/level/897837/raw | 2 +- .../artifacts/level_comment/258976/processed | 19 +++++- .../artifacts/listed_level/11774780/processed | 6 +- .../artifacts/listed_level/24904605/processed | 6 +- .../artifacts/listed_level/63355989/processed | 5 +- tests/artifacts/listed_level/72540/processed | 6 +- .../artifacts/listed_level/97598449/processed | 6 +- tests/artifacts/profile/8451/processed | 21 +++--- tests/artifacts/profile/8451/raw | 2 +- tests/framework.rs | 19 +++++- tests/level.rs | 8 ++- 26 files changed, 207 insertions(+), 100 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f24037c..1e5d7cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ dash-rs-derive = { path = "dash-rs-derive" } # benchmark env_logger = "0.10.1" criterion = "0.5.1" -dotenv = "0.15.0" # Dependencies used by examples reqwest = "0.11.17" @@ -35,7 +34,6 @@ pretty_assertions = "1.4.0" [build-dependencies] serde = {version = "1.0.104", features = ["derive"]} -serde_yaml = "0.9.29" [[bench]] name = "level_processing_benchmark" diff --git a/dash-rs-derive/src/field.rs b/dash-rs-derive/src/field.rs index 9299389..781e6ed 100644 --- a/dash-rs-derive/src/field.rs +++ b/dash-rs-derive/src/field.rs @@ -68,15 +68,22 @@ impl OneToOne { format_ident!("index_{}", self.index()) } - fn field_tokens(&self, ty: Type) -> proc_macro2::TokenStream { + fn field_tokens(&self, ty: Type) -> TokenStream { let serde_name = self.index(); let field_name = self.internal_name(); let passthrough = &self.passthrough; + // If the API type is Option, a missing key should deserialize as None + let default_attr = if utils::is_option_type(&self.api_type) { + quote! { #[serde(default)] } + } else { + quote! {} + }; if utils::type_contains_lifetime(&ty) { quote! { #[serde(rename = #serde_name)] #[serde(borrow)] + #default_attr #( #[serde(#passthrough)] )* @@ -85,6 +92,7 @@ impl OneToOne { } else { quote! { #[serde(rename = #serde_name)] + #default_attr #( #[serde(#passthrough)] )* @@ -93,15 +101,15 @@ impl OneToOne { } } - pub fn ser_field_tokens(&self, lifetime: &Lifetime) -> proc_macro2::TokenStream { + pub fn ser_field_tokens(&self, lifetime: &Lifetime) -> TokenStream { self.field_tokens(self.ser_type(lifetime)) } - pub fn de_field_tokens(&self) -> proc_macro2::TokenStream { + pub fn de_field_tokens(&self) -> TokenStream { self.field_tokens(self.de_type()) } - pub fn serialize(&self) -> proc_macro2::TokenStream { + pub fn serialize(&self) -> TokenStream { let field_name = self.internal_name(); let field = &self.field; @@ -110,7 +118,7 @@ impl OneToOne { } } - pub fn deserialize(&self) -> proc_macro2::TokenStream { + pub fn deserialize(&self) -> TokenStream { let field_name = self.internal_name(); let field = &self.field; let api_type = &self.api_type; diff --git a/dash-rs-derive/src/utils.rs b/dash-rs-derive/src/utils.rs index fc479ef..8c75804 100644 --- a/dash-rs-derive/src/utils.rs +++ b/dash-rs-derive/src/utils.rs @@ -54,3 +54,11 @@ pub fn type_contains_lifetime(ty: &Type) -> bool { _ => todo!(), } } + +pub fn is_option_type(ty: &Type) -> bool { + let Type::Path(type_path) = ty else { return false }; + // Match Option / std::option::Option / core::option::Option + type_path.path.segments.last() + .map(|seg| seg.ident == "Option") + .unwrap_or(false) +} diff --git a/rustfmt.toml b/rustfmt.toml index 55e87b0..d73c245 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -5,9 +5,3 @@ max_width = 140 newline_style = "Unix" use_field_init_shorthand = true use_try_shorthand = true -wrap_comments = true -fn_single_line = true -hard_tabs = true -normalize_doc_attributes = true -group_imports = "StdExternalCrate" -trailing_comma = "Never" diff --git a/src/model/comment/level.rs b/src/model/comment/level.rs index 2317fc4..2f8da51 100644 --- a/src/model/comment/level.rs +++ b/src/model/comment/level.rs @@ -18,8 +18,11 @@ pub struct LevelComment<'a> { pub user: Option>, /// The id of the level that this [`CommentEntry`] was posted to + /// This value is negative if it was posted on a list + /// This value is only returned on Comment History responses #[dash(index = 1)] - pub level_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub level_id: Option, /// The actual content of the [`LevelComment`] made. #[dash(index = 2)] @@ -35,7 +38,8 @@ pub struct LevelComment<'a> { #[dash(index = 4)] pub likes: i32, - /// The unique id of this [`LevelComment`]. Additionally, there is also no [`ProfileComment`](crate::model::comment::profile::ProfileComment) + /// The unique id of this [`LevelComment`]. Additionally, + /// there is also no [`ProfileComment`](crate::model::comment::profile::ProfileComment) /// with this id #[dash(index = 6)] pub comment_id: u64, diff --git a/src/model/level/internal.rs b/src/model/level/internal.rs index d358fad..b72ca48 100644 --- a/src/model/level/internal.rs +++ b/src/model/level/internal.rs @@ -62,7 +62,7 @@ struct InternalLevel<'src> { #[serde(rename = "39")] index_39: Option, #[serde(rename = "42")] - index_42: bool, + index_42: i32, #[serde(rename = "43")] index_43: u8, #[serde(with = "crate::util::default_to_none")] @@ -100,7 +100,7 @@ struct InternalLevel<'src> { index_53: Option<&'src str>, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "57")] - index_57: Option<&'src str>, + index_57: Option, } impl<'de> Dash<'de> for Level<'de, (), Option, u64> { @@ -125,7 +125,7 @@ impl<'de> Dash<'de> for Level<'de, (), Option, u64> { coin_amount: InternalProxy::from_deserialize_proxy(internal.index_37), coins_verified: InternalProxy::from_deserialize_proxy(internal.index_38), stars_requested: InternalProxy::from_deserialize_proxy(internal.index_39), - is_epic: InternalProxy::from_deserialize_proxy(internal.index_42), + epic: InternalProxy::from_deserialize_proxy(internal.index_42), object_amount: InternalProxy::from_deserialize_proxy(internal.index_45), editor_time: InternalProxy::from_deserialize_proxy(internal.index_46), editor_time_copies: InternalProxy::from_deserialize_proxy(internal.index_47), @@ -170,7 +170,7 @@ impl<'de> Dash<'de> for Level<'de, (), Option, u64> { index_37: self.coin_amount.to_serialize_proxy(), index_38: self.coins_verified.to_serialize_proxy(), index_39: self.stars_requested.to_serialize_proxy(), - index_42: self.is_epic.to_serialize_proxy(), + index_42: self.epic.to_serialize_proxy(), index_45: self.object_amount.to_serialize_proxy(), index_46: self.editor_time.to_serialize_proxy(), index_47: self.editor_time_copies.to_serialize_proxy(), @@ -206,14 +206,21 @@ impl<'de> Dash<'de> for Level<'de, LevelData<'de>, Option, u64> { fn dash_deserialize>(deserializer: D) -> Result { let internal = InternalLevel::deserialize(deserializer)?; - let level_data = match (internal.index_4, internal.index_27, internal.index_28, internal.index_29) { - (Some(idx4), Some(idx27), Some(idx28), Some(idx29)) => LevelData { + let level_data = match ( + internal.index_4, + internal.index_27, + internal.index_28, + internal.index_29, + internal.index_36, + internal.index_40 + ) { + (Some(idx4), Some(idx27), Some(idx28), Some(idx29), Some(idx36), Some(idx40)) => LevelData { level_data: InternalProxy::from_deserialize_proxy(idx4), password: InternalProxy::from_deserialize_proxy(idx27), time_since_upload: InternalProxy::from_deserialize_proxy(idx28), time_since_update: InternalProxy::from_deserialize_proxy(idx29), - extra_string: InternalProxy::from_deserialize_proxy(internal.index_36.unwrap_or_default()), - low_detail_mode: InternalProxy::from_deserialize_proxy(internal.index_40.unwrap_or_default()), + extra_string: InternalProxy::from_deserialize_proxy(idx36), + low_detail_mode: InternalProxy::from_deserialize_proxy(idx40), song_ids: InternalProxy::from_deserialize_proxy(internal.index_52.unwrap_or_default()), sfx_ids: InternalProxy::from_deserialize_proxy(internal.index_53.unwrap_or_default()), verification_time: InternalProxy::from_deserialize_proxy(internal.index_57.unwrap_or_default()), @@ -239,7 +246,7 @@ impl<'de> Dash<'de> for Level<'de, LevelData<'de>, Option, u64> { coin_amount: InternalProxy::from_deserialize_proxy(internal.index_37), coins_verified: InternalProxy::from_deserialize_proxy(internal.index_38), stars_requested: InternalProxy::from_deserialize_proxy(internal.index_39), - is_epic: InternalProxy::from_deserialize_proxy(internal.index_42), + epic: InternalProxy::from_deserialize_proxy(internal.index_42), object_amount: InternalProxy::from_deserialize_proxy(internal.index_45), editor_time: InternalProxy::from_deserialize_proxy(internal.index_46), editor_time_copies: InternalProxy::from_deserialize_proxy(internal.index_47), @@ -287,7 +294,7 @@ impl<'de> Dash<'de> for Level<'de, LevelData<'de>, Option, u64> { index_37: self.coin_amount.to_serialize_proxy(), index_38: self.coins_verified.to_serialize_proxy(), index_39: self.stars_requested.to_serialize_proxy(), - index_42: self.is_epic.to_serialize_proxy(), + index_42: self.epic.to_serialize_proxy(), index_45: self.object_amount.to_serialize_proxy(), index_46: self.editor_time.to_serialize_proxy(), index_47: self.editor_time_copies.to_serialize_proxy(), diff --git a/src/model/level/mod.rs b/src/model/level/mod.rs index 3e8f39f..56efb3b 100644 --- a/src/model/level/mod.rs +++ b/src/model/level/mod.rs @@ -188,6 +188,58 @@ impl From for i32 { } } +/// Enum representing the possible Epic ratings +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub enum EpicRating { + /// Not Epic, this means the level is unrated, rate only or Featured + /// + /// ## GD Internals: + /// This variant is represented by the value `0` in response + None, + + /// ## GD Internals: + /// This variant is represented by the value `1` in response + Epic, + + /// ## GD Internals: + /// This variant is represented by the value `2` in response + Legendary, + + /// ## GD Internals: + /// This variant is represented by the value `3` in response + Mythic, + + /// Enum variant that's used by the [`From`](From) impl for when an + /// unrecognized value is passed + Unknown(i32), +} + +impl From for EpicRating { + fn from(i: i32) -> Self { + match i { + 0 => EpicRating::None, + 1 => EpicRating::Epic, + 2 => EpicRating::Legendary, + 3 => EpicRating::Mythic, + _ => EpicRating::Unknown(i) + } + } +} + +impl From for i32 { + fn from(rating: EpicRating) -> Self { + match rating { + EpicRating::None => 0, + EpicRating::Epic => 1, + EpicRating::Legendary => 2, + EpicRating::Mythic => 3, + EpicRating::Unknown(inner) => inner + } + } +} + +crate::into_conversion!(EpicRating, i32); + /// Enum representing the possible demon difficulties #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub enum DemonRating { @@ -648,11 +700,11 @@ pub struct Level<'a, Data = LevelData<'a>, Song = Option, User = u64> { /// were requested pub stars_requested: Option, - /// Value indicating whether this [`Level`] is epic + /// Value indicating which Epic level this [`Level`] is /// /// ## GD Internals: /// This value is provided at index `42`, as an integer - pub is_epic: bool, + pub epic: EpicRating, /// The amount of objects in this [`Level`]. Note that a value of `None` _does not_ mean /// that there are no objects in the level, but rather that the server's didn't provide an @@ -710,7 +762,7 @@ impl<'a, Data, Song, User> Level<'a, Data, Song, User> { coin_amount: self.coin_amount, coins_verified: self.coins_verified, stars_requested: self.stars_requested, - is_epic: self.is_epic, + epic: self.epic, object_amount: self.object_amount, editor_time: self.editor_time, editor_time_copies: self.editor_time_copies, @@ -739,7 +791,7 @@ impl<'a, Data, Song, User> Level<'a, Data, Song, User> { coin_amount: self.coin_amount, coins_verified: self.coins_verified, stars_requested: self.stars_requested, - is_epic: self.is_epic, + epic: self.epic, object_amount: self.object_amount, editor_time: self.editor_time, editor_time_copies: self.editor_time_copies, @@ -769,7 +821,7 @@ impl<'a, Data, Song, User> Level<'a, Data, Song, User> { coin_amount: self.coin_amount, coins_verified: self.coins_verified, stars_requested: self.stars_requested, - is_epic: self.is_epic, + epic: self.epic, object_amount: self.object_amount, editor_time: self.editor_time, editor_time_copies: self.editor_time_copies, @@ -841,7 +893,7 @@ pub struct LevelData<'a> { /// The comma separated list of all sfx IDs used in the level /// /// ## GD Internals: - /// This value is provided at index `52` + /// This value is provided at index `53` /// TODO: Parse into a List pub sfx_ids: Cow<'a, str>, @@ -849,8 +901,8 @@ pub struct LevelData<'a> { /// /// ## GD Internals: /// This value is provided at index `52` - /// TODO: Figure out how to parse thise - pub verification_time: Cow<'a, str>, + /// TODO: Figure out how to parse this + pub verification_time: u32, } #[derive(Debug, PartialEq, Serialize, Deserialize)] diff --git a/src/request/comment.rs b/src/request/comment.rs index a7d0612..e589ab4 100644 --- a/src/request/comment.rs +++ b/src/request/comment.rs @@ -431,7 +431,7 @@ mod tests { assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&count=2&mode=0&userID=159782398" + "gameVersion=22&binaryVersion=38&secret=Wmfd2893gb7&total=0&page=0&count=2&mode=0&userID=159782398" ); } @@ -443,8 +443,8 @@ mod tests { } let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( - "Ryder", - 57903, + "TestUser", + 472634, Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") ); let request = UploadCommentRequest::new(test_authenticated_user, 85179632) @@ -453,7 +453,7 @@ mod tests { assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=Ryder&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&comment=VGhpcyBpcyBhIHRlc3QgY29tbWVudA==&levelID=85179632&percent=56&chk=UQsGAAEACgQBVQBaAwoGVwtSDQIEWAYOUFEAVQoIBVtWDwEHDQEJVA==" + "gameVersion=22&binaryVersion=38&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&comment=VGhpcyBpcyBhIHRlc3QgY29tbWVudA==&levelID=85179632&percent=56&chk=UQsGAAEACgQBVQBaAwoGVwtSDQIEWAYOUFEAVQoIBVtWDwEHDQEJVA==" ); } @@ -465,15 +465,15 @@ mod tests { } let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( - "Ryder", - 57903, + "TestUser", + 472634, Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") ); let request = DeleteCommentRequest::new(test_authenticated_user, 85179632, 7000000); assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&commentID=7000000&levelID=85179632" + "gameVersion=22&binaryVersion=38&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&commentID=7000000&levelID=85179632" ); } } diff --git a/src/request/level.rs b/src/request/level.rs index a8411d5..a04d752 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -687,7 +687,7 @@ mod tests { assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&levelID=17448979&inc=0&extra=0" + "gameVersion=22&binaryVersion=38&secret=Wmfd2893gb7&levelID=17448979&inc=0&extra=0" ); } } diff --git a/src/request/moderator.rs b/src/request/moderator.rs index fd53140..8491e66 100644 --- a/src/request/moderator.rs +++ b/src/request/moderator.rs @@ -198,8 +198,8 @@ mod tests { #[test] fn serialize_suggest_stars_request() { let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( - "Ryder", - 57903, + "TestUser", + 472634, Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") ); @@ -209,7 +209,7 @@ mod tests { assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=Ryder&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&levelID=96457938&stars=10&feature=1&gdw=0" + "gameVersion=22&binaryVersion=38&secret=Wmfp3879gc3&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&levelID=96457938&stars=10&feature=1&gdw=0" ); } } \ No newline at end of file diff --git a/src/request/user.rs b/src/request/user.rs index 707da34..661bfa9 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -141,8 +141,8 @@ mod tests { #[test] fn serialize_user_request() { let test_authenticated_user: AuthenticatedUser = AuthenticatedUser::new( - "Ryder", - 57903, + "TestUser", + 472634, Cow::Borrowed("VGhpc0lzQUZha2VQYXNzd29yZA==") ); @@ -150,17 +150,17 @@ mod tests { assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&targetAccountID=57903" + "gameVersion=22&binaryVersion=38&secret=Wmfd2893gb7&userName=TestUser&accountID=472634&gjp=VGhpc0lzQUZha2VQYXNzd29yZA==&targetAccountID=57903" ); } #[test] fn serialize_user_search_request() { - let request = UserSearchRequest::new("Ryder"); + let request = UserSearchRequest::new("TestUser"); assert_eq!( request.to_string(), - "gameVersion=21&binaryVersion=33&secret=Wmfd2893gb7&total=0&page=0&str=Ryder" + "gameVersion=22&binaryVersion=38&secret=Wmfd2893gb7&total=0&page=0&str=TestUser" ); } } diff --git a/src/response.rs b/src/response.rs index 4d8d167..b7a6c66 100644 --- a/src/response.rs +++ b/src/response.rs @@ -103,7 +103,7 @@ pub fn parse_get_gj_levels_response(response: &str) -> Result Result Result>, ResponseError<'_>> { +pub fn parse_get_gj_account_comments_response(response: &str) -> Result>, ResponseError<'_>> { check_response_errors(response)?; let mut sections = response.split('#'); diff --git a/tests/artifacts/level/11774780/processed b/tests/artifacts/level/11774780/processed index 0fdbd7c..9c89986 100644 --- a/tests/artifacts/level/11774780/processed +++ b/tests/artifacts/level/11774780/processed @@ -20,10 +20,10 @@ "coin_amount": 3, "coins_verified": true, "stars_requested": 10, - "is_epic": false, + "epic": "None", "object_amount": null, - "index_46": null, - "index_47": null, + "editor_time": 48300, + "editor_time_copies": 98730, "level_data": { "level_data": { "meta": { @@ -138222,10 +138222,10 @@ "password": null, "time_since_upload": "9 years", "time_since_update": "9 years", - "index_36": "29_566_73_98_29_29_54_424_29_29_237_132_177_29_29_29", - "index_40": "", - "index_52": "", - "index_53": "", - "index_57": "" + "extra_string": "29_566_73_98_29_29_54_424_29_29_237_132_177_29_29_29", + "low_detail_mode": true, + "song_ids": "1234, 3456", + "sfx_ids": "5678, 9348", + "verification_time": 14400 } } \ No newline at end of file diff --git a/tests/artifacts/level/11774780/raw b/tests/artifacts/level/11774780/raw index 730e10e..4f9b171 100644 --- a/tests/artifacts/level/11774780/raw +++ b/tests/artifacts/level/11774780/raw @@ -1 +1 @@ -1:11774780:2:Dark Realm:3:TXkgYmVzdCBsZXZlbCB5ZXQuIFZpZGVvIG9uIG15IFlvdVR1YmUuIEhhdmUgZnVuIGluIHRoaXMgZmFzdC1wYWNlZCBERU1PTiA-OikgdjIgRml4ZWQgc29tZSB0aGluZ3M=:4:H4sIAAAAAAAAC6y9a492u24Y9ofmfbB0l7DhDw6KOC4aFD6xncRfNk5OABtwUgQoijhFf3zXWiIlUiIlzfHsDcy8I1LUnaRIiuuf_4PLX-Z3Z3-3v1-_u9-N_93__sv8Hn8313X9nn43v5twQ_Lv5v-7_5kALREsM2L5AFju-t2Y320Ivxtbf7n3V69bsK6BujZcd2Xr4139-dlR7e8P1tDUQ83eBe7peskE3U999w66FV3D4zTN9fv1R_tH8wn2j9cfW8Wn8w_M_Z4L1rRTzXj90dz_93rWPmjmht99i21endBmrYhzYMrToH8avLCWF9v7eN7iOxvWhXfy6Eo6ZeJseBvZrFKgjRtOIR9RiCIFVx4K97LtKIgduGA511Xj3O_fK96uZvrzGsxSX4253i2zqzydB3cjPz_c82M9yf-6KTZG7Ld_9_7LGOyyvr2mugkPXFl33c7NlnA019ZOA34Pa7b7qm6eq-vd0GZf149103PUrXmq5nXVIO0qW5nEzaGWdee9nF5m-azui76qLG5nYx_OaU1c181Tw_73Z-ofNrPp87SjTX46HdJ-R1p5Rx8e35tXT9W__vkvjfu6nl-h_or1l_-6f8YvY-9f6Ss_RenLPL_yl_v65__gyvPrL-2LXSnk96d_f5ZK5aq_zNf1m_kqV_yyX3c77utXDJ_wZc1XuMtjtK08215urmwmgP-y9svfUFfyDXRP8Q0O6ZNu-HX_-4a7t_YVFASgYm8sX5KK9MBLKSrcvK08iCsC90zar_TMlnHuy5mvv_7Hv_iLxWx8d5Y0fHP5p-vmeiA23z_v1fwK98TZEeoMgeYb-sysiQ_IBPyz1D_fgd_KRBvaDfjEd3Q3wr0dPjE-o_zDf_qH__an_-uf__Fv_-rv7T_8x__5j__lv__9f_uHf_fv__E__00dPdT1H__UvSpdSuC__rv__X_86a_-5b_Rhm7Ue3pv4P_xd-Z__Gf7T__jH9zf_z9__b_99f_893_7d38BHbVW6ai1BZrqi-DMu_zpnYXrK9xdiPeMvgv_bq9kP5d7h-fdJ18Z172SvCt-zJVe8L0ksdy1f1nYp3crf3J_-Kc_1ZbujXJpBOuCvOv4LkXtECzIo3V--XTPS_hKb_efc1a-fHxqvf337yr60AtcL7hcK4gF5sjHOpUdXhocKMSRZGkFPnlSgBMGpdAVOpU-lgZ59xMU4I5qaLn11JaPDb0ssrI0kksiuXw1clNp5KXGZ4rbzvbLfwDZjAyoHsgB2k9kBdwb_Gnll3nareDsO_g5NAitbCt716Hxhdp0daZGumRNYsTFno1IYwdv3bj3nHXtnk9otfirzXjq0_Pui2D5VD4bGEvJ5ni5ZMjvLqMnMz9LFwKhQaD-YdIAjffwDK1ZXor3IXoPjPuEfnJfiIsAeSvehylT0vc5unrLL7d2pDiPxe-0A3aSd8IA7RONw9O2AsCLvBUq9OFq4lYAcFpuBRlp7KEt01YAQAi9VR9aOTCYilnX9h78SzW0Ij8WcNZ8BboB6AKRnh3zw6oUBJ8_xb8kr-tTItEJ6j6c4VTai2DTlAW1dn4YYPRwrtsJiQnn8N2ETepUzGdF7yUppLyKANfLzSs6Kr4znQVTyr8IE26YpLz0cj-Vp14-FT-qQ0N_uSegmz6gytAp_j2sXk6axfJnmWK8PmnuqR16VMuTy3R2cUlipWSr_gXsFJhIB9daIX0683o5fh8JrvC7P-oKRMaJTKMz869Kpi6M7d2PjRnFHJFJMRXjhRVX2ZTCompnJhaFMzEWJ14c63TCdu3Tlvp4XjWAFvixwISuT_MVeA5WFZWXZ-00Ti5ipB2C3yEYAQEWOyayF_pWePmGsIOAJwzj8u-P-17zWKI-4TEp1UmOEo1AZ_Plv6-K-Hbnci9zv4_pzXgfMfb2KD6dTp2h1nXzkwgE7go7EkTLAPMCrPLVWs0LAK8BksKi27TLbXCA71sRBBdu7eeIhMrlbnGY_IuRP_lWp--xeI_ip86ojuPrVSG99Nzbv3uZ3HM3tfe_SkycHjRayufKz4KUm4eb8Bw65xxvVMchjVbWnq6ZtT9zkhxhzBUAFWJnkay8TOUvzwdCufP8jlk5CTDU5Ch2b7Zy8nQtAY6o0ZU-cGRAfbk-AOoNgJYkUhJ7ZSIwar9ocYodu9anJZ6UuKnkIiVxHB2VR3QoWJ47zeuVPn2UFRZF2MtF22TJq3ozh2FjELFmTGktw62qd5EK3j6NMC-mFcAk5AGDzDwsTq8yyejkhmsTLSCzZgZdYC61pJGpoF2s3tsllNrSD0i_sValp3ajsCu4I0ejXPyuhhPA5Xkab3C1FHfuUNy2TRP-th-w6PtRDH23W0sER8hj-WMJ-nq8Jb-hBSFZjxf-W_1GC0JlJOW3ZhcQ0WxHAzNSvWyl-Hm0hviKlcaWymNx-Id_9_f_73_9y2pi8X1jEFlV2WW6L6zpeljrfV8x2RfOCV9VNuO9pDGfbHEmGfN5T06-nvt519pkWCKwqtLRVqoEzwkkeGn7CAD1NvLcaHsZmN7g-lT6qczDzbSQMYX2d25_152Zh5srLWUMlDbixlZyZ6uk2VrAKvaGWHGSel15Hm0ANDXbBxa77gYlbiq5xpI0FsDF7lWY2voMCpNQ_ipHQvmrJ_DyQalrvVfVvjaaHcZC-cTRbhAk5TPQ2UINL7BpbnrfeyeYSdGbBKxfbFwHVpjeLWJOfD_-YoTSyzcyGECiY-oftPLWjX4yqbTWPK5cX2k_XilqX3sxHV8TusoIbelslDbdypXxIleWhmpj_GjTakfeXe9Y-arGoAgmmEHFJqO3ORF1dZoFAexsouBnGUDTR7Su6zub-4RmQBRuAzCOUPnsPXrT1O3cLTIX2rU6MM_ASWHvK7dCykskuHoAKfeOmnYHx5FRNpJxsDHeh-kxzyF42eXqWTFDH6FUGYHXoEGGsoEV-wmk17FRbYDZYJW_Z7B6uXkxXCiVfuuu4qckojemVkD0S0KHiB0oIHpWEYxOvQW4rBGNtcwmpkjK8yh0y2R6YuXU9PSK79JsQAvQZMiaW69acEl9vKykCt5X7Sujzafys9JNH5WXwNzFiXnCUGJjWaXf3UE3qAyrhNh5c-toJV4NRIT91OYm5gvdGi3RFXvyiVZrLweGPkTDZm0emEHWHNjOjjkPO6eQQdo6SOsqV0Zebyps3Dy13EMNahmgZv0SHSNKzGWlOMbKRZsZjnRk1tXaXaH3NZ_6SkPvlx140sg7gPqay_bJ3iNZncv2s5nCjktWaho_LH4P1bhlMTIUmCJMqMgUK4LfIdSJuIggQTXbvVszhk95Iw-MvxceFraQwxLWWK-vP1ROdN_PbpX50UXdJ987_5G-mdmAFjiT4enZqzm-G93c_yqJ06ts9z62V1PGDRS5ro1Xa8-rKSOfhxJiWqh3sIbEXKwAQpISjFEqnZKhJa7dB6qhwVyFVKvxGBc64ttpz6SvaWTKQNRPzTAb02vbARLE2gONsRJqdEikj92GwcYR57Fe08hi7qQzcz4_F_QP4fDE_NFmgpRQ-4odZotYNaA_3OBRBVTrPDdX9ElDueU7eSLzYRBCiQ9jLU93qEP-XnuE0jv33nZbbtMPEEJNnWzGhTszNDXdfbF8DAkwl0HzwQI0eq-k9nFDUqsnK_KBzPdoH6pSFGZTv_nA5EpXnCpnkYKAkODUBy7_qt93EMjTihEJizBJxCJG8H3hOH2QojgQ7UrUZmKGv3IL4ZP6Tw72xPIn2YOITEhO0LUwxp6cYa1ldpsVBcsHugkyHddrFfSEJxKzHJZXNlWYbtNkjblS-MTXoeTLpy5uN9pV-XYvzn0k3lCndAtBy3GgcxoK9XBcL6rB8IMuk4ydZJKh1g5SjJjD0TR49xuKRyr1bJpus2P1GT-OpD3C9qeWotxS5SYmAjcZDd20ReKOxtkJYSqyU1E1cWKRnbFyL4rQzXafb3ZQUl53ST0NkQ63AqqRlQFqRyMMtgxO886ITeaTgDWuWoNeExFEgl8au7Web6HhKFiX2BzK5bmVA19uS8HkoBlNrlCflVilxE-1aAmxtWIX2oigayBDa3n3VdS7NVAYbcq4WE7ZVJmuFZjEq9RhEw_2b3oZrmEsvJfPTDk6RObJsJcIAFEBm0cUaHSFFwhZQ6hXaDpF49WUEIg9pOI-q6NNj0gmo1nrEL429uGIJckWCf2F8GqY4RjTHgrExjuWYoos5gorfwNrIz7JQV5h2W9gbUQxYRH73uu2zIvM_3nobWVuFsJzyGGFokDdVKYaSaXjTinUA4oEeoEfC3rEEdK_EuHN3SYIqG6ixkpGwyGW-KmWHztBnVDYc-JhgqI0lYCP6fUZ9TkYnEwzQJg0cFRJROoG4IDB59P7_KW5hdoYdhiq56hTYE4iXq1zO9tNjFhS53q2RuJKjfGxns7fgjPXplYIaXObaR0IwuGCC429upuKsFmgrbJh6JwOV-x8HD4ZVYGuUJ54eQ3hMtam96KViGun6suj44asM_VUQINzcSLFM-OyaHZQwEubXd9bsskNqC-MctDAHuNEulnZwqd1eyeRrD_Cat6LA1o7rBOrb9uRRy3usNISq225NE_Fb-0eCnuoGUEr63bdGgYFbiyIY0ERC9xUx01kWcmllPiplp8o-6l1kCL1ogiDMBY9EYNzxI9IzfwP9S8GmmpelDyNogdWCSOZA4dxRBIkqpCiQq4Nz3Zuh6BaoFjjEgLySrcxDjnVOBTIYi3g1wl85uGVrlAelfIy8PwqsdzrR0iervSzHwIEW6UuHlx6xAM660FCTAKi-v7btFg9TCB7ssokpB1fhZAlpu-HUBnhaM2epMAdwmlARUeyhEiLzuFA14G_ZhI4nc-f95CjOJH8AC_eNfkRSzzBDSSfYOlJFDJTh04BRai69ghKBrs1OK7B5QCsynvY4Ouu6eBLBqP8rtRXukBtYIlxbTFOjcJOMAqrWBuZ7Y6Mw84dYV3fwNrIf-SQR73fYa2N1m37N4u913zNfDFZ7A96ediGmDCosAlUnk22cryZOww0P7uZgx3cweNi6-wnv-zJ-sEOrqJMkf7GE1MshvpXALyt6xE1YPz2aH-cAIQUjWpBwBTWAgBHDdsCbjUE-KS-qkI6xHDORkcqOFoe-cvoOohuai9kuCTQhnQR7uN8ND5InRb6Ns0jB5AatGtUxfTEzAsKDhQJF2og-5oa6y3dk-hf7inDKs1AKkXOsC3CjJsIx1n2MMvzBdqDtbO0WCIBx_FQHNnv5x0LyhEQIKhPdB2SbWTrlZ9UrLFxlNQbF5HmplD58qqdlcJnpc1Poap0IWoUNFiKaBGuZwv-6ysB-9CDlS6OVjpKBo1TrawbnKo5SwBclwjAdmt5ZiZDD-6Wplv91uxsHl0HvzWrmideObRKNTTdcNWqaZarjmCr6YrXlIxirEdp7pBkJmN1PFXJgDddRCZN4q31RQGHA7CqGrVOr8C6yuiDAkc5CQTsHG2Kda2sMhHyi6A13owUY8UGstOp_JH9vrNMxUkO4iV-ck2NUj4lv5ct35R62H8qzhSKdbdqPuZxj1sfP3dFLvNBbIRmyOFv7UxAO1AX1aGZYGhQTH1t1_FHOR6aeWeIkAokApP6rQOxtvMiKvIpWfZoDekm6dUaVGPP1sABGYi1HifH9yLQAeockAdYNfAndLs-ltA3rL-1OKvQXxnjQLoDAEt6PDPVZUIAf_zkqQ8XKAyzkhNIJC8tGZ-0zY2mqXd0PWlLkJjIjwPO44DpulPdqC8KSgifaeNjMX2WS-iSqGq-IYL4GIvVmt5FklqG1GLbwPqupZBdk4Z9zcpJ1BYoPJ1WmDUe0B1C8V8taoBrXeABDlmIr0YYOBxI_hWy75vxuMclW9KvK7aZAdUqQNSxvUhAsq_KZXCm8tWviAsKqkqdA1ugaujpByDaJ0yP0V3lLO_747vVTgyOGAXA88DQ-V-48ufpjDH5k5Lh7BQiQWKzcAwhIgCI_dmBiRff4LCs8eIHp85DHE3IcTQhx25CBhUnpjkGheLWIAhKjpW4qeSaSiYrdZys1HGyUkdipcY0ZDC-PNuWrAAm6mZ0fBaxmEROipp6VE3DUTUNw5REareUqC4xygLDkq7H8XEXzqTsC4RQD7KpfpGHBCbaxVM2qEVfTEA_58tBVCy9UbEM40JolxCYUx0eN_AiwyFzG5C_aA64alh-DuY7U54Yln_51K3FoAG9Ods8ofbeMGgWISxvVwGWROiBvnOfbdcglE0u4gU2HIzLmVB4H69h55McQwTNm6VzMbATxp-Ywcqoen-MvBoWr-2sUbHStm7XB-tEm8eKLYhaCmSFbbDRvmM8wjqyjsYj62g8so7GI-toZ3lL0yKgSa9B2NY48TtD908wt7ZnMFfiWDVzZcS8FmfmSjAwgHB09Py_jiV7VwwaA3jntvYqpyp_QCu4hfVdCXvw6E9Pm7bULj-ZOZ80J8mW9GQ-rJP70EnEtABdS7NVJRFrAxbR9-ZVpUjsWfdUfnUNtIrdNBoVUxgL-nt91pM0lXiiZiTJvJLSFJGFELACJQ3gJwCYasjjlwFC5B3Et6ZuN2Bxr6nZMoRi34urA68tV9uFPovloNKlyfgMSl2aDMp0CahKlWwvYZWpjZZWpppVIsZZjDtlfSWBqlM5BLZO5SCdai-CkEKWQ5pUA00shT4no32XTLKkb6X53T5Csgohr9Rl_Qo6FGd7chrXUDIEp3hiCE7NittfbVeFJQ0v_lu5nCAAt6ZSritGOO-KYgQzOKdN5Kva8rqNysoADiPlu_0x3yFC3Kg-pWaE5C5wUinp6kxg51owYzYctETqps7OBxQSLaKX2gPH-jvdIB1FD_eDs8RaWwxR_qZt_HPD3MZUcynlyOuYFHN1-V2qV1BFmQ2OqQQwOBrB4Ii7ra6Yf3bb3Yk07-OKYBNFmHazhFTt_wR8s-MK7CrTEwHWd3A2KBCg3mtmcT42ss2oYKvNIdUHR_GtmJ9e5a51EKrPQjCdwzSV4zGwvhqH8Zd_jLkXS1G9IpJeeVClTEZLJH0zWVle7hkSwHSLJXMmYgDRB0WkBORk7lZEkHF5tAJWIZWJbY92k1nDSsfk9uKrU6YWaTDIZTTIMZOxJXWmBAzQ-vxSMgfpESXt2OxLzpfoS85KpzMZDXtdmakxO2lt1sNAB0Zfxfb2uGm0E0LlARZwSD84rU9_VQVqQKa2VyaP85wLAkQxbpOZBSECdqOL52xHu-UswvNgcJQtoPlq70Tbkloyi5HiRsda5SI_D5HSkHI9x_Qxr0XR5ze_-XMJeR3DFl0w5usP17_89R9YlvO7nv-82cWed4pvNrO3HnCemhltgfTqdIV0zJoeXhGMq4BS-W4_AJaeJsgV7lhRH3iE5csv18yN-8E6NiYYYW_Uw18IhQBT5N9s_xdluSCKgasUFk2DzhnYWZPFGTZpufoFC9hfwRx5_JFlwxQe_XfYUD6YJIERlavdpWriQSihzpTSQ4eHksENQonlayAGAroQmzPcDaGozEWum6zh7GMTA0toHWrW9re05xaAgp5awNAZnuzdZUwiX1kYGTd3xxUzPSOlXTBjF2gBWkh6iZtK4lRSppLJEF4mY3mZDOqFGN1tVXyBUOwbFC62Jc3RCRiGCcPSAqkpeLAh1tNWeiYJxhwR3AxpiuW8NhCFuxeM0OnWb5iVFYZbYNhOI2pm8MxOM6rpshusBPxAhnzXK5AqwNj6tAZ7ULes9TzFG87sdFWr86KVPw9g5CsczJYOdxv4JcPBAlb8awG7WX27zGAa5ckKnuk7nEItv_0qUgovxhtF0Wy2pcdPSyETxQng-QZU4hla-QYauZutGt2jjXZnBW00Xit3MxhD4inhJMR4iuhOEa8t4kWa_kY8aeWELn7ia2N17uPfHWua-QBul8X7j6_XJfvxVaoP8SwqCrldUh5OjWdlclSWyZlZqMPzYZf3NXbm044A7gvao_v015W9xlTk5qJCi0KvWIU3FhUuCqFyl8aARwvyWDBYh5H0WMWMVYhghDqsJE8lxMDJauWpVp7mAxM41KI0xRMixE_vdauht08rNwB38twCzJaXeiM8X8puK63lQi4H0N7nOo4tQC8v4wZDPazqAbhAywdZDAlNGpHuT_5UKxUB1C26dATJUfXifXTSBCugCDoCdEiE5DVkoTXgFlphWBUDxGUfFxejQFwoz6tyVSxjZ3W41eCv-MWDqMVE4jhU8DJeE0eleWCB-uKdCTSgYNARlE0uQHpaF-KTcME12piOYUVtjzaaemWZjZ3bSlgcrI7Ieb4b_HtQ300s3E083BFbnDXwTLrhmB6iBwWjtQ7qTK8JoHy04gFFM1KkBWg1GupQoWD8VJIUnJlO6BcgQErUFIUQYtaSfFVATeJR0PKCA0FP9JsN0BioW9IxJ_us2hrqbVfjbr_aWBPGNw-EA9WuzpwNZm4uT2uOBpOjw83qomLrVwCz2YXqFEt4DOzFVWQ7jlNjhX2jLMA6HzZGgeOJBwQlAD2SNhYx6mTTUkPYYNKkPd6yNlyOHWszc4ICFkmChC6VpXm2_uYibb1r6eruN-Xj3stB8PctIb9YY37SBRaJH4HZOk9Eg9X8N6vleu7cY7_MH4ebKCTYwvQBUFWwbpWT-NsPqwasdU9VZWFv6rlHqcU6Ayt0Ex92EyPGbym8Y7AXKqDPx2wf1bhecHr5y1EeF9bXe1EDS_OzG-8a5q3xLrPtn8vY1cikhddMSwoEtwNAUjfo3kjXF3qEf0MjLOD5boTFRmpJHnHyNeCA5o84TSk3tINTAAgCiEcGu9nyTNLIb7grkClmd4WpHO4KU7mtMt5CWgbXeEhzNVpS8f3a1LO971PkYTu7pzfvJnf1PZojPXu_W1UrtO0_oJeL4qeGf1-UoEIZapjKhGBFr1bjmdX8OG_uSnaqlekMvJ-qqbWMx5E8zITU-TJJqBh7xYA9fA7RUO8WxfWrhKxy6X192UHwT7PFidXhM8pkE0TCXWpXnxrX2Myb259OybPTH-cRH97dRPgK12CeARrvlx8ZI4tvzcbPLhj7NVe1pOo36vlCR_eNerHXc9-p53o9_516V68XdvXI2Xbi6hmGl0S8T_C-PJ_8rotdXYrWVutsqWF1yUJhwMKv9oYkxtS5qy3NHkBAZQAREZxMmWtjqn2AlRHW2R6hTz69BeSIswcYGvlGFy0xVBlHSFQh8-eyPTDGUuo3r1P8ZKKmxl6tgFXj9ZVW25J9_K-v5mDs53HbP38QcDGf_GbYeLKRe4PgQFi-A044cvwpag-GRWWubZ9SIyOl0oF-AanU8vfR3vMThujeWXfwojG-XUSzDsAgq6KvsCHoqzbnWugoDBHL-6dGal2-hRzFeiJIKhIVc5UyPnAKVTVxo9sFNo_DfJUrnJadEJCG-1qkgw4Ni0ZgWIcP41UyhXboWTkyd239XKY0no1RFSSsdY0zDvoaTNq7kgEij9yrC1mCTLKgvRwDyT79vKoC5Qa1GUymFREUNNO2BLcmuB6Td_VVwShbilI0OrUxTHh7TZR05HSCG7srdO4iXoL6QsMlyMlb1RrD0e2zI9vEP1EHD25XGB1ay2BtQWt0gWxRsqMgFevHkgqWHK1lcTvkdIsHMuHmGvfkBG27pBIme6RdhOg2EhDYFJnFDCFi-BaiP0VMC0S8YFZMB7dj1xh3VSFpzyZYHmGUx9R8Ukgg05NJj2S5KNv0n_GEPCjWdZRoPmE8sO_lzxjC2ey4tUhx30CFYneGjatrN6trd4vWTpYVJhG5ukWOTxl6V1ccPs9EhcWQ74bZZ5I1Zca9gVFLjYWjtCCRemqN63qAfQflyb3KXamDIeqfOgjJyW-3VVxP3wfORTabE3etJR2QCTOZd-P5hCxPFNYUpwyyDUS2b6aelciuWI6LbNiHPg0URgmbSTv-laRiU4iYdoiwo4HiPHbO_4CejuYJsRzGIw7gpIBBhGNfRBGOOGmBAxzCY1TRZxBiVCOAowttLuR7w0yHmkAludEEfIvCoz0cRDLFHHSG0VlRW8bMzNdIdsZJy5YZqj9AjUnUG_gxrMLA0ySliuaAqGmPCjyWUV0KPEZUY8f1ElNR83R_T69BQLqeQg1ybXfMuNDxkoj3KTGnVNpttvbYUAZFWCsHDHYrGQjGKxmYO6grYcjjMKZYKuasj_AhI_IhwljMEWMxG85hllwhEpzXE7PlCub0rJvpGE9AhRFwJHeCdB0gJRkHIiyULpHYDKU_M8bYmQlj7AkJ5dBmhpx8c37yzfY8g1YHzcpXXbYhVU0fSKiqXmtiq-o1TH6HIsO7bOcoT6wIM-N_nGDIB30OqnxCV_88eCCpAzJR2Z56Sg_QJyF7HKQ5J_zQEaMOOBvCazrJ5qST4AoP7Rn_dPJBaapoUVHOBrQWcHiCRphYIX2Jiq6EWHFoQ8ZqMY2KgkZoEazRlZzIvPIm-302uJEIj7sFrPbsXWtKISZqljA8naMDnZ1GCb3aopUT-RBaflFR8QxxDS4auOr-wb3v4oLp57najLxhF1QQRjgBKzUWR7_EKXuhBjhHQi0059dO1YXenWCWHSbw80CzKqyVvr5f96g04neDWvZUkTX3w7nEjaQL6HV3dA66WGDDX2qnbEgHmKB7rDHjMc143M-yxYSrc7hGRk-vzqF5UcWLLzLVa-CDogE6XDM_JAzuGvkgtf8BdMdoLoVV4Jm9Fme2HZrr9NBc39izbexHNllGer3Q13bzOLpGxg9HALxFIZePeRw7pXxSYQlPq6grqcHzVdxgJ6qh6zeV9ClvDsWYP1dNu_FEbWG0LdlTqNHgRTX1QyOD4gwC7SlkR7Un5q_jjZFyp5THsdxCG2HS0EDtqlWo2gWeqfC-HOyZ2sCYFtpbuaqOcclRbwvhSUb0mATdPY_5sqgRBTIo9J7izTCG8SQTQ1ndBBEjjvF8EBtaHMz4rJhqeIMLKhEsjQOAfhfzSIsFrsQ89oB02kyHmoFV2xxi5aGDot4XR-eeqLJFwZM2q2IR3Xw7Ix8gbnhb5E6-ibchlZXpLeKbhIU-Uuh8aW6-mDedkdx9hMfGlqh6p8LEUw9f5JHgsIUiXdeNi43uE-XCD_a0GL_h4ItFddrNelBbob3KFPMeFcVP39dH4of1Yil-4t55yDFXgoodwvm8s52pmgDixpcXt748Pmm6W6weqfLaBi13WdN7R90E1JGFjJdAZsZEQhHi4O5ixYurLKF_wobsERuym5NvVyoWThq8lmKOfoo8Ofqhxl4pQ9IHlimGqu3IV2LHy3XlKNdgmnfMlc7jYHtdaME-SQyazsPSuKQlaujqQSQeX4sz9y5lamHcXHynDkzTuoB3K7nrSzAbJz_vvpHmQmjmTsOardurklshwlZMZ26vdOT2SppfC8CaVww2dDpwe6WV2wu3cDr2ZaVTX1biUfeTsCKTOco-jIkjKKMTSxJmKe59WMmde5vSubcpHXibOOqJ4SH5PSrwC4a6lHWs_RPMA_mZ9h60ctHzGPJCy_N04f209xGcKHjMG1CV4YTvnFY4nuIox10L7qkNqPIe2teDe6BxVSFonTtbq2PEg-CeNv1bXYRizroIImG7ms-C7XlFsWFHSMMxwAwcXVaQ4fR7dJAwoSk7ifs1ubJzEQwSdgkGCA7AfQzOVBkIzlQZiCKuuQgHAUiqSueDDGcSaGTvHvlP08Z_ilTW4uXUKdqnY-GtTCd-z7RyaXJCVBT81vydUlfY23LwWCq9mTEkzyk4NbVhky1_4NTkqEdSg6KuucGJp5TuuxZaKbDUtZ80bfykaesnZROhshK6NquQQBiQy5_e5R4MCH0uN7SmCLtJuedNmzWKnq3jEkU7JbJnRjvcO885Dse_8qm3yxmyhUzilejkGcP2J8tVIiR2XtHsaC8GCYs41HP6xYPXshthrCuF9FSz3yEWZtGSsWB3Znd4H6iNrhFpuyy8b9Yy8ujaZR6EfOb4zGeOz3zk-Myq69KTtlSw5jYNdAeubyY4nCXOypOJHCMfuyfzsXsyn7on87l7Mp-7J_OBz5Gjnlwo8oF3kg_ryJGZedJ4it3Zdj72Y7JpOsE8kF_52DfKpvMA88SLmvceT7gf4dhX96Ni6Ql1VpRLlp6x7s9Zhtdg7GKtNMcuHrzNg6rffntY67WXmcdvD6Het98eQr1vvz2Eet9-e1jrKS9HP5ZhJhFzeH2IshXeUw-yNXXYLugo4wtp8Y4MwvDaCcNCiW1Fz8YVjlTWTH_vCjfQeYzB8U3BgzicMQwHed-B85yjnijbDHXNLvYedlQ-roX6xNZEVbeBhKputya2doCGqV_xI8Fzks7NVcYx8AOhU2AfVxt9HXewnzfJgnkSOJeeW_Jdk6rbZ585zkMFNPbyWwsdyKZ-ItnE5-uLSdTYmz5OTePgMc-XgWZM_Lg4vEytNcnL1Le0sMeV0BYEdhbqT9xxdUJNMW5UdlFGL_SYTYShabb8Ag5x9taIrR5izBePQsAbu37FOlDPy-Q1l-k5zFZbOC8F_b1MbnWR_0G3pDmkGjpQU9GADyK1lYKMpJa8EgmVT9gqvkhwhQuctbwf4ns-Wc646hTdCLyytCiBLVst567mQtOAbVAPvNIcddlX5Hsl73sAzJRNwVIGsBk4wTyQK2Xv5uaYJ63n_YhqoHepLyDddzTRSv07r2igxvYVTXm_FLV_RQOnrPIGszKoAI4alQ5x2YDGODHhMyMTEq0JZfSuj9yopkkr3B8-oUGKsWIggX6MH878AhwGlc6L4yvOxXU2HlQDJNYcCDzwz1c13JEHvrD-HbGAAxd8GUe9vwEDrnQDFphAW-Pt4Wp75Qzz6GjvIws45upog4kU-plkEykESVYkMUiyeCFIEmGX-vykUfythUIWP4dCIugCwLtuZNlodKShfMf4_hy6ib5CD_tbr8c-ljxocpX1uYu-6zzR19w1Puqd-A2iBEX_qdoWYDEHE4eP748nrS93NBqiUXrxGBWJRLWuIZYSFVJVMiSSdqZanIgFYmHd2mhnSG-jnTVqC-2skVppZ43QgXbWCC61s4uu7IG6hVSXqJUpcKor9sGJakavSGmunqJUdR5PkhIUAG0KPtFXbuJhUBxTQFzx9UNl7R6NtFVfP3Zdu2j3oZ1N7DHigQufta258Bk5xTXP-qa45t1VCooIcnN4F3z2zxc_7hPFcEBWsETkR8jkFFc5A69uvoWgKRdXAG9e7yHW5kkdoqmqW2FtblmVOWFD5py1mD2_aLvm-DkuJ7ve2Xt3dLUCuSuUT66vPm720m1k3Qh0owSGMtiAqn2HSGTBI4s4NOZtMAK5yxr45oFNH5NajpY6ItNybkLxW8eMzkUWk_I2MtTs58HrwBp8ogBBvTDrx6gOsN7sne1Di1T1h8Nl2neu6c0CgfgpEsl-iR2JBKcfN-OGgyR5haGBZYoyoKR0AnQMgy7GnTJi5EeuMyMw09ta4aaHWFnBKmwudooNENPRMpkztU3gKdjmSv3BBhWcoLVGuBKSOFGOgNQZrtvj1jAhPCZDmBAHLoOREGkZ1oQHZUOprJBiJNth7DACywQc9zzHlCOgxBYHVH4iY6RzkdOHd6-GXWnz2cOptMns4VTqmnQBZlr4wlY37idwj1q-i3ryQIZwipMHMnxsSynKhrbGLN_E3JsXyLB29wPs58H9wLCoKIWXA5sQItQYtw8iBuXLS8cb4GjXidoLFeqWlw04mvplA_q3QHDr24hp-V6PlvEEsZwiYkzZWdMn95uGuY4-ZqNe46yvQXRShoyGbB6UsETEEcMS6w7lL-O5Zci0R7zTTYXAuXmGbH50rO_MKYC4u3wg2vLygUgrcYwCxMCbxTkXqrAQLdRgzzcpqsaPEsNc8aN0dVSaltT24uXjhQFHmX2dQ6zd-gg_OGTX6WnEoS6mL4xNqwfoWhwysLoDUpyyilpPoOkz7c9umq7-_ftIw9XMmvSJ5f2C-dVOFUyoKZ97YZ7b2PW5ku84U4jvArcHDJAjTC-LYG-1QzpbuC_ZTE82KR-d5uzxOlSbMq3B_ceOj9RF64fNpEcCOMi7FZiK3T44BwFuN452OByWP_LW7CN242iHG48dk1SLjjJnvegom-9G2Kxy7_G1zcFhJtyN9H6Rk27PnfutYwc2HnvsgieLsUc9iAFAod23woDbeY499ZJzYieYB7ouG8wqTgto7k7HQt-0y3fnQEBTJu3yVTrS1nVFq-WoZvNwIEvaKM4QTzQ7iqkJE9o_FWf1oB7uIYAj2p9B6MDmiXJQLoQ1QIf-nABbrPrdAFsUKN8NsMV63w2wxXrfDbDFet8NsIV6BwG2N2YSMYcAWwOsHFzbo_0Dnw0CQhIV8Qqbjz0K-XWwB0rWMdhDetiCWNtUCoiohuiirAO3O5N1A0FP6GnBvK29pSy8RllYZjEHdA4iSBB3F0Hy6G3mE_oHYd2j_0l3C-lqgY0cXC0Y6ur1HGLqr-cAQ2fmdsOrW-DCRkuniBND9AW5Xf-M8-vmYIm5nC2OwKtrxRJtOQJWoVReVbjqAbF3ZP4cjHu4bIpPPRM-xeZmSYSNx_PCr4MuApHDc_yGoy84j-I3nJLWnTAHtw6ScBRr-ljLu0RuDBL5RZ7WIXTjIHHslb1sMHOnkRjuNBLDnUViuLNIDKS2ckUgqaUVApFOVGls9ECVdueRGO48EoNRXWpTjOhST3XLhAlsNyxYlFvGUbh1HIXbxVG4TRyFO42jcGmHiEpmw9yYD2nTmhLpFkEM4OSB5rovWCu_-ayZAWRXxxUUeJsIjfRAmyZIH0i9kDgWV1bp-RnQ99CrBE94k9e1oQH7UwJ6shcnpEaBALu2YyrBRICTCcWN4RtyEIgb4zdkRogJD7aM8CyAA9HW3Os8gMMdBHDgKdkHZdS3iw1zndulolp4bqGaQIncFSIEYQ-ygBzZiuqU0B82_zozWuci6D3Y8hrsxhniyoramBKS3DGlVRoE1JjxyOip4mqoLyCuP5CU5_MNZxDZSj75rlgyE53fmsnWxfwp8aVsPj4_ATm2huN2k60L5vN-LMPk-Hm-zdlwZpOtjktMto7m-ugm23pGPXy8Vbop3tuqowyvNeubLudLn5H98wjSYuFcDkD1W4ULmPaYAVR33lUYKQQViTCIKRJhcN_146s-ZqT2LE0EVYTfbeypx5XQnL4FUCjNMTu_BNXv34k0rL7rAMHgxyT4orrtxywVovjwAT54uEhbTWc0bzXzRlG3I1SW68e3lcw3CINkVgbM1Vz5bfuCkjynDEuPuCxsTnfXBH8W1-SP4pqwzdVlAhtc2e6n1nC6iXjBtk7uG9jmEe5BIBTolH6O3BmByyRKgOMPcExRNV4fFw1BjJEX4n7kwCUfpy7NcUu13ax80oqgBDAJFandh397o88ViVuSJ2pCkGYJ4pqU5eii3p-HNfnzsCZfvot64jTy8Rz1OMUKdmD5FAwVKN-yh4xxVSOHB3-z1_OczHozW4ulyuePw7DYRJzQPME8Dthic3vS-jHmwVs4IpjO-rmkCcosLqj8bg1dEhXrG49QsQZx0rgv4RHqjZdEvOERaj38SjYPcrX1LcOEuDEdxaLfkmTqQiGUdlKc5_FQgpP9WZgQoi2vuIh0IthIOIoqUlqglJ0DZT0JQVmHtHqMSOIigjgxkDsdRCM17tSW4Cjqk5FeH499dhGIc2o0V8mI6s2o7856feEaEZHK5MMQYFwPgSzC4c2n1on8evOuRdgmukAUPS9O6k2ob4aCEg6D4OnLFCzkJqyTXCDWOmogHOTAIHOyT9-DFDcJ5pDeOssPYikZ5LAt1W-IfVnp4diRJc9AQieKMxI84C9h_hhD5x19F-rAQZGVvs_gQlRjHTtXCeeROuE4AwZSPXhUj1SXqMB8WF-XbIp19QTzQNtgY1qyvoDOs33IeVDy2fDDt3CXhGXwTlgH74Rd8E7YBO-07p9N84m7hJLUrJKUmuYqwSO2iLeBFVWS4NUFsLPVaZQIVjR6IC9XozgKqSysPwWrpg7QpcIY5SFbRMJZ9EY4jd4Im-gNbG8VvRGE6A26Ab8RuBFM3VanqT_ItO251EG8Bt70APfsptdXeHuE7DGn2keMQHgEtG6EVBWe6FdoWMbjA0LsnukaqHzdnM63EIrqR4vVBum9fnKiksOBgRdfR61YQ3IJuKjE97MgT-XpbSnwViC_NH-W3sImrQ5gqV_YgiMYjz4AAn3bKUlx-QEQbEs9ntiXlZKEHVkqSUjoRElCgvsMYTfZs--fgk4VhW99TJe2KHxYZGKXHHf8upV4i4vxPPar75Qt24nH3_tAqifaWTz44AgyMyC7YWYNO32D9bEeLxka6_AJ5gGTZKtwQPOERXdOsYuViSw0QY6VqdRWDwnjKuKGcRxNQYzreJq4i6eJm3iaeBpP04ZyhnjgDI-n3yDpJDfOcDoYFWeZw4TS0cLOgfm5b-c5sUQouOXLJZig5pAG0Qn2kVpeBvcviFUlqAKcu5xklxNVsVSgsBPHPCqiz7JiHVgjAHEnQXmmlFlEmhPxZ47v_ZHGAGgOr0gDRjdISlYAcCspzRG3kUKGvIfXuky2cwt-2kue83wuDHV90Gn7a66rxPMgO1Vy9LAtpfPKTbhPo342niPGZVaMhE65xpBK0Q8mBMvEEj5vFuySPtFGYEERGQNcFGIi20T65ohLY3AHYSvJyQuDYCGugDIfwMrSlwCB9rYyBkPQ-wKNiE7rlDaINX5YhbMv6Ms-5CKdZoJJm2AIpJcgZo4MQLrFpCkCQ-SuaRc0weZs571BaquLCJKSvwJcYzQS_65Is5uDqaF-KXL-di_EXEy1DU1CiuCjEAoc0BHuQbhFYT08YbTp3Oudzn3p6TxEIJ2HCKTzEIH0jcwnSfxeieKdh5GdXVxSC_3asfJ07KFOx15vtgtOMA9EaDqON2DreoJ50vpxFAHbKktRn1jurpkfwgnV07jgeddvX0mJOUTmJ2dhYRxUhcr5XTjPWFzL0i6DS9pkcEmnGVzSaQaXNltniCfqD8XU1B86EBVnSriicZ5NIDQgLQOhPWU3sufBUcneDag00ha8C4lmCBlFOuo_16z_EIVlmUwEjsK101oKJbZ1MCS7S4VQb2eJhYgQXeA6uJUh0onnIJlqaTz2HKQWLLMXWTRiYmM-A9xDKbQPxeCYJxKDRmIs35oCzYVnMq1zzKRNjpm0zTHDJnidEglwxJRIkOsz-fJJbwzVfX6fLPJCss_kA8d5SJBsnziy9Lm3y8MInptTIhem-enAApncrhJNSjferuDJaoaoEXMSCvOuYW5RCMKVLLNsMezCFEhz5A1XXfY85mAR70mZpqiZOQ4QWT4aJXS2GV_yJkdLob3aXmvy0RdYGrXVtQZJLVkZEjq5TiDBg-tEn5S9Mt3necAl5LRELv1MMypLRsT6tsbcZ1yBiADEPMgEmJUALdyeNehioRtmJfqKbTRN-8vrqI28i9rIm6iNfBq1kU9TrmSMdzujeIx48pSNYmraG-2finOQDIbO3JrOOjgFOe8iOKU2tbHjQ5Bx_vNTwuQ_MyVM_jNTwuQ_MyVM_jNTwuQ_MyVMPk4Jk-vy7FLCgF8nW_zs1faDgoCPGplwC6AYlyzFR6OfGENRsXR2V4ns08MA4lYw2hOhZ88F2UFwDkc9ebvMqK55FSW61Jorpig42BTqvN9uWDvGk531ecWI2Yxt2CJpVWV5Vmd5YIPPCfPBK7oIJWTnnC71JJpsPxd4L3_D5CzITcMQP16-k5yl3stL9XTICQJKGg8jP2llfEc5OiPe-SxCLFHXncuY-0X0lJaDvCtkPLvnGoXHGinhTeUsNUs5S82C1FYKNJJa8hIkdKJAI8EDb25JZPNoEUeFOsbUgG2KWjR6tdEAUd1jXJIUalTOk8f0Bd7fBvqWOXpewnqx5EqsEyeYBw9RWut7tb_IaUX4ucpC9onx2OXF1aBMQTvj-dHuBWUdrFN2wTplE6xTMJTybPKPEQ_U_bIN1mGIBwKuTfP2AtFGswn_oX1UcdIBDpnmNc76okFHqIURlWpoHkKIxggi8zE1goiw1HWWDioK7SWaqFHirXPeMCxBbuKxOs2JU85y4iDaWmyc58Qp5zlxGOpSW6yYIi9ho9U5wiYkpZwmlinbxDIM8cBqS5tWN7g5OCir8Ba6dSaVK_zWkgjeV-7PLdLdk6rzEzJJImhBS8wdHmsybktMudVdUcxFqbSEhmDDZYoASdlNhJSw_98AHX9FBmbJX_zVHDwHOmzVewnBQ2Mx1CAWrZ4Uxl8vl4lSwhh_FRFmL2Fcz7DrtOYeVLhBKuIMDUh1lhFp_XgTsQQVPvfBDtIfYYrXuer1AJYyrZcO3oQYIRGhe6V3T8trFUk399FFMBWrD6CTNtW3D1XxQaz1RQOb3EQgIbHNTQOpbdGOApVao4sLSWtxibMKZgLm3Ro7uLS0Ro9wTwKOYDvHs2-01_sQbr1l4CqermUILGdPa6Rlghns0oaQ8K0qpTUlVU2k52BOTDb1R3z4C_0Yq1cWwVrgV8ThjNfYX20Ke-yvMn8TgpR0psYGq8vUxDNu4oMLKO7hI9TjiDFEPbANEs6zRz0Ir6saEZ-Ble7EJ2CNefpFLT78k9ZPME_Dyoj4OKS5wgR7O6g7y_wokeCdZ17xz6-DjCpwb-Sjc8N5bUhtYNs8QVyXA9bQNCUexaNoSksk1JTWlAyZ5ClzdIW9o3h40PS0EnWQMZeMlM4OsJahR0hp63pAxK0EP0gU05D2JkCczML5OQdu5OG1kofI9udcM5P-hHxfaJR3Hbn73PX3yKWRySujINyQpo_ZMM6DaChkCZTqmnnsI6EK5QknIQA4bCU-jW467fqNJLTrd29iZ6LqmKqJytLtHbWcVERDfb5K0bgVV4RwqTVFyanwqNRnCg5uuXY15vXEBEAwMuLAgcEYKZkPMCqTBEbFr1ymJfUQORVwIcBa3Zfq5cVMH6CSto05CjNCYjuWhtRWFw4ktWR7SOjkEoEE96YxJHvCHczxt6DITO9RD1LcwIlkfV3yHNbVE8wDdYl1dOXT6P1cJdeim03lUWYdK4TwZefJLP96ZMtVqeSH8eSnLVtJ0SiDbm4xuxCF0rG0p3sDmLlksIG9DmGOwhca2vowHYcvIOrRrj-IdMCF2wclcMzVtgOZaNDPP0ofOn_6VtNiF1DKNfJtcO_IySIao0dM4f5auS3YiJdWakpS_yIX2Q_je2kCeiKM77_rXBhWTo4BhKiRLZ9bCe7kng5qEk5QyGSHvhRaCCFjmbASI9PFBZq33DZcEAVzjy3-Ms2a_cbA30K7ZtJ4EmqQ0mJbKVQa2UTwIPNrruoIOW0gnRA9uZf5mq5foMGYFm4FGgovVzUjgKuakWlBWgo8bOoHpX3QrEyg_R7VVY50NSQ-BRH2k-dpjVlG47Y8ratTUxwtymhwBxmmRbBBDROrY4z3kxljjKsgdxylFTNhSLcguHwjUyHA-fK9RsLL_hoLmXHLXMcMjY2ZNvDG8MDVlJUFBi4ZgOm0S0bse23-8NZd23xceufy-WiCRT4PrOF51VPXarAp2BY51fM4VYCfAZnUqHK_waq-YFsMVYtiN5TaTgdANPk5MuvxltRBBFNr70Q3R4IHqoc9jg8iM7ZHPQgl4qgnD3rJQgv2NtjFDGep9bDxnGAeaFJs4CeYS1W_-mCsPw4YLlW5sDWomoRK36081k5bHuH8WjRvTvPcua8amszqkVBp3-r9WlbMY4PprME8Nhg3DdqBFbz1qjH3Pmbl_S5Jjb1GY627eo0QWo3ndpLKXaPYoUK5aI3caoSnEXOVt5U8VDJ0N7_ZNKCZUN5r0POhFevcUCt5T2r5w1q5kEloYed3B93TwWKeSoaN6utmrVPNFhL_tPfUvLdeeS9zY82vh-1P1V2v7t_ePtUfri9Vd1P12KtH-zSfnuolidX9VL206vHpfHwav39IlaviSdbHuNTVbjumHRod3Llj7dMOIeKW5x-EHzWkEz5-EH7EUY_4-EFiH-Rn-xw8HPOE754EStFNEUhJCy-alpJkVZvMCLAbAilJEyG21OoVy2qxV0zR0KtrsZpMudCr67GgPrBxaRblhjgFao0bZL4PMpVBB6eD2n4Cs92xDwRlvTiQyceI-7A5NoSDrX6CuI28bWrD671MR_nj8mt1qHcPa9pZwpIWr9o3hyVC8hV-00XctoyYvlomGiBbAqALW6_6raJlFoBWzWomAER0vH5rN2zaDbsG2Azdox6nSJkIN0yEpVcIE2ANbnaDIXrQH3s64EsZsFUG3BCuoWfO1U32vlEziTgwqi239rS_S8ep8KUyGnQ6xD4-8j1kR25exJHzblcHcS0vh4mkrxVaPe1XeqB5gtYYB_PWNdcEvkjl0G8MpF24RKf5ckybrx_EKxoSxNS9lIxgYYIQQQepCF48EnUI5dW46vt1nkIzBTLSqffTpGmaEA45zuaJgm0MBHwlMJWbmTDAIappWj0Ov9bwpIJrrhy19RecVfA76qIPukZdrcHGbuqbsEFwQgt0R1jXN-i88rZQqLa0NvXdJ4a2wk6ObSfLWDRQM2ls0NF8aWlcDlugDaWeM1O9Ok0XDkHplol1m4wV4zy_cM10ZryelqPrKdQj19N8dh92fmzwGc5Ji35s8RnPUZNpatKdNZmmJu23mizStJ5UjNK8HswOaTB9az1Ig_E7G4A0GA4bNGODft0gP-tROetUfiRZfoSNhIBImQOkXxoSO-VrieDXHP2X28gTURnw1YlT-YexE7i6w-oHalEvJuBqvAYuoxGvEtAI1nLaRh2eCRpWJnzdCMoFzHdt0-R5QIhQ_TZ5HhIiZLInjDDftQkntEDhxuoItQtW6AKjEHYUwoZC2fQxbhqIOv3qkdDhmShg4iRmornpCHkDDzq8PlLf9BDWIGrbANiHEQQ33W829T0uo9VNnvomF9UA2FoXUtNGdSEdbVgFuz0J8JufjsLtzMAMFalw21mYsWacmjxTGqAibfJQa4CaRNqYQ3EDNYm4MRt5w-eHNukOm3RTk_asyTJOrD3TjaAimVh7qBv5a1rLM4cIVLzEeV03WaaJPdRVoGYUt-xBZ2mT5lvzQ5u8NuoKY-lrhq7z6_fY_yqyIA7ULm-mO3-oxpP01raT6A1gyqvgSUfYEQdjdmVI2oW_dgEUDStc-GlPqtixUtx_5bahJSIZu4QIdcR-HjEiQF6Secww4dCEE5oAhNqEEZqgCFmH01fcYgtvH4vexerU0uFgvF93IOnto511OQNWp18_rrHp_24F9DUGx2bfLPJeqb30fecttpQNfRuLAhyoJaSmDAq-w2QFCQ8NZWxIacJduyay3kSdeDMjABMLdpIrZxdgqEjlyuEFOEx-dnsmV8LkaLeHcgVq0qxrh0weahImT8TnwfxQuXJ4z4eaVK6cXfTDFE9gz_S9MAUU2EN9D2oWcfuc1Izi_llJz3pqnNmcml9mzex-CezEvQb4iK8zURZFMMNW_hRbcXt2jrFQtRgeJr16u50u0pE-I7PaHRiaBFaFd-DOo4DKNTcCxtFKAOR8_GrP7VMH-hFYp6eS9gJlCk8buJF6Rho3U8_ouBMft0bEXh1PssgAlttgtQdduCIiv4_ttVRF0xDsDiELCKy_kfZ3gpYBCucpTlY-cviXJzFOZj57pjrHyc5nD29ecTL02Z2lz1py5t74Q7aWkCki4lPw8UUSwvtr3QbyiVSFrDH1RVuidWo85OsW_hK0Vm9NR46koFrZBUzIJ-A3tESMmztxXvXsiSreXyglMENrqJYM9TVOAoD29VpiWU_TX3uSRtity3wGlpZGmgpOrk5ZpNPCnyuaraFQ3nUcZzpOUVA8R6nTJ6OmY1R_rVDhpREKBwhC4kv4WwvWiCRVQBiL6wBZCM1FtuoMxtiTiBkKIEhFxMGUKwIOeKljz-9Qg2A83ZbPx2vHSFrK58BTpfO5X6CVNhnwSoCEoe1vX99pT2ACra8-3hIMuu6cIeErqfboOvRigWHA46qEDwgIpdB7ACwgsadQ8jOK2nt4BJDh4EAjVuEBtSkJkJeAFddIYYthVQyIyU88Rr6zlUuHQqS-DAQOAIsrchCYdhmWd7A154E5WePYPXdK-FzuPf-cOSTyOOrlDa8AsvRhe6JPGu1UbkMPB6nLweDwEEOCh5P6wFQpHB64pMDoy2k2sZUgUKmtuBo30_qBHnmER4BntR062mQ_DW-M3zHCrDUs8oKFT7gMzBwI71aEJfkN8575dJXPfVBeRfXmCPFd7VfVxQ9VWHpcrvhq_HedUjqq7VFBBjp0-U_MgOuN77iGkPVwRK_77zfeKKZPpH1wBBkEBnJQRZ6kfAD2qrhJYQMnmeYl8vTxMAk0Yq0PYKmV9nCzSTQJNxKaET4Ys8IMW8yagQSat55vRITCO7SI5Sie8Hse7AqZMaFtF0q55cyVFd_sO3x4i58oQkL9dhYL2Sgyp1aVAGkJWMmg7LcYRseoHLtiyFIBhivD0g62lhTQ9zWOWeCANMk8xTEXJ7nHiPZ3gAJGYgii2ppJ3O0BolkggkDJNGxdECi5OQqQ0Y9PJukkhYYmiYMM0ZZEbIhoEC5K5AE_JwWavD7-5cdP8OYz7Jo7HZIG1ZuRgmQgNcQSxZL7l47Uk2Caq7I5Ffede_hwgueYF8Fsn11S0dOLbU_R_ffQzRIdoqszGvJ5Lt1-bULOd-G1abgvZRLwLYiSbPqRkMD-AKwTJwHxatvipSx2eLEfHcNrGKwJXRSyju7R0hqtdSudCkto-ATTbDFrTHWO5mPie_nKn3yhByO12yi0Wb9RhtfQV2SWlhdOFpmlWWJHBRMRegq6CVQ2tFvGn-cWJCTISZRMUu-VDM_4Jrwr1RnPEXrdVCXTvIhCgLhUClvSw-KbhgLiuVYXZH_tgwSIGkAzkZVroSRYMuAbAx4UWjYR1fhgib-rA1quOuKwuNX71_To7n13F4LtEUQ6TJioNlSyCiwuYAuTGoxeUTPq3QK36IPjR7MkxVuZ51BcFZc_6bl_lPJJl2UiDW73Gsp7OSppRwWFnYYziZeG6AiiKowadlxhu5L1PjRFgBNc0QPRBewmbCx-dVVVGVTcAVgVUSWuweUArAq4sjZKFrcGXwdg_Q5Z1ibNSPjIQsLWVlYyuI5CwQj0WOoijh1NYEGg8LVvAOB2ibhPmkCDVcj-EzqbJ1dJmMgSx3JwjdCNZ-FGGa6WhuMl2YQkAoigEsRZuMKIJQnMcLE0brN8QgTuSpFJYo1AJB-vQfeQeXFvFeGNkbcufvIz_c8Qn6ftndlfxM0EzD40Pm9Nn5JZ5EFfGmCSVzg8AQF72CZI6NobkeDevkHPTPN-Xe_HVtf17fOAzIzjmMUqjEMCTFbnHHOv8Xj6kJvlmAggUgCt4bQal1LDJaWG8x1wS_kobV6U9VWAIlgSrrivZJhubcYVWN39cZ-scfbWZuw_zfLj-I0dUBLDmE0FgOYFNMkOgA1_D_3XHr9KysaM2iWPnOfTzZ5xs1f5gdtKES-43CpYMYACzwe47CwDyYGnSr29tW2xvpa1Hba4QgW6gw7sndj0_mKGrS8wq3MQEW0XCcwTaC4y7TFySfaed2MkAg0Kl-23a-8uM9U0kMyID5LMNGPSSpIZv8RCWmiAPaBFscYnP5Y0abvsrTkShPrlGqG0p8N3HyrPNsRMKgol43cISUMAqVWbcPSDMvYJ2Ah7AVbSe35p8Ad06OfIpZ8h5-k2iqQAjNyeXY9RBA2LS48BkURmb1VGSmuclVW5XvUAByM3oKxbvPswiKQx1LBkhqEwXg97YeT1UwQGIo4yRw2u4LvsvIK_TipEsgr2YJQ0zdRylEmeOL3TSZ-_VYWDeSzPY4JnWh6vIHx7JD_a3PVs_r-x__RPf7J_9xcE8bKf9Hgxg_3kq1hawb5Hx3z98T_9zT-9t_-kkW43e9yhKgrIN6PbSSlXXcpa2CuirCUsy7uZKXB-0NlB-Lgr-fRyg-fjp5hiIdhmyqM-umAjEVAdL3WfHdbUfHZIItL-w7UHqS8NeUiGxi_OwgopVqzcTI3ytasa6oOtl9caIvowTfyETSXdk6DYLkcssdaJgsa6HULcIRQNAdbdXoSJ_OtlS-3xz5GLP0uu_Aw5tn78Wmj7LEiAIonGaips54HFAdX4IhkGhhMZCALQ6jZY3BxLAWpXIYiI4w5w9rZZoDP2lY5RF8iWZfyXhZRt8eeIM4knSz9js4xtbLO3Qq3eX-xcFbxzuFFfwBXYr8FGBUeZeHUpI-Oc-8bg2LgcgRSstx_ayui9QTRYxPQRWqsYCVIJKe3VSKZ5wnqU0jxbE8xwWA1gEhaBcHe0xYLLQMhslft1HQXg2rDNOiPJdauYj1GmAzfT7892Y3hFNHeGFr-DFna3cdtyv-xv4xbTVWxv49C6297GLX1VMt_GHTD49HmdGf5zZUuYDtzWYYXcGMLkLOWLcNWBjItRgHQD3gN0aDt5YERtcc0kLSssAA-qbxTpoGnnpa94WgHLMgeqF4j1SMuJQiYK1OYZAHR-oclgf3SEnS25NgHXn52K2zybwmsM6MuPqSW15z-mljjBjG17tynA0kW0NViAfE3DkmXs24XvvSre3cJy7A4swO7AAuxWFmBQE5xkpQZB6IjpVxB0jhqPhaApgkSk4RimVVtime2VAKzgTBpkHZ_Zqvm554FNDV5yH_tGAglhqzUgNrjy4HzVSKdS8-rNAbGoN9665Se4OuhPCLljk1DbGsF74-aPf7Op3zzR0W7wCF7PWJH4VuIKdDU34sSd2YDdxgbMG1WFE290K8Nc3uJF0uxCKjXMcCi_XDPfM4nlvKtLai_7cUxmWcr9p7NeBRqc9SjG5OKqjuLu3cy-pRVgl37fgjIHYea7wWiUGwDaPB9DOhiChtxJQTMDE1tgNWJKY2mG--TmVki5V-gyeFIHwch7YVMA5_c707bfmbb9zrTtf9a07V-e8nPkfsi03Xr3HY3BSRpDqKZD7-vXJi6ymR9JkEtv5zVISVs91GuFfxP0v-m74LVl3RX1UUNpxXSp47i0glrgjWSUMBRYP5MhOmt8GvaupB54PbgcWcQCtlYL_IEbwB-4AXwPjhctDJ4FhXNbgB_iwOlTJqxOI8AVA4Wn4d2ygQJHsrI6wDUUWfLmGorztxN2uBY7Eea3Ma0gwnzzLoyPIuua5fh5Ewbd9_PPkygPVgTUjRbQipvLC88rqyM01IfKrjPycA2bVgylqVh5iqTx8ykI7a37eEywxW57mkCu1xVeXCKcpE2wntCMfVOPzfn5cQukZL61PVBRedLsu3z6rJVYG7AeNWOFBhpuCfXTOOMXgeg4Kim6CHacnfo6DvhZ2FnPw856HnbW87CznoeftZ6H-DPS8A2YwwlqCgVp54fs4K3b3xCTopRkSyrEWQUlRBqalgBziLQhsyKLm6AHJUNLCkwPSsY9tBRP4cDIHg6M7GFlZAc5FKhRcJRDuKA_5MWBi3egX_MVbt6Bm8dn4zHFiprpOKDp2DU6AycuMJHVqOfNx_ji6hD5eyYFx_TnTCoGec2k40yPmVTUMWK7IRqKqMV3N-x7jT854AEVArKJvFNjvsIyXhnAakBZ0K3ZpHE1nhmXf5WiARnR0hMOrES3moczO3co30HbmhJCM5svzeFUYTgKZIP29wYFWPyeuwLLHbkAEN0KwRB8bQawJ4zEpFk1e49sBIsc_5t9k69qcBESE6cOqHpXtESHlPSu2E00o3oXpdjqeSJpQ37S0UCFqR2ME-OlPQm9q2JPQ2th6AFoU_GyXWWT4D6t4XFTP76abhuGqNLF6wTJb5TITPsbLH5NJNNhBrtqYK1YTh1Z66ltUA1tUppZj61ZKcc4z6xz0jwrrbEt9eIIm2AxcjaLrassMD_uvC1x522JO29L_FkfSfwhH4kh44ckNhA9HW_1oDKf4ZUA6YSCQrVb5Y1E1N4WRO1tQZzeFljK0hLX34gtF0JG2hjFmBEZCpEoMhCUzLhw9cTFI4G4eCQQD1xE8cBFFA8eCUQWq8-NNJH6BxQjTaSOi8FIg_vE_qj-3MWiHOERm2N4CZ_1bw5Xw1NQTGvgoJAHxT0GRn7kFxwrNizJZRb5R6FVtC4eTtDWbjrCcZcxME08K2gQgjIvpp2AcQV0K-C1AA7hMhD2ImyOrr3GzTOPuE50g4uqqdp4pHfKcdx491DZjdtXG6C1VsRcmDaafBcKtaCZbFHrBFUttZcGciQGwGn2M1mrTKYTEtXXivAsqxKmAQhGyFKDbfhOQmwincCT1AM31gcDHQUkQcMBCZd27qq0c1elnbsqfcuDs4r5SD_0CAPVjdQcprO60XDejx88Z1NVSTSUlUqCVZUenFR9fU4_FrGSlERKMOkSYHJmWXpgdOXI0hO8R0trNFA7kp5XCY-fDFu4vtKB6ysduL7SPq8SzsaoEjlygrq2w1xbBGX0fpkv6pPClib31Wx9TOZHfcLJfIcFiOZpcKihPNg41JLyXoOBVTtZSnJtlIUJ3ZALGxecJ93GhRtnJ3hxE-4Ebzr1-cHcuGZlgriVlPznTXFs0vUp7Ssd3ckHLQg5VF9RndF61QrAFPibqUnKAaNmQK0YxLFEIlZqRWqDAjmYtew4IGez63BRludueR-lOIDGNDSyISl3Q_hEB4ykRyatjE5QZTyxz097OGoIxFK7QrLjHP0qdPLcui3XVgsp9Qq_6CPPjEboUVmvkRQEDKk0obz08nEyLrLsTw8g3ALHP5b8cqSkuIZTAz1yfq3zuU8NAt4IkLux7sYkq_CoMF4I2QUe9uRx-kGWmN3Pkos_S678qHKRtVc6WXulk8VXOkhulYWcHgJYUa6TW8oKtuoHHIItWjxDK2s0DE5vnHI0HJkVtDrOZCioInnhes1LNymc0cX7oKy9D0I4cY0u8j41DrZSlzJxVY3qUiY-tFFdglDa-7h8Kpe4u5v9--bzvXgLuXBDDgWR_cdH25BpiG41-OWA4bwmfuLLnSuqECicQ_6ENyGvCZ9HTUNcGicMMjuT95yCByjT0BcCtriun5c9xFttwo9x35vwuvfdTbfcHTDtjlED2NzFKwZW8dmwqdwVix3qlWuomFnF8DT5fJOkvN_O4HU9OduRZMbzhm5cAnh1hWL7zDBdoTRjlmwgAHjYGggKefMhKhUlE4VqaAQMrkdKRUFLj2JnKGDvF5Lh1tt-qd9yD52P15IfEwsl_Cy5_I1rgXIrAEI_c10B-VI0R0ERHAW2j0SqsUt9DzSXGHrqexArFWMrfWpT26t0WXgXysK7UA48COXAg1AOHpkUyZMBsqFQ867iQSjNaa2GeZbJEaG-Qy2iP4KjgqW-kLzigqG-EGuuGoYDg5fei9Y24toMDlE45XGCPqLpCU2wV8TIFDpdqFSscQ2Jytlh0uicLe4cpbOrMsXf9AqRV1DidTp-WeKDQQLlj2aQwGt8IS6vQMvRPK8_0ykbEz1H2z2sKZsHMxxt2yj95MDCilFa5MU-XgZoXrs8u_ecppuBPxv9-lh7FcZKyOQaKSz5ORdXtVS-ForSC1wcCozpefWqsI4Xeipk9QLhdqdeYAtOi-5HBMU3gA3NMSusI1bJmFsFKLTBvopVZoOcDf45uEz24pT9kE4A3f05culnVIyb0E-qGNCvWZOA8csXX6i10BXaai8wjIpRoxbaqZCCFmRgjVlQKlrYhwh8umHG_ZPIgoR55_XpM6VPhKiG4CStVAycpjXO_sFKO-XKgxU8u3GhiiAOUVdGVQS7MmgrkiqCqAdaSxvgXmvBPmrhCWRxF2AtewbMoqYT4SQvXwUDUlecxgCH2lBcRhtUV70wWDsCxbQX8zC7j1-Yoa4H4XbORlKW8HBpOD2N9ZoOaFA60qgK4UnyHVPWmRDRqIg1fbXWNMtfjcTSYavPl4Y1Fc3UVmvI2DZzCGC_jB4je--N5NOIXnzT_9pm0R1SdGtMn4gA_QhnpqmH5aLCW1WvCl3SU7SN6oeNHqJtNMTGjjcaIs5AOdAQsemFhvgupcG0O488Cl1lMvgy6y3XFKeKFaanZ8zbV4W2iQNBSVEzbkCSjDyAFVjOjyAQuzSbE4I2NifsudO0TzZXPLkIB0U13hrmhbvJOEicfscQSJHodXO8m7Pew-hJvQVNxPzoSzKY2p8j96N52PAM_OsV45v6TyrGRnEVwXRKgIWrCIArfdjoH6xFDLfFuFQMyLk6sghUgkHXNLpbBoew1FXN_vUaDmONs08RhyxDcgHhMVT1YUNeIDXeRqNTkPpSYzbtPYyqMZv-IYOdGmxaEMIp6lJjrsa1-Dy4ufJL2H_i439WjWsbXGJc22IS49oedzKubatMKlivkHkFRWVr-M4s8UFrAgHOcq4HolpReTda1nAvBiWKB3fRKooHRYIaxYMHZqd5mYvs2B3aVvPCU7pDi2d922SLQ7TNKzqOdti3rVZoynoMDS-uJ7jhtZxzupZZnaqIeknBTrh5alIdW5p1sKbpAXCKhNtjTdu8haiV1mLiZWSGRhu6ZiMZGgGet4ZGS172iaqeVd_pIegoHxD26NpouoBmVFUX-51HcoVvKNNH92Pqkf3Rp1Mwhh9TkqzioIRuSwDFQYkru1Bu7M5BCd1Zqj_jIr76z8PtzWVs1z-s7nrE_SLD9IdNOMKl3mP3bkkc5SosBdrCKN5Bv3EdwzOMKET04pBExFE5iaR_q-w2iLPIbtOGudJxwJxmCzzL6S-Gfg1nM1GWp8jxGpMT7ZU_7s2yZ8zHx_er0x7UgikoJz5hqMCg8ydWpuapXtO_UX3jug_kN0ifFGPHpaE2NYTnRr5q7Mqbvy8T5Pkb1bhfNe3ALt_eAFh9ugPUF_DQd5tKfi9j7cbVx5tbaABVbloSeEPlpqf8RggiBtEJ1d0gO99Ho9F5oI1i0ynvgwBTfR-E8O37IGxBfR8ECFQ-ieYkR18JafYkEBfuZ51gLv2o9HHKUxHotgQQvF0EsJItbuvQcluHVts1okdLhoLVXalq6SYCg5S14wK-SilObpysKe5mRzkMni2nv2NBqjJs4RFzBx4xd-ARc_t3LA1HlEI4Oz_kMgb3kWuueNm9RFZpBdfe9uLUWBVuNvSTAgfXlTNK-whPDC77vwCpP8odHWC0qY4l5YiPzkLiAhtkuY54cf35ckSr82vgFi10LtWDGB1kivDvPxpJghIi5G0gKHODJY16iZDBftwXLKEqePTmpZ-A4zNi8PcJm47YZlw0n-vxSoVw6xNohLXM4afiEIffkg5adVSkyTzTMH_5j00ldM8a7BO9hqW0hQrgAVT6QpMmNpm-fuMFaNojLliADVhVzdzyARmeNlUzc_sXYihG0bZEIZ5BtOb3GptrDr9tRqMmAFa47xJ6dBm9ld87g6_aNXzmLo66mG_ft1B0I0AwSpASaGLedTKSIuaJwUa0OACC9FCb9xQ-SxTVnvKE-jhoWYH0hdOcL0mmd-4HnknflH7y8Sb0_-fI_awfy_-Qlw2USa-kP4R2JICS_hDmTQWsVFq_9Un5rU_K731Sws5EEUhsMn7hm_J6tkY8KjJMz9aI07PURP2Bv8sf-Lv8Plsj0BFfZgNs9TIbp2hAES04nn8ciS8KJ8q_kcQYi4SYJUTeB_BbeRM_wb7Er09-okB1v9Ua1xC_1Q6T-q22uLPfaldlUlt6hcwrKH6rhn_f2Vb4EEPkM0Qcxa_lZ4qaLJ3ighAOEQ-FWs64qOlagV_mjkRaTdvAYiUpJAPrNiiPsTQLTcerXrSGETcaDx5U1fbE0HZOKu_O0M48Y8hozqjtHvLjGPZfqsCGTzC332SqIfXR31e__CYBKNd9BXCEHYJ9DvZLnPeinZCqBtWNf7hDh0d8MYAhsytiAc3kisZY4WGtMIaezlHSF0PPJymqixUedG0xNI8aToVkaQQs_hFLQV1kI0f_oKCCBulNoUITlKTwQw_tbkI_qlqGn_WyhR_yAYIGFTQvW9C8bEHzsoWtly1svWxB97KBNhMW3rOw8J6FhfcsHHjPwoH3LBx4zwJxlzRNC0Rq8IVcT7dhucBRJrkKvqZg3Se9MvrJyvx-pKnmY5VcTcHg--_nzF2mIc-epnCTew3FxpqPqzfROqmzoyk8ITB14t3HuNJw2VtxELNh_JoThSkZ4kDuhKMPLuEibtGOno8h2i72JFBn47ZvW7SjN2tIbdu3_lm9peEkNL_r3sgSWjDHPsgZ2k8LmV2FLSBeS4lckVIci-2iWBPyIKlis7uvxF8cH8KtrSXRjERFa0n82Vdn8WcdbvFnHW5Rc7hFzeEWNYdb3Drc4tbhFlcON7KGhu8eYngQFrkZHuagkLhwieHuEmELl1g8cInFA5dYPHCJRf4IbHGtj_wpmH6tj_xB2OJazyuIBoNVhXPDwT1dn_p0xuZHXC7sBktUajbYIFKrwQ51Nhpsakw2gI4fGL5iMujoeYUO-kocnxGtLvpx7ZyIa9dGXDtGouLaYGDRHhAJS1rc9YE3yRhkfHoPqd9Nl9_x7O1UPHs7Fc_eTsWzt1PRk9O1bXTr54lpPSN8cZYqRTUDxBzq5i2J8bxOJD8BPJJy8GrrKbYLff27RfUPrqFEUv1J2kOi_pZVmE6i38iQbvuph2BPjVykkV10a0KTr2I1SMMDrrmnsRGQnvunIoNBtUg_-0gp_ewjpfSzzp30nUdKK5dY-tlHSknzEiXNS5Q0L1HSvERp6yVKWy9R2nqJku4lAg0nLdw_aeH-SQv3T1q4f9KB-ycduH_SgfsnHbh_EjGbE_dPJG0sQ26T-LxIQll_ULKaQVJGM8jT2NoGgkx38zQ5DY9sBmUjrb0Kae0-SMoHq1ByVepr9wHwKN05kM6s_rjaO7Qzq386ew-Tynca3Ur6tHlv1OZs-82phrl1DXjCS8qYmA_3h_DpT0-4lPxp0Aee8dMVqCvk6QtUtTjLxfz7VYBGXQm5x8WPshpA5uipdG5mI0XDyLkjSLoMwM1Wl8nd-SFpCFn5Dhbrp6pA5J_yBeQfTboHoxIEYdas7VmztuettT1vre1Zt7ZDVHHbuGJUsQyF0D4ZCEIpL6z4eWHFzwsrfj6w4ucDK34-sOJnyYrPYMuP-7R-SAITt97PPvbKP-SGgnjf3Iy871boG4fD1XjhvjuW8GsNV5Px5PWniHAfad8SQjh58yMlKUS0q6OJEcnZjmhSdHPWPq8EEb_ZHX0gKGb8eNHyq0QNTckTNDSaNm3ix4vYR1F_a2HE83axE1D8GpGwUWbgtQAOWYwgclnYfl1ZzIpfjYFN2YTu5vWzqbx2aOUzp1HeeINQpcob_xgqSkBv_MACKh4Fs1S1gmYxaoigKFRIUoMbKjyvgxuKb0QkZaR0C9NEvoI2XzrAZpoJStF6ChrrxsEweOL9cLTmWJLENTWkyr9eeSk_-oETmNKfI_ez_qOi-Y-K5j8qmv-o6L4fT_flgzHkqK4aRFk4c8rCmVMWzpxykKkPz4OafB40lEKs42O0ZyGG9THaE5h9udZfxwNnSbncx7x5M5L9pNTiCQVnyRLVEGfJBpE6S3aos7NkU2PyfjR8dzF8zVvS8d0SH-QM8lZNzkCIRQkJotlugR7ji-Zem8mcjz-WaD72jd3I18dSZCMEhTw5lr19sc3nodCw6VcBIuCGT66U3Sc523FpuAmIrbJ5WlLQ-aN_hBto6EGbeGBUCVpf8kJXrnbjB-9Asb6-1L7vup9QHDuR5LA5wSbwiqB0XUQELazt6SKOA8ncjghJtbeniyT-EgRYukgaM0kcI0JaptCFIf2UCIBx_Ry5HzXMw5T8lICCuZvFDcyCBFDs6dCzKCxzs_6N1plXiABcFDC4yWSYbufGnSNlqcd6TpI0CLxEUcNw0oCCLFVAHZ8pLFDXzxWqELtnrHzM-wET6z7RQ6Z0Lr40JCK4dBQishZIk7DScUex0zHLi2ia2FHeO_YK8bsV3LcrrPGrJMT1Kor5Hre1cpcCsOZqh42_AevES9-hKlgnrufvih1e9EgDZCsrDKdhZDq5ASUbVLBmNINDbyTApcnCtwmDVuXOqQy3dM8CyUAOjeUFESgLrnVsRs0Hhc1Y0owo88wPpTVCcq_69d0nkHakEv5VVECOGMUcDdMqASZzNKSJwDWud3HraFcR0LqWputrF5ZVqBjdPoxrLsP0T6ZgPQxR4yLBUIOQIowMtTZtRIwhxqcj1CPBZZjZWUatTtRkzOJ5FvGgtsOpMVejJOhhYJU9GpIsSgXrxNffBUf4tbooQA-TiOPJObB-4mvAhPzwlidZYjCiVq9kW7iwosoDghf4DaFck_8Aj7Op0xScXcn-aJgw9PDbb7R1Uj_Xsx-1GMG8CUzOKhYj6AADVLZjdYMPrrgMkww-wK5sv5jO7MqmfqwUbmXJ_XjDViy5jmuo1XGCO3_OE1OdGAivhmY7lm9cF_cibZKyoKZsTfo8Bo77cp1D4tp09T-qKD2zyJIK6tkazqTTIqKhiAsdGPH9Br8mFVH6gR7D0HehykztMiYW96kKVuJxkctaNGvrQS54sEg2kGqEwbbjYIS5iebP9T5hvq8ixRtySt5caclhqMhvNfsUL3knzrEEAIlFViSHd9DOwh0q9IpiWuFrxwXQ1RwXAMbPlyahkYpgpvePSXNiQL86TdmVAfMjSBMOFxKMIMI6QwnAkyQB3dgAfHHZDfWKVI00O2rybhyY9JwGEH5MGrmftTa5H81AAuQOpfi9cAtK7htXDe2m4TTjl9OMX04zfjklmBQPgB7vgqdqhbEKJrWEv4hfXa5BM50FSUEzChSEllK1PimSjjhetV1XKtzCyOcWRj63MPI5PZgVe7WKqcGZX-Msg1nZ5K1ic6At0buFY1T0JjrJ9BVRn2lObM470tiugOiMhCgqZFDBf7dC2lcAPc71lwJcj2NwNR8gwsMGnlW4U-iDvugc658SWYNkQkeTkv3haBPJzjfRq4hhyvY3UKx59cb5EfLqCVMwAwMH1ugUafK7IubW1ky3toW6dawywNVYZdKEquoRLUn24GFXNuHArMc7Px422P14WM6ynTGeiUqgZ5GKg4bowTrTdTNPPlEtyABDkMysRYK25kODS0qUtw0smhG87X1YtR_7mCTrJ_aSvVsWVEzeq6ipmDg2ljIkjfWp_Bo6ZWWsX6AnWqUTfIr4DKud9bSzkRSAiVP61E5be12D8Luo27Z2etIyqe9N0E9vh7FTS-Hq94Gv2LGlAPaiYRZg3fgqfRgHR0Vf_iqS1dPsBSvJ6nms7V5Q4jx8s8KJ7AaPoS_3nbU-oQ0f17_wN4e9rFEN8R5uEKkPcYc6exI3NSbDRcfPDF8Je-n49ySu8MEeDQxXM0fjJt1IDTwTZ2ibBy6ItvtCC54A_dWupwfFyTLJEy4RJ5M0sKfRIh3AINj-9iMHYXbqkBiLVsQDYOWdeACOG_xIVHS5BUOobmwb2F3DKU88NWi23KDZcsNky8W20EymiN4K19--AuX5bQpreCUgwi47BQx3KULmxRCuimFhtw4Lu3XQAxVxfEtJE_ZZJ3CMy6teYIZrfo8L9Gn8UtoE-p59JW2w28fCo1b4xsUPKhxc_FCGtCY6BwVeoMSvN9bA0jDws_cbhvWlJ4HTm8vaXO7jQk0eYSvfrpKkENtGuMrHv583uezH1e-KVGwMGOx5pG7k9CkGkC9HkA0hXaMcb2T3sZWy-VyBIDvWD0_5RTMzt3K0ceu3Etx2G4kQaK6RhUQINIkJiZL3hBfUd4OSKKiZiqDbYwIjHGWcV_B1vKR34mJ-g4TqvRZjNjnMP-eYsDIS21lZXrw6y1NvPPFqbE_kWXEyAQK_jO3mJt-FIsmsLXLkSDNriyw50i-nLtoQEicgaJM4gQ2yM-Z3rt-THdF-PRVJwSmVBcaFJS-uLGHYGzf1BmuvbW2mD1ysvbTmQe2i1l7aC2nPBU9rlD_MSTosVRItm7SfUiXJ5GmxE_XzSd6Xz5t__rGEp6_r1r3CzSKerzt9YVxLePV4d_GKiVV8lDb_VCx2qFcuXvHhsKRieJpM5W0yD1WRKcNAgmE2ZbBVy0CwRstAsODF_t3ji505DncqvBzBZ_pgqouefZjLTIdoxHMKHgTMxLD7gnqh2JanavxljYwP9r55NHYCOg4EY6AwDxPQDcCLHJV7iz48x-ZICkOXQZWR0DfvjnAn3GsPhdCL3yS3tdjFdyOldH2hUXAshffk7AaS-Av4mSWn9mhyEgtJIW0A0gxIk4GKIYQdgvAgzFTmkHKnAMzt_XhLSmEC1G0INeprgElEAk79Uhe2PAo7SxrIVJcASJvRuRKdkdGrnZV-ZzO1V-1w-SIDys2PC1uyU8LdSOc0tQOjz7s_wDEyzjiRDW2YSzo2SmocXoPVEdLF9_ioQ1j_CZZVGJs13_WXaec07WaOS7BsMF0_GQ9LGoBsd2iPVPjuukYNC7h1oo-XdfjM7cGRQzbv1SecA3vgpei7ST6NAkGSBw3NyWgoNlOONfbk4aAXe_aU-r3k_u9TCmLZeiPp16dU7quNQbB51YzaSLWNpRI_7yOjJ08uPCd8VsCgQpJec69Q49ZH7536TsaMDNw3x6sdp7f0Zay52komPP6Kl-Ja1z_-CxtqrDKf02xGjgLHJ_PgSXZ6K4JnCP0kAEnCQ9_9OVXC7V1PcjaUUdF7tBk7ZZQTRvtlxrMtwaQeEF4yd4JM2cg82Ng5FxjXwytnmCHV4FJe5N6KvzULZSYPmv1YzOWIH_fRQ8zTHjLg23gwZM5erenhEsD1jBuBv-4rKk4VB9-TD5PRMRjxhvCrt4CG73zr0vXFYvrcNzLAs9NpCn6LPWNd6eP9c_npGFO7I858gku42s7pp7LUSKYJj-1-hmsjOcEUYPLc2kVfD08VTBlXFqoVsq3G_szsoVwaF2AkjLKjGRXl2HJCTC_i1Wu0dWUExdEiitXOCMdsxby5SVACEymx0-dFUuSaoQiY3aka8wDUPIWdrwhwp8Ir6SEdgSHadGm2k0nNKuTR6FRqRU0Xmy36iIraI9yI8AzJTJ2cJp3VyOQT9s7Sepjv4r3k_KIZcut9rl2W6lZ4vu1xedNUGTBBxjLglfDxJZR27QSaAno1QnwP3e57UfHSQzcek3Uv3e3grD8lW9EvnWzlI_nycKMbi9LIkN5F7_jT_Sxfwl2Ig-atQokm-fTmqz0Bmm8eFEE4LdhwfWKtQeVTU3VwJO7IXYwBYphOGoBe3lzYBAhnL1--D0_gFggvKrze_S7h7PLGpdOdXr37bsN8QPS_houxOOXP7FqDySePCn9Jt2fEmN_Jc7jwhWvsRsmsG2ukZwjT5RNnQrJJs6nK6mTy8V6ClGU0irTURdjJFeGJMWl76vkoGGWN7y0IfVCVi96tsIxThIsWzkRLqfivb-q1LGJAngWHUv3AmGdYPiOWM19_-Kt_-7_--L_-zX_4h__4b__v__I3f_EXTzfqAYqvjdI1TeqmZj7ejAigEc5YwHJMqXpHkx7ZeJA3zLaV7-rAfqhyBti3rJl1YKiSwsTKWKOkG49-RLWLqdhAcTuYPGwJeJ4xMhiJ0pGotS_xvQNrX0RhjSQJA1ggtKMwSOMPoAr7ZPRXiknDEU4MNqGDJK7G6cpaDd13pW0ntjOmpQMWYMjjwNFwwzCiwBSxb0kfGOGXIpMyhGHKCOlw4Jd6KsRdBVzKRMeNPgPrmXgVLBVm2iVsjat6gHiqkyGT2OlkiHeokyH6QicDtmMdXOc4k7K52gVGRGemqoazLDjZtgUMjwcaSc8Mw0KUfd-EtvSSNJawrj4Go_A19e65wDKOZ1tYsMhtGDGth52Nsm6S4iQW86mJeGKR9jUWU2zXNvNUg4Hq8tdKp_cIQN9eT3DyDjV4RN9r8HXW-9DZYpBihu3mg98hdEIc2TepMF_8YP0RsboBiOG8msdgIeJtNQzSDAzBgV29b1dX4_bIwrv2gH7cpFh73p8uTHVgi7o8Q6ru6vJ0UkHwuR4tTgpDNyhSWmIrzfr2GjSB4BC9PuJIEe4NhzZjRuHFzaLYfe0suyCD2YQJYDprMx_hU9eSeE3z19N7KZM4tCzOZPcjb6ZzichazZLhTNy91g4cF_rkClc9GTZdBp_ZnkBrJy3GHJ2_oWfk6eYno-MkOI-mZHC_3K2VTwNXZ-LkfbmRDCSde7BeGu97aE_ndrJWM3C3cYOHJTtfzbPok-kcRHfLtErolkF7hVijzrKrjhywJ_dWFAP0tsKMCPZljFAYDdEbRGaNfvH9q8QWsoN8ZF5_OLB-TocKe4jizyfZa5Gz9SR7r4DrSfZeYDj9vNZOBfn64NOCGYrtDsfTG4JDitOeCZJ-o1EndpImjPPzMJSRB4yTpOHQmZJ4yTQjC_amtyTODUUcJ-iArY19FqaKXJbpfrXXoDr65sUSVce6R_E2LEoFoOCFyQG21_YD54Z9KqAceJFvV2zOo3y7uEsON9JVIQC1MiZfmsmhze3Ei3y7aImch4PnesV_xi7IFCbEqaPWIsrc2zcoLQe4XBDzZ8CUDbLzs26GWq-Q7VGrFZKfk918Qv--F5YUja8Fq2ly4SIbgZ3U4GYIjGe-c8HGq31SmFjtnga8Fhxu7MpwgIP0Na8RR_qk14iD3rrFOW9jlzUXOPC4CdhQYu-HUVdC1RODU8B0TVQ9EYgv2KVIX5zoA5Yamvd0wzZ5tw_mVLhRV14TrKgsQq2Rx4ae4YTqkIyItr1Fvsq22XKG6D4Q1ouyDJFzMu4AmQFH1lDCchhlJx7aSeDioS_rUN4_14RM0170HEixvQqwcDYqxfYisJnzW_c5XIv2QrgW24vwS4W7DX23ob-OLUa4FFtcCFwPOKOsqmNJ3whBrHkueVs08o0h1Q6bo2DnjqcFO9eIY2EB7AQsK6AYyCysux03sRzILO2YkfW8ktgSVvU8k2JByDlW563nGbf7mY718eQk8YG5xDHmUzFIRDEAtK5AlKI1SRNhaKLLpyiGjgLVIFCd1ZBIndc9Cq1utIh2KPlSEKlNpwsQsdYgZEjVX6rIiixeeDmzc_g0AobQPzqnNBKZTqhELIzEZnUgNqOTfo-Kk_FmnJO1oCoSEWEG2e5-8tGzeHq6ta1j4optCx4qX49dWzl-GvtaDeWjMQfYTjRT6K2YiKUjjolWGyJcfGJ7cS3abmJLK8ptNzF-M5621VjG0-J9J6ZvRAyusWcsPWJQx5kiBnNyr1dGZYGpJtHULj1pFQcL5ypNcbAzJ4JuaDHl9YwlMxLqx2KAtUGQQT75CoSTUIdgo6S4JTX2mI1ND0vsTW9GxcKX0wQYxjMEzopQEra7KBoieWHEfjQspBSmYbxjqJ6PpETfVs8JhU5Ok7SJzJ0QiN8FBpxfeW-EPVyHCnD1vk-IPDkq-OujxIDqKuYpThOL1DhNRJAc6VmPs8T-TlG0vWjep5mFqdKivgGgeB0hmvun0AY479cUFMy6OEXTwXnLLMCWFrnhHpmF6GIKyMT1kqW4UyxdhncgjrRCetwp9lyIhOUgJf4UwHkeG4lkhYCG-xwrAQ0gJ6eIBmh7H9GQe5TpgdM3kyDThSs5fyvGFNEX8QzAoPJFJttP5dfEuOoZLVL8ZdXWaCg0i57sMdTkoljv10UORiyrYEQ4N4VGs9ZdUoSAVdh9ZRNZWfTISj5yITSVkaCRoO1zfsrRL6vo26IFyr6HsSyDXcsq2BU7p4epInHxqFIMiXMW7SEbLO1JgGdZBnjittrEmhY11hSHqUaI4pLr8a5IQo8h5Y0IcaC8kUWQaRGCTCszg7qrGFLErLs3Oy0Qf2J75ZtBp-Uo6LSIQad_cn_4pz_99z-YP7m_-Zf_82__9C9PN-pGTi_zGsJJcxkQbKKOJIb1HqbSA6vHKL9MwPJpQujqNDWc-TQ14uqDC8DoAbC803LQJ635Cr5mjKh9XsV6InQ9IjVSsxFX-QNizAGxL-Mr1xjaCwNexskiUWU-COE5ALgDJJ9jpb6L9EQMPdITMcQ520R6FhoaLyPsIj3LJYa48mJhzrT4UAIRApMrm7h7NVgcNDbx6iewYQ91JDwYGx0J0Q51JERfxny-M2PsEKpeTDWMmhFndI0DYo-qhBlFiotP5gIiOuBIdIxwny6mbPBYw8QKtmi1Yy2a1JBw_EO4LDYwhXTSYhaPyAlNwbO1K1MoKC0eyNXlEkMlsXgOlZwrsGLWRNWgYRJHiwDO7WQRQHB3Y0jlsz7-WgNw_S1GRI3WgAlhisJEjHXEp4g1GhaKNUM0dLH1S1cTzhiGDIg9KA54rrXS_v4NbT4SnK8fdmmRWuq1ckMHGCXNjA64XuhVla-WuBMmFGguC831LWzzpgUhSbJsaYcpYud1nD_OF8bJ60b21cx1H_hm2jo5ac76F8XFCaOtjLOlEc4SYcrXfGYrwmzyZIbw2JS6j4P_uBYbeb1HoSYOtLW_z8d9EWxBKe8BlhGQ8se1MMzLDofftq-cCcWz8R7M2TfV74U-bivMiJvQxw3i7CcoNn0_KrRV2keFAvNxTcPEaIq3FIzyRJVyY5Ba34iIPbsYASS6GAG2czFCw3ImmfL_c_YvLZTsPJQg-ocyN-G3zUcNetBwqfmtCzW50A9o6K5JQ___3hGWbMmW5NjJgTyZsZYf4bAlWZK9g3gLBnu3ktaXGNmjWIk7iMyw2soyAQdn66QTRQp7Be6kp2_OomhsxLcAJD7fApAIWAFI6GZM5ImZ_zoHfwQi-XDSAlsLRW-BxytpM1Kwl1atZ3HRF2L3zihfQei8VLbxskqr5GKa9YLiZYJMR6FVUnx3KL78khqR22OsuTif7zUklmfLWkr1UcCe6qOAPRUGP6eWaoOFT7iWaoP163hB-9HCy6G4lqmEK80r2TfYPTuHB1lSDk9vhf9unZae04J_lcYzeVoaz9JuOdBaWWlSdH7w6Gssv34SijyqfgXl9CFhOu1gsEAxZWmfJZ6sqH0Gb-CSd-TTxFiW0fM8Ntw7b9u3mKhByjYQ8cINxA61vmvne75e13J7F1STl31rxG22sRWP2eCAbIxh5Uy5tmBcOY7OWluYiFmPdn4V8GQzJZI8ZmHbEsm1Qof6Bb0caRbvxMjr72F9BoyLKMmI68kEZLpoHo_YtCpgAo8x5fN6doqL-3hti4HJy3ht64FJwnixVShnRiLLzGls0Z9ygVaiml3U_Wax-U9fB89vcE5HG5wM67ubSgoU1y83z-nTWilDB2SFPxqo961_tbscYRyOjfjgP5iOjG-xNQQ7qIg_scQ3VhGT7KXHqusmjp-Tkg6RtBSp642Jsw65xbc0nsrpF5QkWnBYuTOSXhAlGS7GoyWZoKWETjkecEegEmGQMPq7PVou_GqpUiZ9NJj9k6cK86S-m4cpwxQJ7-bh4N97-OOUWvp0_5Dsee7C1Ek0nL4_37x-8PHypVwkR1GS6TIf7RNp1sUd_BB1yENgUalA4xJ5iLyNAaI9u_lh-SPhh7yxVgw_bY-MAEjOeyiDIWtoZC0gDM8oaGTatRznGAinBhFXY9FIkCJHmSQYiYGhzBMvBIIegYR1lpfg14bkxSzpYWWsOZ4vMRqT4HwVXA8otxzUgHJPFnkiwF9aeF5ueHwevF9eRwj8pApl-eeHjVu_cdzFdlPSWNe5oixw6VnXB9kx-PWF6ACpWdZgUZ-zRQ4tlS0GVWacCr5aOYWFQBKXU5gJVgG2oHvRGfEUZypv4kzlTZypYPYat-LLyHbbr1xBkE92iqyTHYc5ibEjeCzEjo7I2lCfQuVCVZLf6beCdpMPLwugzeTqq0k9Gsjv7bLZhnutRMebhx90aJHjbcWOtxUlz43DUzHDx65y9Kou0SvGpvGwOuNhsKOqdiCoHgJB9U0gCFr6IcJVDxGuakW4us6rh9CdgPNFUs-hO9qSEuFaQamNU4Srvo1wVSvCVQ8RrmpEuKoV4aovI1x1DWXhc5wY8BiCUzXnT3UPcsdX1itEvvh3IXrAUz-9vt0h8q3cf0rDWh7tOu8Qgaa3jVg9RLjqrxGuU4GdeIpw2UQhWFX_JcJVf41wtSXCxR7T4FATIlwMmreOPJ-kkSAZTFh8JETCmhEJa-8iYc2KhDX5PngATxGudopwNTPC1cwIV1MjXE2NcDUlwtW0CFd7EeFqdjyrzUATDcKIpZYQDCkqxWmgeRr-4t9lizYxbmNcM8LV1AjX-hWENtUI1yxrvZkR4ZoTZIlwKSXX4X0X4WpbKAuer3ejgLzG5SpGuGQQIlwyCC7NxiJc23M1MtUOka92iHy1Q2Sr2ZGtdohstUNkq-kxK4pPT64YOmpixErAD5Gq9jJC1t5FtNoeqbJoRWZB8GgfSb-CSjRrnyI7KEez9smxgXI0S5h1G7iEup4z84Bt0azvUKjhrNuU0OJZX0wNaGGdS0QLq1tCWljTEtPCSsyg1vcf56jW9x96WGsDmUokXTYCW_gGx8gWEkW7Y4yoEtsauBncGqxNJw9ECm_hMOzxLY4w5xuOvh7hojNIcbaMCajGuOiLs_lOxp2J_PFck92DoMW5BkENYTEWEaJrPAya20NYgrwiPOXHqR43xD1i4c9PUahR5F1k7Lbb3ec5ufImaLWUOUbT-iYKR2_dXOGEWD0mYxoolwKMibS5VFDOOCUsNJBKhZWz4lMDzok-U0NpiC2xNPLY8FrSinUK6Q-uLMdiW33Kjiu997Ik4MXKL4GwUQcPe30fZ5g0r8NSd1WP9_tVUIoVeRdbm506xtZwFjk55jUAJeh1W7FG1GvAjU4YrwWEBtTWCePVUNlenaAvvRZJGdDmXeaVa5GmpX_K7x998S3Y9n1mh6cGQYhPLdWqAbvBIBG7Ua8Q1vpih7jWYKiBrdGmFNlaumBSTjHOQZGCnD0U9qV45WS5fFP-3bOXR6d6QOxb4FVE7ObZITFknGJi9ztJQbHv8wqS6FVQbPLfB8XcFcSo2Pe5GBZDftip5ADX95EYMBrPnfS7Il90j-Tgqg7vQm3Yv2Osbfb6FGyjQ6FE22gX9XAb7Z8ab0OWNBKFDqEBaaIvyOGwZfRH4A0mVXhMrpcxL6S_j2B9S4DB9S5KRvivI173yDwv8S7Sh_QfwmNj7Hd9G8wQ2JgQSgQN8WlG4ueKYozs-1wMkiE_po1KwmTfR2acDHE1YoWEN5EybP9VqAzJVsvHYNmXY0fLRMKyiuLLo25jKHikifdEjqbtKPNjj1c1I2X0e60RLT6mcrxrlNUDXsvI7yE3PmZq_A_3wWM-LPvj-broMI1sHNStVFziXhBV-gJvw0opvuD_Z0SSvszXoaStyNuQ1fIKL4Nz5xJEWCQxvoXPCzX-kxrhQmwejYLpm9SIFmKKaym9immRdoW41Rc1olpf9BDWmoOwM3gdSjScjq64upMcphqA4BNLZqCKvbcQMqMfcfYGBFUy41tkNFmASynHQzCssBriwuEkMS7yldaA08JunG2FucYAJWkUDsEo-s0zu5hpH3epz1qUTG15G8lDNGtpaY-6obAd320RwrOLPOD1RXz8hL6BkRIUMrC-FiSySJpDz1-4-58nPPIXhrRLLRzvjhwCNX2F7_E2TBCKasVi629uzDzQpY6eL_pEky0bYY5shDmyEebIcpgjy2GOLIc58pswR34T5shWmCObYY78KsyR34Y5shXmyIcwR34V5shqmCNLYQ4-2voNoDjUEoOPsxQqyXuohI-wHAiJlQ_vIkBmrQDAZiv_HgbI_-DXz7-FDvKvYYCshAGyEgbIhzBAVsMAfXkWw0tfZC99eeWlL7qXHidX2c6QjGdh3Y8WNVxRtmMs4xnz_5e01gxvaYc2ynbQxny2hgHKP4QBylsvPfJ_DxuUd2EDHISqu-ar4ppfiwjfv-ru92q73-ubgx5f2smVXg9HPQZDdIrT22Rllze9T1ZhWBfOwQjboZS6nSAiz4SFWQ8xiSrEJOoxoFCFgEI9O_zr2Ztfz958GMS0XZgIje7ufHwh455YdOTXl478enTkd4YvbfdfSP78uvrzfS3kdZ-tyW38kWdgV4deW6nPiuorvuHPGrnwh06fJh6UGM_38xBvoN053GV1M456DJTJA9gcNHp3svf0Wduf8VtlFywZWGWYI9Az2r6RJ5g99Kf8uf7U25Rt3_--WuCZLqBjaO-fI_SzQLgLpKXAnctKGr1GCXc38TTglyLuomP7nHWGIt-JVp_3_ZZqbilWYqTF8stitdExeA7bQrH7ndxX_9xdrLTUvSb2osnPot-Nyn0l091i3Eo-P2O_F0-kw-kuft3jeYnFw168juI1PR13z8dzYvlHPOJM6Wdr0Bfl6eryLZKdN0KZQ1TkIqWpFA8CCxoYt4c-Yo3i-YA3GW84e8JYONefMJbmgEd4YcJUCyCt_UKrJ1p-Rwu_0LS-3W5e2qo_8uhvGYm8TF82wFlKiQn7-DZ-Uu_6E4fJEjbHRBt7gTc8p_GSW1jDu01nOuRzRVa0QeZY-25vHs9D_NqeT8ztmbNUnD9HSTNdLHeuwA3QJz2EtC8N129aFRF_WldQrU2pp6UHDTm3LCmoXXhelefeXKLYWx2vGl47fkG68IeYm_5Rzom-SFt8gM-WnTLgd97EIEulr56JS6GLR0SwjbtEXwf4eh5sKKy5G2Y1BkpKk9Q0TpU441Ur6Qa6JSg0fstuGDAUBbcvffT8SsxjluIQOFg2YGv6JQ_kYj2Btb_LERyxjTAq8GRIVPE2ZteJVlltKqFKHc6UYQk0ZPqz6LvoIBBZQ33DEtx3qd0GvpLEUgUjLgRJsiW3MlAqPs8d7DSeKd_30vgsPBuw7ocePH4IY7yPDPcDHBqKEss5TSL2ciJSVMQdRWSv1qaUoxTtDe1SEV52f16U586WktBbHS8KTsee2PbRo3HPbMth3GOJadz7eFv3WSgB1j0UIdZ9vm3Q7-jtRcC6xyLTuvfBTTO9rBsPsO6xWH5ZrLIZSKz7r3K_LfRLNLG3gsS2v6362zCQSy62PRZPS3ddvot7L5YPe_lp3JdnZxHvt63y1iIS4e9msHG9VKlQRhgpCqti7LOzkC0pPBqVEo3oRvTxmi4K1HId_Oumlnt6JJKiSEJN5R4_gq_TLeK7tdd1FXgeGtEBKIAeoU2fF_Jc0mUOw-6KcnEjCqkR4olQ3hF2_bU0YRCcQhh6Ddp4oQGhsTdMd2CCzf1t3D13FXtXP96hzT3QetvjKX9KzGUY5JXqh2mQ98ceL3Rfn7Rp3uEK8ZiGRp9l1JVbbJ8WamBAjad5PF2WEq4cH2jFtMU2VtmorW_LoQ3eXYiOGH2Dn4qU04pYb0Zd2wDwtAf2kuOXKAFFAdCrLLIA8GGuJQ9DKzGcwEC93N9O0vu9YyLSVCSoyHU0EXw4U46eEuicSOGDSrcQwj7DU5-Bss_onOAkjvP4gcePJIzzifmRJU6CH630tevu4Fz96sNbMoNW9PffLmKowFLbDCBYbvvzZvF1g8grniHeD9VzhP1Uxam_5jwW8fwGN-pvMj6Ukkd3SeyRMZmUYSmapGuuV_1VDdXiLzI39Ze1amgKYXldzN-wlQ40l60Nmqej3IQt09h_efpL4nHWRZmXxFy3RwI8t3tdRQXcoM0nM3Jc7gW2qq6AWZyKlup4NvRRb7Ns6ijsPoPAfAa8C090BLooKyXglZmEjHRZNYUq1EukZu-4JLiD6ngLquMtnB1v4ex4C7rjDSV5bwg14CKdg5_LXlaPnfFXpmQ6cA0ofJiX2ioqeU4V2qUuFpH51R55-XCPelg_86NKlmrr2w4MxSUwUbYHxYUZFBdmOLgwg-LCxK1IuHArEkmU9i_bjxBhFxTX2JB1gZ4zUEk05V8Rq4F7zwRCekfQRXvwCmFI43B2lyEzHZkXlVlDbs_PQoUypcIM30R8pA1LnOTWBh3bmYTWHvd0S5_r9pL1rUnoG_fYnVqZCMuoZgxTXNqKo1iMjtRAay7b9qE_WrcPcVyGoUlqYBjbh1EXplWwVU6rckTorxwQmlH1_UXV9xdV3188-_7i2fcXdd8fitloOTri6ujYBVpc3Ry7JIuKGzEqbsR4cCPGgxsxKm5ElHRxehk0p8v1oT6XePKtxHgiHJwv8Mr-RNBFWDx5Z-LJt4LDdiIYTRjeGdYJI4BLZ6W_ztGJSG9H2s1VARaiEwtLtI49ZU6ZvccxqICjwt1mDrmth0YivSBKDRpTWpfyya3tQNykP0-wibuGuZ6IJ4k9IV6nSsp2Kbw_ZwI7oUNl0RmoEdJFcPoc96CLpkhtPJelempKH8Q0I3yVNH1IvGeSdylRvxPf0qQwurfU1tZ2YglLbVSZhaXnwgmTsLbYRJykKYtQOVZedm2LUTWcRUpUTYYxqiajqMWS6ilLqtcrqT60pPrQ0tlTls6esnT2lCXdUzacV7Di7gxDdF5185v29R4x5ry6TTZwXv0NM6bTvVeo8hPx-mwqHyvWNX5nmHu3lGkla_l8NCqAIjtY6UscXImJOn0UV2LnyK5E_j60HqmxbFXEOFHioPmSgmwWJcWJmA5OwqSlp7WlS-8yJrHA-4xJFOE_ZExikR8zJrHYjxmTWOwfMiah6L9mTGLxf8yYxOI_Z0wOgyxRDy0DMIBh7MzT0eGZhi9fdXjiLO0Cew2A5c2XmMmJcK5y88GLmI9exKx4EfP072xmRh4uJZIZ7WlXBZMi-xVBRTur25Xw7MZ24gs0S1a9iVn1JmbVm5jP3sRseRM97XXDM_XyPjRbDsV8dCjms0Mxb7lpghDvJFuIA8cU4lnx3mXFe5dV7x3Mevh5eN_8Z05wuqvNzN823HeX6L0bZGVl50PiW67za4kLPyeVkOmsg0MeNJMha_6-seebv-Qi7vkEWNjzLaxxeGbfd-URvXjJvCwmXRQzn1jdHea6dFPe9JE-9s1dnxSFesnoBq2MTeJtcbf5nJxF59K1jBNhTwlNyALNDynb2busLXGvjzbnGExbcKQFBEcxesxe3GeWwhgU2c73L7WOo_dEJlNozUsKW5eaJEOL6jksquewqJ7Dovv8Ihv6Ttm2H3n5Ohj96B_x2YSgRC3UY6RJ1E6yJWoZd_7qNjj06EojchLaVk-cq8qzeSrx4kueU3kovYuSQFk2DyesMrxlGqS5ELovxC8rSeDi5iuIOLmbWcdP5Q2XZnFyA0PC9xoaSnguyONWh-R2S26h8bBFca2nW13tk78Snsctat-EFnLuB5Fu234_xio-oMxVbCHXad9XO8g4aIhVR1u7GEwbuGYDWx9JBpgc-egM66jA6GBmddFWGkF4NKRDWpZ-HRvzvWCHMi-Igquqbp6qunmq6uapqpunnt089ezmqWc3Tz0nRPWG-jlyRaZ1SiwnWQXTwuBl2nH7rEelZ8oUoV03p4gQRccVIOoRFJNVSaeqSjpVVTwh9eAJqUq61MAVTwnKxHpIl6qHdKt6SKeq7Q2uy_Sa3-BG_07pWhV99HJMP5NaZLnP5qBE4UNpBKNqOBH4eUeVYHgv6nC6n9O16oujhBcdZS1iJcDC7mVhGXsSOqm2zAG_zUuTMs8rKpq6khgD1dT1ip_nF65K_oSAitpdXZK0Z_PS2vOIPIGzPMjq-_eLXj7YB6ON7AhNIwLjRSpxS6wu2kolCFdsAC2pxBgawZdRQiMyjKERGR19ApcX7cv0ctHXwv0m3uEGOqqpDqOmOoya6jBqZ4dRO6eftXP6WSP-h81b1IhzQ_YWdYbpLWrbaUdBTTbjuCPnyPsfVDxNOcbaFC9SU4-b0vpUl38v_stBKijxy0Eq-ES_HKSCIr8epIJivx6kgmK_H6TqBf_5IBUU_-eDVFD-14NU6Cdsw5dqH45txOcqqc92cBE24tAVtWtbc_8YiJ54I_zQNEdhY_PJ0vDNPsA7efXAA2Xb-OlVQy2PFzx5CqHKqek1TyEdcENFky46egIJpPoSgPFXmftA4kD0VxyaB5QRUNWsQMCNrECodMv1gOeGgkfGWcFDLwQF_7UsiI7cIz2Axy1mQwHhuZA0AZoOCIImhfcREc3zB7VZqhWHyaQccwahFjm8D6AV3keKEd7HXpjqF0mW-sVJZ54U8ld-LvP0fWuabu323CdwOQqHJzXQz5NCbk-2cKlFF5bzQvBlNj2Oi2t_bmdA4mfUMhxH-fQywxE7oklSWqFC0LILcYOEDOs8CzLdidlP93uHzgWySB3cjNIz9h0RER2pGXxq0e2lWlMFTGd8mzxlE3tH08XYPgGhIDrAAJVWttMcYN5pDjDosIgcHWDQoE05OsBwzMQ8p8sv33DZHKVswt3IVGBcJk72K2Gp_bnsb8LZsT-3_Uk4ijpuH__D8VOXuWM5GlrIF8nsZ0nk8x27YHC2S2t2wcRVlxb0ysBtlxh-mkP9uksLXhAaoFII1pzhxsLFZ1KaRuEjbIhWd_B0kS9rE1RXGE60I0G3pLGXb3xlWNv5MA32_A3zOqoXLjPIcUnqjpHg3f-2ssSM8UhfwVtMT4dQOeTj6dC9qY1-U-k4EO0Z89Z5V2r__Z2UPi6Mu8M66GE_MhUse37_zFS57aJx-1R333nfnXy77oVfJ0ONvVrfaLdD_Yy2w6jB9_pb2_Vxh0Cpr-lb0OOyNMirJSjfE2ywlJMtvLu8UfCamw5qEBHNTQe1marbH910UL-5UegNFTdiUrfhxu3zTomMIu0Z_HCRabxMehXNJuupKsr769qxTfA_N2uzAqQ0SdpL3gpfD6mRhYAhtdgtk0RMBy97DvG7qiaHVzyLqAj84dgnEMwTndgJXd146lcVCSfPDjIMz06mTZ3jNjhtlLiNBAt6Y2HpDiJ8Af-CSYdT0RtsOAwvEnawqc0mt1ZITxP50HeS30Io592lIZU-v1aFEsZRq-UiQyihCeYwzjlpezlgvHAWBe54oq0U8v7LDjDsWXco8Ce0K4NZ5XakqUeSxhCSVc_moIJDLEktji_rpGHlQ7GiqAyC6skKqicrqJ6scPZkhbMnK5w9WZ1iaoswfDS2tui8F9oi0JOamrYI1J-iaIvOUV1b0FpwQwUzZdFDDY4K_6C4qYJ8gBcHWFUmQXVjwQpvZdyKihCdqe8PpWCB94dS8NP_cCgFi_x4KAWL_XgoBYv9w6EUKPqvh1Kw-D8eSsHiPx9KQeUe7NRHnI76prtXEEhu-YQig6TaT4eYx7R_s9UNR18manF4azMfncrp8xlkJOrHmpNbm-YbwfD9_73w_cf3g4hPvIgufhIvwo1ezKhleMQoBqpA5mPiLqVZmj5CHtG2YewltK3VYGUQ7WNnJav43q2oGBO9rbju8ngj5J1Grc3SpZHmSPIKl3O-xGoBaM2eBONhGxS-Hd3fQ0xWX0dNI5B2ZCd4PCWp4uuck1RxQu1JqjDKgPS8_AXKUvCNfgRy1TmUyZQD7nUerhz8syM9nh3p8exIj8dMUqCYlkwcbmzbkum8F5ZMfJFKiiQrlRQ5irWT6WCbhw2AZB42QI542GCYTUMqCHvsScJFbmzEo52TihNEtZFiUW6OH-oooove2HHHkw85ag7ehaBfkYSMZSdNd9CwkIOTd9ACLOygF5Z6XQaKoOslsRg8qilo_1ZFSpp0f6LeBldco5b-xdNI-OD6MzGPIhF1CV1eiquRsnwmZxaYgEdp25vR9GHyC_pXgE-qOQ0fg6YcoLMvlAOMVt6VQ0oE4ao00Uv8NlW6vYSsKrehELVMUj236eyFTWcvbDp7YRO9Z0yTnIneMKZIzkSzpWyOeSpgzjFDcCY7xxHHRhWcyW2bS5Rl6eReTJp7cSHoHsykuTiHuExnxyMy65EJUiZtPshd2sHcVzhsUc2Nw3o8tfa6Mp4JHUIqu7noePynczM5yeQz8fAtK7RD6xkjXPTZkYLMss40B21rit4DCo_wHlBcT1l1V2XBXUW7o1mJ-ezMymdnVj47szJJ4tnSsjK4p8a63JcuUKy0rDx8q4YoyY7MfUVMZLrLlrOicXpsbqds3_-GQ6VKhuxVkyplPq_XjH1nwmDmyDDmYeThPTjkZg0DL1N_CJExOVrPDelFumASdPGWtdvmZqeHo_vsJIHaXsRgMjswuhqQOyrYj5xkBO5h2Gcur2YX5tXNIh7RpzTuZck19F_q8vHjwf8zZWyBm7eGiC1Btg9LGyKOPpACKAWzZhYpWSBbhkjJ_oifJoHyUlikXOq2uzBGOZt2JbC69n5mQTUUdBY8TXpO354Nt5NgpRbMDFIM6o5L6cy8m0uwB2RIEZL96GvnjWuqh3L2M5Szn6Gc_QyFJr5oGqDQTBZFA5R5F9-JY2qJoiT5lcPGuxyS74qa3Aery2_25dAfuGZl9aGgIOIUFMRqOV_LhCOiCu9CPSkGQRf_5SLfVyScMs5KPvXhmHFW7B-0nLwLZ-xZE5VTvhnK7FJqVw6fPBzjLvjK-g7LyK3avcvS_rkrHLOZchx-mPpKxHiusB_ezj6O8pqU6rghpOo8GajI4Urv1dqapocP4dG4qxnC1thH-QCkAmPQWimMfaOHMnnvKhlGZPuVzd-RIigTez3UKVyTZe7X87a-nrf19bytr-SKpc3cryRPR7b2O8M6A4mdsCPVle6VFTneOeYdizhuh8YM7wLnHE58wKyi12t26eOWSefZb8P4tyc-qnzPF3565WQm9v39yUws8cPJTCjyy8lMLPLjyUws9uPJTCz288lMKPivJzOx-L-ezMTyP5_MvJjE0jVepbeu0YKaP2voNWDoYWjUa7U9x_Sz__hrXKgDag2E-x7xbX1n46lvpxU00W_3nic6ApCE93dFvxWT7u_ixSEMah7EaTQRjLuXGs0A2_YQbez8Fo3XIgWkzj0E6YfTULA3NVGqqZ6npiZKtbNvqZ19S-3sW2pk17wpm-bmvJSVTSMuDPFHAnBoN4HZlCNy7ZB71A5OoqbkJqGbpg0nyum4TKHnZZqSmzLww0G7VsgSl_Ao40NG9Ar2O2CGKIBptl-8iEOiG-04ZofeWQTN8zT7zz1LqieoDd-x6AoSYMEXtLN2N1AbNyUewojwac_-okbv1rMuf8Qqj2e_2VeXakxubdmRnwYgxd086RGusXFCKQ3Px90z-BwUASA-k21KI1X5QO9i6zI1XMPL89SmSFakJXqv5G2OrbsV7DWrj8INvxKDR28yg2kH8hTti9aCzklZOwhlUdPguOyJQNDcuIxSetHGGFtXfdNVJEh-aF_QLNBvERGOnNIe5a1-6zJKHCD7emD8qiHNnJRVDSHnYhxBGwGxGLxM3qeYbZLziWaT5JziuUm6edl3QUhSrstknCJz6KDaKTVAMlNqkGN36LJHCoPDZAGP6LAfG7TJauRjMxZJbF5mhto2GAUoZFZjA6UCeQ6S0Ku3Ml90-e_qmOOqvoeWT7hqTwyxqelbqKBoV7qxWi7Z7RhgML42xRPGuO5fEXO4eRhwDJ_WLY-vvll9ZthEFxSiw6zzXEKltASYAaFXZQY37joxdUunNVW1jOpg_7W1YBw2Qcb5sAm8APNB8TqaeOMmolWCcEt5aNTtGZQ4eGnrx4iYe9KG4g4D1NQAjrjDHP0RZkRMnxCSLJ8Qzg1RiPGxuhtLf9a8JwTp8KNwcGqiCszYdsjgGzWYHupu9-JXUe4-l-Dd7l1ZogUa6fQwfzc20ql2PtmGzMti0qll2t6UeLJ_6dtQ-5f2iB5mCx52c8M5Hzzsg4nc8ZCq4jaJ1JHWZukyStMHNPkd16XHPTVOxO5LH12SfekKDL50BcWl2puk5pvXXBLQcRE5uiRwxEzK0SUBDcnpLgBa6S5IkdwWjnW0f21XyKMofLAl8wgCzaOA8GjVR_zb410KuIFhHZhiSurFWpSPu3-Kbv2BKsXnWJn0GeSsIwqqFib9kAoJpayX_UPYR8WhDvD7g2RY4P1BMvyaPxwkwyI_HiTDYj8eJMNi_3CQDIr-60EyLP6PB8mw-L8eJBviTjOOce4I1vFFV2JR9Q9ar1iTGfFlM3a9aC-EJc8nhLY-GFkVLPEHiEyJgAIK11BA41FWHkkKrJenCMrnoDoOguo4CNpdVfAGInJMfcFhMSnH1BccRPk3-WhfupdshfIOgbwOM41oqRYJWSOATA9hEER1EZqMoxgfPRCgfIZOta69Yu_N9JT68iqLjYCiiPgwqCQ2n7oKVD_kjvNRURrZSUol15tKLrkS1JqB7Ow3vRvaETwo5bClXglKOci5VzAA2nM1KIPvrN0-gu2pAj0cvCFY_6EC47encNiVW0AkWNh_Laz5svtmKQz3-GlbFdYrqQ5M42qR5NYaHffWwIcwvDW-9nkUSX4TytqonYn52mkqot1mBLWZwj0eE26gfpGCPoDeUDw4kzorHJ1J0VMh8KJKEvJOQr24hqKX116UbwSCF9ee62s12jcJYac3PIRA263o94cte70nUqVLCmfQcgM-7IdlFDe8MjocL9Gz5T7NH4Y7EMGSX2Z5m691vL9JcGykv_XMYA9D6jXHgtUdVpTXz3lYP-N4nHVwlggUg87A36SZH29-GZQCMKUMKbBQTymRDr54fl6kXGOy387J-Hy_sPMCDgyh4LJOWrpFSKpvoyN1_3U0dFim7vygT9yrIlUOcvHOmmIqWa4T2pQWUFsqclIcD_rhdp8ua4GVxWXfy-7iJCn7d-iHV4CiiZdk53fgO6he2ZT3u2cWqK5BnUGAA0R1-_W2leFd9_qCgpVa6RQN3AJLaDgnPAJHn810hs3Q7t_MxC59j5LikeE0RsyNMvopDHw2ssRW1ccJ4VhtEH6AMVfa-efEyf64_47ktpqaY9U_JcczKAlj6S6KFQOr821HsEhiBIMBL8O7LHXBpLgz5SJdGQMrUbJAQW0Cy9DQN7CQLYY7aaSlLoPZXKTDGNsza8BEj7ej7__7P_7b__Hf_z__5___2RQW2oO-TfZ5R8guzWeIP3cXoePlRyJxmInEDRxhX3l6-_O-2qxsRWdqyawijipGDX6WZuv2azmmRQ4ujHCRY9Lxok3DVKZAMQE-q7YKd7ixrqQHiPc3cd9v8h90c-WR3nGbvmF9DmlKJMMIXMB5JCe8zKnGEj_kVEORX3KqsciPOdVY7Mecaiz2c041FPzXnGos_q851Vj-15xqtKxhcnzFQaKWNUcrMzRZ3DeDI4XsxvojPOLCeFK6Fi9mJ2V18ypnNC_RHMhJNVZynNFlsNY8rQu8sREuxuf1dfOCd8ZvPZmm5Ih887bRShGrWLyz_5m-toz-VFzPHHiccGRFCy6_PH1kN3FHKlahlXXQukqIEgFykHYGKocsdY-YiqgkPNnNZF9gNyPvdgAvOJUpJcB8Ts-13YF5lLpu67T6jtbe0O5TL32SlE-_Ji5-dVjp-VjfV308PhdUBblBNX5y8UnmfCdlfGTFx_kaJgeGvqB3ahbwz48K-vRJ92HpIz_eA_g3fFq-vFE_eL8y2OD3e6VxTOhxf1LFswVyynDsjBlPAZY-VBIRGzxxFKAmJ44Wz-TNhmxnu6E-9aAg4MLJBVztpZmSplRJ0mAhS0SUuosI9MAUeiZmB5acmVVMoMOTfhIt06WQTC6b05PPDpz8ghNecK4zp5wpUaSAg222JPusVZZQiZpwr7IeGDYUpfbZcqd5x1pILbkSQ1GnPYIJXYM6rUHwttNK6L92EN3nGkmkVBwCr7kngdGl9mkhSDzo3uOW8vGZ8H3TMH6NhZD6ViX3ZQHG9OA9_57z_xpi9S91R2gc5m6gpHKup7yoJp6riS-qcS_e6k01LZzrIRy1Iu_PFVGOXlF6UVF6U1F9UVEVK-ryt84MJvqAXbcKCqLSo3dcF1V65m7TRTWSgkznVMcQQesA43yaD5j0Bid4FPjlJNCduCnAShO0pK6UUduiATEmWmm-iJaO2kl2vnwdWUl6cn4lyStbelsHzfQ2oEjpbegDrrq_Gd2oVXGjjir0PL1RhXJSj_dC8iXXxZfMGxVLLKcFMa5bSa5goM_t43ij70mxMHj9Om-oY1yPhsau2yFTlWMaEHUcXDtyTIOGSgzNhIB6qjO1v8ASKun3zp4qoayuV0G-PUGwFBfVK8Wk6tUePe0fjkaGiXU9Or2dtlZnXnvFw81V_dr_rX-iz-XGJuevK3Nf0fDM2vqk7UK-0ZubuJAHSD4TDVUKF22EFhgiSFZgGEcgeF30ShF4tPyeOXRnu5MEXqKoQr7j2RDybbuvWBDyzbywmHLMSy-AE2Uh30g2iyzjO0NUAygrm55Ah0KzKVk3owo9wW5UodxiPKrQs-tGFcp9TfxFJKHfliwk3m-xRFBKbHmIvHe7YmmHjKSmnL_iuH6-q9m_f4j9OuBnBdbWy5Q0XviJd273snljv98ueSAWwvIDhJoapgJS03uNXDJ34Jh6eIhnQ3_iUrf1p8AiKuqOyvWpMFRJrkQ16XgzcXSftTtS-ki07365eaqKNI9bc-kDW5RPJjtCs8j1eVRjip8nDflFkSt9-qB-t9ypLToyofi4_gzPw_DkPSMTr8o3SfCAbpJ6LCleCQ33d8cJsMD74wRQ4pfjBFjkx-MEWOzH4wRY7B-OE0DRfz1OgMX_8TgBFv_5OAHYHDgr9n0u1Gzsc5Fx3ufi8Kat8LLPhe7MfS7YFNhNc5cKJHOXihxrlwoceZcKoLVLRYqxS42Xnt8J2hkpmu2Ao29WoaQz8l4IpgEgmzEBNYolllTMhuvYz8EUlDmWO-BHpTuG66R0kahq-4UYlVPkV2RiVteVOJ2svS9yLJ2LHEt3I8eyAZgiUHQ31mPvfSWWUIl_VYlfDQCcV0-AK3XH81_F8xzh1gdXZc8zmBPxcuEPDU0pZsULXnvFgx3vl5e7O_7bwRymAVCmxv9y2hNyrPVTU4syx4fH9kjtc9256EhRrIh43-XwfN72CbcaOxf4CuS7E3-vT8w-5KgXSQ5fDV6MWyEIh9qtJfxhBIDHyHzx0vHPrW6HnVPZe3fLu1yfhr9MI3DcY2SF66tTYqQsa3ge6-_OFHL1XZE7nBu-m21PvO9CASYWSTwVVaJbcqCIanTUfzzQUZAcr5b1MjDOrglgUtcEPMo8FQS6pPqZAZf8zKg53NEvixRVXTk9T3hUoWTQjiqOrl2k6FVorl1AdqXptqxs3l2hhOYMBmRXs446gwX16Oxjo1jvAVf37Dhqpz30GN0jL77kHZ3UkVao77WxRXuv3cOmX-FVn7SOO7xaIIWShU0h-PutFPJI0i3eiAqksV-dRUO_OotGfqNL5dOvu_w2EvLeNVSZ96_a3wMBmhUuUKOqtQLvFpE1r-p3dyjfbf8xdusG3kwcYoM4ia3QqMDZA5H4ra3QqMDRK7JCowJHr8gKjQoctSIzuC5w1Iqs4PpOUauxgus7RX-tF28lVtPnn8ds2_UJu3mpi2APfql-TUV_FMija2P5WrZnBYQLPiPXfEPbwsXdgJRtF-wvImS1XbDfLkDbd6jevP-MNia78XHcjE1wZ4j75LEToaOv7VY8ycc_cMzd0_j2OwcF7PhI4jU0Cgyn8o6FL4StvZDAuicWGGEw_9xT-3pOADugob3sd4qOhE2tBpETWD1eMkx9ILOWKk2c1WE4gfijLvBF9jONFSwY5YJ0_J_28C-9z4a8FbEwKv2w0yWVMyxzuIzw-YKtgYhIfYOWw2cbh8GJGTh_1tM9rV17mzgNCtgUS8PeaJgSYndb575FZgcd2AQmT9r2dfigYKYijMw6CD7nj3gdAH9NWsvWa72qtefB4VU__Av1LM0kDCFF2CsHt1-lwCZKYmMCpmgvW_COR42Qqk7IrwjZHZo4E-KpiSOhHAjlOhHCoZNHgtWHpvRhmPsgC6ANhuTTdwy08g2wuhVOgxuU74dXXUcPPwE5KhnGw8YoabGWtLrGOAlMNp5tvjJdL5GdwORQ1qGgQ5cKhaJDTOh_VT215PrBAg40DQC5VMgOUCgpEGAj5O8zb9N2kndDB1I7k5j8-7uL7r9cQ161i93w-KTuRGL2tOJT6sFBGynnuLDow1HhtBygUZTFwaPSksBkgVUAUZAv1VIkqUhdERi0MEIV6_wKSUXqhvBXHkpG7T1nrN3ZdREfNbN-gbG-lFF_PdYvMIgw6y9gCNJe3iKkVzUYGjOkE8G_IhgqNaQTob4j6GoBmjAI_hXBUKkw1EeCrtbhaxqEdFCP8BYnwrEPVg319Jp-1XT8_dup6p0QC5Wwt9qLcCUlfR4KGrjLCcriaOkLS-M-ndW6buLXqiWHwawY7whGBpNxKa2ufQ57E47NhrMJ30LKgqMGY27G-ki0WMLIHdEgyWIJI7dFgUSLJYykmx3yJ0is0Ovd8Hrnvf7Kfh8oOLaJ-vDq7mUK1BWAH4iMIbt-cKnVT2mNe7AglgGulAP1WTrgOX9Ra2j1TB2OqWRxsQM3v78cOJeQ7z3jtxM9_UavNh2MU_zcm9WKk3gHqgb4gyEMFoZBqLalHNoThUBvpWIqn1jtFQtavF61eGC1Fyy4lyN-l3IP3Pj8yY34p2_B9ud___8191_Df_2_nmoh5gKfsD6_0fTUu8dlcOg9kOb4Di5byX91w1aG6g5BxmDswcJ6XW-vnxCKHO-fEMocL6CQ2nlzA8XP5TD_bi_38g4KqcVfLqFQe_z2FgqhgnfXUJRYaPn7R4nifeLkUQMdeUKL8co0t4F90ysRyybSLAT6KO6P-iYXvJBxxHS3zUt0wq4RHZQxoQcWfKgXq47scJZq_jM9k2sdkfCl5RTjhqCRNsZktcxwYMQACw6RAToO8pfatm38Ey32xzJy3R-bxBGhGK-T3tJOPyq9pZ09r_Lz6erlQziuuJUGg19NKzLqiYF2qPArR2TY0mZq4xahj7yxn-2z5EzQ9y_QhEEorwjGHgpq0Hdh0EmD4F4RrD7EdwR9RwxNHAn6tr0TDNdB78ORYLl7YSgr_LQhc1TDKM4fAZc2nSAmMsszY0K2qVCiYT4-z2eFilxqIz9NgpNf4KVhdtEIL5mWklwZwFZwVxI5zbuQNOkMy3t9rO58AdZ2vtDyvvNl-oZuXndltHX7luOwpYqhB8fCE_SDC3QKoV2MFuOMoS15TWuNlNo_wloZbbPHH8Z4O9yjLm_N8Hs3an0PZ-7rYeWp38XZ38Up34XBu0cCvwx9we35_mLUKrl6wGU6W_rYDc2s4Hwug1pUZ9RUm9wakly5fLx3Nwvq_QYTA-_26LNiqH4BnhOaVxEChqiFCiaIxTE5A63NIOdXyHDjpu6CjqqfDLdnzMWqRXhULaNMVTZDh8QTwb2qoZ6UTDspOotQ3hFO2tgiuFc1dA_NqivB14ALcHNCgCwVAKd4J3hJi_CcAAgk60WA42XDYYHBUxH9iyjigdTOJPAnTLMPMtW5SmbhxT5uotsgxh3iGvzvsi3BJJz0uG_-kgQ-tEkQf646c83GvTvg4YCnA37onz_0Lxz6Fw79C4f-hUP_wqF_8dC_uPYPbnWN_ebcgoc87-tc07jO9fFnhE_OLjw-oetrFjMzLZHczPFs_qTEZkYknj0pMcYvPewHJbpkT3hien8mXc4M6yHhGWpiFI-a1l0hrU6ylkdlPPACQg-GVN-rzTHXCOEd4diEviXtBGMrlk5pMVCDoF8i7QIRQAkz-XZuGM4UGZLiKumyEUnQpT13EZH9B4gYInY7693OdkM8R5L1ToauE5T0CkUonyCvV8ghsGPHwqfPLjpCG8AlwV5XW2Dc8iTXvVbOB3qrPd1AIcVfi3XLXY4Jbo4dd6ETw17Elh5g9XzDtfYPNgQwhnCTxvpEnGFNn2FNkLMMkgKEAEmR1NT0z970ydL2iTnSk2BROZ55RJ5u8noWbQppTW4iRPpdnwmULm9-_PyG1AwSV2zTKyMtj4VCJxrr7ILlBRsTHQlNIfAXCKtbvdL6A3PR964GRzZ1y5j0fO2L51Gnksl-UBgB2hQbFdYU2WHuNW6jhp3gwxGETLHUSPNUdCZd0slQ1qF2gnSBK0NBhy4BYsoZ95ebWjc2nr0vZ0I6WA4W4ToR2qkP7bC_7p20COFEuN4R9P01NGEQ8onQXhGk_TVvw2S0wyYeunkyFS3CpRCGVxwNQMlRgEY5vKtHr7pIym9IvTtN5KB-L92pF2L5NJIqQFNeNA7NdbHqwSQXmbNkt6iNbZkng-kIc5pRKt_XH_npwAd_SQplteWigOcDHlQcVMjuKQINISFZRUBNq84j2JAYhHwiNIUQULu2R6M704d0YrVXLPAjYZfOeSl9fJhjqY-I6FhKWYeEbHdupMomJez67jl4_5gBMyxXTDAvCzUeeDWr2arVMy3QoNa12Lq8HcdeJcTRCE2BG6xssfrnyz1ZKA51j-4fDqADX0atYn-NcrvTCXHNKYa45rQCXHV6Ia45zRA_9E912iF-6J_qNET80D_VaYn4oX-q0xRw1ema76Qiw2mbHzFh4M52-mZXDng64If-uUP_rkP_rkP_rkP_rkP_Lrl_IN4y3lG1Cb6cVKTaiOQ86LWJiLcR0eGQkw7VEyS5KaBCEfInSHJuQIUiVE-Q5BKBCkXIHyDRkdIrlKF6gqRNYE465E-Q5E3MSYfqDqFHoNd4cKX0ug-ulDyOHmwxAvAz5JZ54Fo8pyzV85_pJ-AdFvwqvLMsNu7NbmzvEmISeh8gCZEPToAj4nxcN-cNe0fM-hP6ho3wtw7SCTvS_uI4yeNMmzJbZKjqkFe9JDnpUD1B-gKRIa87UDpmuCB6vRahviIYHobehEXw7wi6AwGaMAj1HUHf_EMTBsG_IijOAdrIQxHS6sbOHzor7_yR4y0XArgHcqrPhU2x1k_rd_SRsybUSWAzqavgXCc6DCzm4jY4NL9t7pGfHONrvoBBj7_Ri02HjTIYSvpOGuwlg1DtrXZOTyZIaOZW-8RqL1h4riTH6_kdhfgdYDct9O_k-243_rtv_8__8j_-5_-ymRm6baIbNCoi7rmzfqgEtxMCVPCKwR3Z0_wRKTYimaW9NhFxNiIapSXqUDlBklEKFYqQO0GSUQoVilA5QZJRChWKkDuUEo3SUk6QpHNLPEGS5VnKDmHuRxkZnBg6wzSOBVmzlgumq_5nZnaMZ0uyEyvhhBJbehQt8RzVTF8DcSs3EKt029uLzWqvn53lJZI3SyShRLJKfD_GVqLfqauWENrwSxv80z1nZrGqiwMbeVySLxehd-ijcQ2SqJj7kTJyukkGLbwXy7xWI79rDavlzbsh7DTUdrZuGjHb3soTLhX2VNjEkuh7aXULvWNF-buNSDHTDaLAKTo0EveVUhaUtuYXux9fBnOaLzYOzgDB0iqnhNxySuktp7TicsoaBoIeWe2jcSYYTZySo4uWdzyM9cISj6ntDSOQ2cEZj4Pvn-tbQ4yflooYkdM41My26kEDW-YsprXa2Gb2DqYjTCPChnxff-SnAx-sYhRIqtkMq3AjRFxBud9Ld2fdlMvh8D33rj7F3Qo_AjmDOClNKs2tEd2E0e0eFZFtVyHpmfVBPEZde2yKHMA9_fKKXML86ZW9yPm3V4Qyr3585ddyeBJZKPf651eEsj_9_orU519-gGUvX9_9Agseo67j7rIU73lCbpetFX4mj9gLoKPryCvmp93qyMU0TlfVkWzKTrtVzE6Vzz2xJqfmvsx219q3E12YBVnxpsegHT1Dhkva0bMacDsjHz2rI-XUGpxABkeFgwnvY0jRccf4PsT0Bbbne8eF8aOjI-SySsO82pq0r9v3QlO4jqxbfplRf47fQMuhB1ZbLzNita6W_Fq1lE8_K1YuM6qX-YEu--Nf9se_5M-LdnP1Pd1ldo9nCWs4q147Ezm7bnRb6FNPui08VgDLuchX8kGZLGXdjndZOTtWhNDB2jBt9TK2WIMU3pDyi81aHfdXjs_QLgnZXoH1U9ik8T5KhKwQRt9GCv9yKTEIilLYxRYbnfeEjTafa9LmCbovQlmH9JtCq546W_XU2aqnzlY9dbZKma6sLRHSs2qrnotbMeN-WzrO80xj_pQVL9sT5u4MDHqsCOkMqww3ZsOs6Ki6OpRJYtUiPKqWUTQ0e_qu3zZ4gxBOhHwitHcEfR9bT1nK9ZSlXE9ZypXlWOtNGIRTrnU9JULXUyJ0PSVC11MidD0lQtdTInQ9JULXUyJ0PSVC11MKcj2mINeR5mxFEKuVO4yc8ILT3ygLHD4qebpJ-GgYR_OqdpE2r9oinE4g1tMJxHq6L7xq94XPL8LuZmejB5ue81dQOIlatIYjBNSmQcgnQjsRQM_vqcig6CUkq0jTawt_0A0uZFF3mP6shwRnDYZtptYhEYHd52HoBEJAi_AJpPYfl1ODtydWe8HC4G2N7nP_ct595cdFfnsu9eDtf3vcAJhTjZPLyKmeXPjU0uWBnhoheFJ72yeotpGIqKdTq56wXfU073rpkJ7mXfc0bzyW3tgv8D3H0jM_ln7dH4IcScctdQuZ-eiF7fSgVDFss9VUpSOeazvcqdGkvCiOFboTYePS9BS8pl-E2_SLcJt-EW6TLsKFHch4if1ZWbZRaCl3hYYRVL5RljB0QDS88nh_5vRD_Y2k16G_YdS0OiJodZITYlQmOSBa1ce96uNe9XGvwrhTaPn8OHcwcXN71q8rkeeTcCkwh8TOSxcQ4yxg-aMbwPtCp0cvZ_ogZA6t5OrzKAZhjkkYrtgL8jWdvewXGkvdbnrmYtMzF5ueudikHERWoQjp-Y5NT-BuegJ3ExK4x3inFx9N5NBK6otKRM42fZ4dt9pPFa0SyifdRLdXk6G6QewrSFlJTU3Kb2oif1OT_5t6YKAJhwzA8u6QsbsGgr717c1ahFP6bDulz7ZT8ms7Jb-2U_JrJxj7qHb67YN2-uWCdvp5hXb6gQaYSUeCvtFqpx-JaKefV2inn_xop5_8gE7uhLFdgzaqtaWDZmyONzh-2A39F7Zj-JQrG9m7NpOmFZzrxOQCi7mkGBya3xIBBr8wvpYHgGp835u1x_EHxpKw3wM8HvCi4l7bFYK4lZCqIv6wkwT1ZBCqttWEDRo2cT4g2_Rs2aYm8zY9-7bpObtNTfRte3LwON0KvXgut7mPlOb_zOOqG7Yp63GCFppg1cw2qlnPeoJWqmt0aa3qbsexV8kZGoGVNuAqwn2M0qXmB6dLzQ9Ol5ofDBWCcU4rkrZyUJF4mgprsqwlhUMriS8qETm0kvKiEpGzvY5imeFgLyjsbnC8b-UU_5BzVOmqmcTOB-gt8JKr5EMqmXpbJ9evLphtUEZEtER3QASDDmoTES0JH2qTzmBBdTLkTpDePxmSLqdnFerDJ0NFh5y2scYR1Ff1AnUlDDXqRh7UaxEOl7PjfFCNPJxKBsG9I6hGHjZhEA530CNBtQKxCYPgDmYitmGZgNiMzXEGJzkqDnaDA6WBhBSKoIBZqtQNERTIBqFolgpI0_TEboPpFD-y2gsWOMW_rPacaLqvwnOXG05xB9mw3-L_6__4b__3__Y__Zf_QiyqMbrZ-L2c8bZHswuqk8wkGFMDEhEtpRQ7rpaRGxpnUTSxJEJFh-b5EaVCESo6NA6WaBWKkJqvDxVKXi-oUIbUVH6sUPB6YYUiVHYIHMjJzRyr7_SquFzBJbzBwzGMxKWeC4h7FddWxVa6aIXLuWzUykapLHjEsW95vD17s0vmgCkVCSqat069yzQ59QbU5ITbJNFOhMZMU1Tm0ErCi0pEDq0kv6hE5NBKzvl4CmerpMmGtcsCyPvXBFN3qXP9pJLR5bQrcqEyHZFsQqddxQs9EBG83FGrToaCDl0nSO-gDGUdajp0qZYutKV_DxlqOnSp9jEMvAhlHWq6VQ2yTbcmgaAbizAdDUI-Edo7gtHJQxoM9uFI0A13d0jFAYKxuwCC6giHPliE9o5g9CGfCFrO0jDZgWGa9TCaNie84FwGx4PqTeXz3Ew4T_KvB9JUDvEcm_WAz1jhcG-x3tjqJx7MSoj6-bJBTw_dv-b7Ez9RI8bY9IBGNgiHXCLUpKcm3LVv6VxWkbYg6LTCytYTpKlfCB7GxeA0RV0q5fVSuOFz5cnruj2pxobvxGovWBHFeroPG7rWPqU4nKlw9i_dv2TJQHLy7wvWvSTXPtJeB7SPCGUdajp0qdsgaEuEsg41HbrUHZJTk_KxLRFSk_KhLXHz5NSkfGhLhtSkfGxL2nI5NSkf2xIhISm_H7pMfsQXX_50LZZo6fUv10KROyz-9odrsUhKP_1uLRarL4vBqUUo1lz68VdrsWBM__SjtVi8pH_7zVoo_6ipn36ztv8wbfJ4dd89HxwdeNfv7312rpWi1UR7MNCVuaVSapA5MF29cIce7rz8Oc9J4RB14DEn5664lu1Ziok_u1Vb7Aiw4Sg-fdT6yW1Hs_rSfdD8lszex0-ZmVOzVRGeHVjh_iMgtM0Hd0tx7CUUD-njaWqgo5-rTk8G_4lJibD7Ovy8J5HUslwVorC2zjy7QqszjECSCUGf9x_bDXCec55UcwY6plbC3fk235Sd_gJSJK0Im9vSHtFr6UswDXVE2t16LX0JeiAi6mWYCLElCWl930m4_x4MncPP72T5q2jQ1_YiGYW8TulnXqAzogPBs2sbhfeWS_m91DIv42leRnFeLrWUUy1Fnt3s5cSJ4HeItx2vQ9uMoLQt-iK8FyBU4_6wve1tnwn69tYfMsWQYDRxyFbDRas7Cvwh1wyX8JGgezP8IdcMO6mf-kFGNs9hDTnEf2I5M0lkxfb8IWUICNKuD80ZAakrgnEsrE0831HpK3U7x_ntURgyE0_iSCc7FbgNU0xCsZNjEsMJk0B7kWpafmQSl2Q4rdnwYs1K2zQvpKAvbedT2_lF29K2zQuZ8Uvb7dR2e9G2tI3zQsI-b_vZkVltM4LStrit80KG-9L2bh4tbacXbYv6W0qGBxM14DWKxC6lz1Bbj2ebNRx6KMtTczh4MIdjYm11i9Q10RxW4NmDFebm8N3mg1fRHA4XmMMXM4fRZgt-_1G6BMMffNkQ7NNzKU0XdxJSpnXjYd8VaH5cJl85jLsp1-s60NoO47LJ_UYQBIN0IwhMhKBmNAU12ypA9l1QoUSh8Zb0hsu8AUV5ToZlqSlpQFWAOJ6PKZAjzkq4aoUifs4d4T6SL6V8-PdZJkl-vHffncO8lubx8TW6-fwaab2htjU0loFOGZOLU-BymZB4Q3M28NrZ81mlg5ksN-hgNivdYTOPi4fa-wrrjpcflC5BoE-4DwhwL2DfW8xZTZtuAfYXBnzvMdgnXerv-wzrozrxo475VpQpXbQpXfgE5c-lRVO0NVCWNTCma4Ppeilv1p43u-TpurLk91_b2mYrraTILa11-G2O0Eq8f9Nf79_1l93QtdeSXrWV5LaYiIQfxFqkJyaRkkd68ihWJEF6GmjQk0eDnnIa9DTQoCePBj3lNEhpoLRCGRJSTr-mPlMXzn9q3zazo4dPEO_-s6fV5OzPhUaOTruoQr2dbAZ5dqg-kiiWj_NAt3oisvcudNpXOyk0apwZe9hwSmMNWhrr2IQFhz83d9ug9ADK3RsnUPN7atCp3GwisrF5HKPR2vUt9thNPbhmkYbz2DnCZtNV2joFPakw6KmIQU8PDHpSYdBTEYOeHhj0pMKgpyIGPT0w6EmFQU9FDEJ6IK9QhIRURNTl8fn1pyYbKB28QxM6mq2i1Sp5BywMNJhlo1k287JDnES4eecRPvcxm_JWyJ1LbmKmF6nXiyLo0o8NNSge5cbdbMRcw7sB0LgRsvZY2MZdlBGOjOsdA9eLUL0MZR1qOxRJU5LyjGouXVTz76Kasxf1TNCoZ4JGIRM0N_rh7n9cw2wRWHCTiUJznlbmyFVCHNj1yWig996dGshD6i8NZNwKaw20pYG-vvy1Dt5S77iVVKsXLmXk9Q70WlD-WrhXn6tnuxh0uT9jLqc_9AJ6kqREZ0O3ADysYSxAUpqSpyvl8QMTX2eiEkRFs4m2c1mfW4eJMNvbP7Iug-X77T5AKrM7noLBArMFNguEtKutY3vbJ1I2SMMJHuEXsVYf-G3XuE_oZh7VKs-feP9PGff_3FMVcwaux4UemWxmTn3iVYqY0rs90iWauE2IeqJs1LOYop50FMPeMz2TOOqJv1HP051DqkFiz_LeMz2ROep5x1FPE456DlcUUq64wFl_8WKNaQ7WXjK-KhmFkuVVybKXjNebkvHJ7whdBCc4EN83ceARGg-XIFDKJgyZTjIcU19FCe7Q8WH4rys1NlIPnbXdvonek_r99xtChhUrKVowvYgMVc24SWqSQFITC9KejIA6sRdqsiJO814drojTyJWQ9TAUTEu1qB1gTBQ9BMOiofWM-mZrJ_xWB5Y3WKgFYCRkZdPb0cBqgZBoZWogeI0DqRokB9HO--ftn-OMLnyCH71JF84sgEOHgxdh1zo8L6BDJoZihXYCrajxduYtHmuLbW1xXA8CzNjVb_8p-FKGVoTU4HR9C94tXe6Tr7Atrytt60qS4knPvUh6SkhKOiRki3BIX_kypKfdJD1ZJ0lX-bFu8Gs2uoDOBTYcZCuaMaBGBH6GeNb2iKf5gGjOGPzzT4yCEEDy5gge4_bEKBb5mwuimz3PJG4Gh6IElR2KpJA0uFm97SDvNySg2IQB_C7UhGkgIDpxbzKGkwcZAxtXUdtykmIGsM6z1xpSEcZJkb8wVCZ6kru9hSPLvZDOGeKWspCF4dZQ90K6Ql8PpGKJYExiyUVKUwnjk1gpLgrq0z6gkRg3Ko4zw5FoSdYvmch6nCDr0YWshyuyHq7Iemgk6wGVrEdNsh6hyfpVIlmPBmUhGjRchyU9eQ4QW7hdAfUYj7DKbO5CIENUQiODZCvXNGsXi7cEDglyseQzpZ0plyp4ex9kKOtQ08R1GWfL129W1GO-RT3mW9RjvkV1M5bdzYg2dRlbO8wB3MDb0SQkCKL6AFJk6kN2IfZ-hGvXNJxxfzK1stE58ITIPR9OQqvnQVB8SmOd2uTGAlgg58aWYWL7njLuQJDaGLcbWG1cwujBJghs2JIeUze36RWs03T2i0ouqk-77P7poSRhbSnqGJaXhmYTbWf0pMJRfhxY4RXremEO4KeTFT5MHg3NJtpM9HphJcB7Hkj5DamZ9gaff-0w_4YnE35StkTlWhrBo9mbiHiELfVtGXg08-bQrOwzjD0bPh-3_HNHpw_8A6V5Bvja4aDBNFhC7o_snEqryEsVLtrwhZp5-F1xNB7nVCzPIJSvTLtPiLXnuN7dKp07OIabQJAMq6I7RIvukSzSxQUUEq2nons_i-6yLbqfeY6SVqGuV2VIv2hiqlytQhHSL9AowgUa4NKseMyKWoMVIuN-BHfb20jyi6KbbdjLQNKMXWR4XUe_Za-rBoPXVYbROVDJyT70BNT9SBWHRCdBbYqTAI3CShy4iiEKfTUp9UzxqmHaG5AM06qePavq2bO6u3vR-ujVLdadHOwcn4D-Vt_qMxjfxHIsjK9jOBbq7rweGhwGTrEoYOw09JXbt75y-9Y3bt_qLX1fk4m-8e3WN77davp2UdHDqneo2TYLE7R1Hb7Hoa3bjD9u6hp9xzXDDRwl9KvwuO-4VoBbh1Wf7iAmJK4-XfR2CC1SL3KNvMW1nbi2s3qr0SaAj9yGTTBW_EaCJIgDKb0hVYVUaZ-mnfKXstAUWTqFMF-Fkqavuq-76r7uqvu6q-7rnvNNgySdXXVfd9V93VX3dVfd110lXzfq7tZ9i2nkct3Rrne6-0XRTXdDmXi9KDP72D2PwaMz6M7sPHibrDJ7rzo5NouM6r45XAVrWlqjt0iDtoSqs6Fr-5jYlJ59K6njXlqGyg5FUp80xZoaC2i7O52VEdPEWtkhVNpzII8ZSmNk7QylNs4n4WbwO72EzeDCv37jl9f0eaQC-W7hOxZRhs-smAfwpTW0nNFTvhPORNNoAJKdVoSvbpN6pEe2L2CsFbAIIFoAbXjk0QK4hQxaALcDjJsAo1zZyjljn0-XXxlBYAzNNl8__a6l_HEpbnP5lPwyWHvJQ_LLYO0lD8kvg7WVPCW_DBYR2_kC3-vUD_6lanlTdBXiUOY-AxZ4Gb-HDPJFMn1lmZuvcKbkM6WdKWpOMfRBhtScYmhTkPvQlCDDoSUR0XKKoR0RUXOKcVhFSMgpZk2J6caAtbPXHZjUsb6DwfKIYz-c6nXHqXvJbWAYxmpi5EUrQQ0YwqZ75aGOcKm7pYXp8dcz-7IRdRQvEH8t4M4FkqfrQdZkuCQ01MocxoVhogcdOdb8gfUmv3hIIUvfIslUpdjegWTl-g5Se0Mycp6hLxpo5DxD2wuIrnto9E0SMs5_qsr9UOV7DnKlrU-PPdMDW2oybHjnmpY88kQcWLDiz4e2NX8-_Rb2_h4nkbm_HzPN2rpjj60QA3aJbO8JyUWRtMHyyAzrwo0fHkITIby1Ll4U3awLKOPT72XyNSySoFskzh8NhV6fTalniprKCw3IkJbKC_VJVoDTUnmhNhGZ7iO5OhkSrg9DbQlYeWEfzBYE3e1GEMBQ3kBy5-QFfNOi63k38o_PPR-uMqnnwwMo3st00eHFXwyxNLojqc0e5rVhYjji9H5Nd_7IByXqrDRs1lcBtdKwx2KylTiuygPLv1D1QLJ1L85Dm1TfkIysb2hGA6Wsb1S-bjhLh_IN1gmgynqsBMYHbmowINlqrpNUhUm7c1BhS00IN6zDVbhcJH4yzPFMRv59EL3SUuSWr_5De9nf59HvYzh3Gz2NOftxVtyFP0xz-u71Kr_ca2AX2jQfsJN9sQF4VIE9PKpEOIF_ARlMvkPQNPskHHqhKgI0mHdH1eiPzlqoRVSRXvXEQsWS9vSaJxaqExHNE4vvKKlIr3tiyQeg9zpxVw84hMinkF2xIO-85crEodJQy5U5PpItb_Frm_IWSLaU9IaXEl5EAyUvJQe1g5eoDGEhTVmla2X4MF6nQqY0mcNXv_teek4-75M__ez4B0m2_3uSSA54fTuVOnCHG4kWxbey50XRTQJBGRJoel3mvtSYl5GEVtgzalFahd1jhsIqlC6smpzgAV0wJVA4-yXD2S8Zzn7JoPslg-6X7BWHefII6pHEVVCdjkF3IAbdgRh0B2IQHIhoH4dxVH4xnAGAqwEEG74TnuGzTH3oV1RoKIbHlNKTWObcsgJhOJNpYCtaNncYKeU2PdEFqQnoYHroYH6Y6Em44_w-sF754MIbH1yw_F3B8neFN9678MZ7F95478Ib7x3IZ8XNhpPVv86QhZdkFn-0Im4Jp13N_T7N1D4hz9sk1myXSXRIXLNqII_n23tghs7EvUlrGhxoO5F3aO1GXLuxJt2sKzAclhRuFcLVLw4tn5yetKQuOCLeNDuVa4RUOqpbY983XjPZIZ0SJMwym2YEsmsWGZMtRxfFXEsNvS4dRZ0bx_ZO2yhE4Yr5BRM3EREyL--tmrWJiGcvXTx76XotQVKfvbQM1R2KtFeS3ouC0wzVWxxJqlztxeGdk7VehMs0hzpjXqveYlZ03WgjkfXhdmC_R47XT3Qub90vqlZASc-VztGUObcDeescqvA5Q887qTkfjYRUMqNNjR95rlxfoobGjzyXT-cnKi40tR2TidZz2ZOaxoVnqml8KVP7xTf-s17ToiJ5DRpYBRAVZdySU-8PoMalmMzZA1CRyrWDq2ohra6qGL6a5yZ8NVx6bhKIfcan7oWI4T_T3ZQed1PyiT57fjwsO3IGc0DXQ68EctD1VGCh3c_SxZ_BXC9U0wE4zwuk_HYX-aLopvcSnJl_UWRov9l_Uf1p8LW-PTsDDSogwXFxQ7ekeKaUM8Wpaqg3IENlg1Dy9gozF8tDGiVy4N7DQBvCKxEH0Wt6ONJB1MC7K-IKXl9Dyxk9pO6PL3hgvbg5AElm_j22dyBZp_95c4pgBEmigMXakkAPyw9bkrQn8-VzEAJeYOwLMGSQhi-WCk-OahkAafyswQLTJu14euJ5qjLHLRxJCywVDRjjEmncPfDD4T4o9VypAr8y0td87r6KSn6ZCYITHUm1a5Dufcz9pwejo8-y8KzNZ0MvZNjrz91MOe6ArDKbJgByaRYZ9yl5Jn1su5Cs-w2z7jfMxT4ZBmWNq71g3LarvXJebk8Q5Ho-3qIAtYhaoZeWIe2SA0BiWp-wgUOre4634TkbA29Z2_mVey3Tqz09zAYrpQ1GR1ELMEAaap1RH1_VVhY4Ow6sN2fUx3wxFU9-cVZ8DKNNenFWHEfhDUk5zwY9VtRT76mmnnrR9CYxLY_TxEMtzdsxtxxz9Dvdq79fOBY_vq03lw34jlzcsOCWCjhbgNk6c_GOCTB1UmXv-pmzeH1irFPQF0j0IYK-EiChw4ZI8eJ3yQ5s_qzOZ9mRR_kx4lCtoB4osMGfoaL6dn_wouimFaCMu16UQTlcqnHRF9QoHbyF4ZGhqsnRop3Jhep0RArLlP1YMxr4ZTiFjslKZaRNCMlKZWxUjTSrTjIuP4F3kC8_QdC8_ARJVc_PwhmzM9jgUyVW9sS3IViLdXQYv76G1jN6EuzlzcFi6OVB_JcXh4OxvQPJOhzMm9MkO35tBa0SOlQ9Vk1U_Xq_SF9uFxVATMDPy0L248cYei_ubvw-6-7H5WUmPK0wRzRBcVxRqAEVJE6VsQZUUOiXkj_9B1bDJ7pGRHN1u7iuUXhWNhFe3SbCEYkqUnax33_daUCBPovk2UVoPjRiqte5I9rs8brfccgh0VSv6WCqV_fqLkdow7S669mT04da1Bo4bLvWqNpvtQEi6Ya6ZyWhoOyQeZMDiuY6bqQT5HYduUeG3AbSi_vD4GWM-8OAsSgBpefsOOva8_HTMkLPcRsz56KxjRmT0trGjOlp5mjB1FCURjWzsOorVxZO4QPrjSurvnFl4QSxSW9cWdXK3YJZsekM_NjPPZZfOSPdgamgaILLKLhvcAy0HNne6eFqEv1ElZ-RRhiEbOsHZ54fnsdHbT5Ckd6uXcw34r8BgdsgE-QxswtpIHWJedFH7trjAw2vtyfduTQFAa1JSFZVB1ZHVQdWxJ5lqk4qHRp6kKS93W28KLrtNqAMOYOil1n6SK7dvH3wB8-YVUbpFbmqUyKjum3Dc7JF8EF9tgiR-GxH4tvZbYXfVoL0BDb80hKkHqxtao5bU3PcmnqwtgkHa1nvpLtO8UNII4IaZg6_euXCsHxxtpGwSZOdXKxA-5Gfz3xQE83MOkNZo6DmuVDaEw09KbH2ysXW3jjP2hvnWXvjF2tv_GLN-KUZaEYDrcy3JuXMLR8_-IaqSo3eseH3IGDO_Hzio_euvT9W2sYNgmNz14yg0lKMRFVuEalF_XHP154flu2TMmrP_6xZ2oEOPokqSQdhQBrYp2Ua_bUq7bRM42dQNzgzWLRLGk-YW9uhYls7uoNGw3pC9S9lYShsGaENzuIb4a64patv8kL7fEeqz79MTagfz_aAMrgSxtC66VIuvxpk5ar7ozQfdasKHsWvNi3T-9qNsIEVioGKhuqlXS9Ajz8L1HW5mpnQDv2XtCn0QtCmUEjQjNhzvSXLIIDSNkW_GxH2hEiRf54N0NPP2mElfk0xw_JtSzEbhJnRt5QcB1TlnTEStB_EGzS_vJ-YrT7nib5ZHRPmD_2peVSGODdktYvTQ0PfuDTHBz-wXrg0kWSq0vFtbNILlybUJKtW6ItyTwJ24k22BAh5nHTKLQUIK7cUILyK0gEnS_qz0VVUUaSycKiiRYMATNr5u6sZvryWzoDsXzuzwfJogCh2bhPF_VH0JDyGSNwQx6GQ_4Op3_DIe5puNh-K6WYqfF0GHHJfvq4gugqBwSBHNzkDBapTM8mgsAypmWQAyb_1BVXuv_UFQFFlIxRcfutroI63Kct0R1N53A6oP1UKBOWXROGN415td_cc3ilovUbx56x8N6xEQ6V8NxRTzkgHw_mtgFI6GCw_98zUmaP1XGw5x7CmET0Zk5QjdBpjWXUaD4a6FGJhS4G0P5Ci1c6_JD-2wGfucmwh02Uivhlbwo8Hjr8Kuw40ojABry4PMXWK403eMQyuunLuHfbdsr6L9pPn3bU5nz9Wb9meYZc9ZvPQZ3g518Ybp0E2Nr2icyBjAyEjhTVwTTKfUf4ahjAutv4oLCsYXMJfNMCYCC5hBW1j3MSy8DWXTgpTl77z8lEHhQylRiFjqlF2JxjvQxEhIVmON-rY74viqvLXaUX6oL7VoGT1rQalkTVmUdYgIm2hiNDuGsRFjxB9tFc0IKF9jokHgMjbaz3wXsSuHUOR5PPcI-KjPXNvQBcZf_4RkRH0L8SaVL6QJ12I-jxy-nu6tHWnP8K68_7oIj0CXeDp64h9BSHcKZk-6d5LidtU5NJFOWvIpmSVMmxI35V096_1mElbAJaHcH2eI46oUGBEbzcCKnpHv6uINBW5NPXE2zMZWWO4RxTdlNTz5IZJNoCenZx2oNzC-97urMD1SPX7ovqeDbYzxtknlRFD77HOyO5RT8mo4_kpjvG8UvHRf500jOFGNBeQzxkvGH-yTBzotXI9BrFDpTWA-ABhAKhYAx6wo88SmtWgVQcwNmIi4PFE21b_XH5bMx2CQqCF1kKCohr9ljDWf4nA3kMlWOYp7bysDEN9x3DX1o1BSEo_B8Hv6hplqYklA5uuCQWTTePg93IDS9qbIkH6qRAixYMfxuJ4lA5FpB8Weay8vpTCsyRyF6JdTD7HIyB1l-GOSOjeFUkD9B6JSN0Qx0ZNEKwwZhJSV2TIlFCeHy7exWR4xFHepVB4JNntPlwBcP7swCM_d7ka4iOiL6HxMH9KhAPubryERQaGWoYdCUbPI-Vg0ofWA9bx-TaRiLnwnHx0bZN_ITya0e9AdVBiTBOKJgfFKAoxibvSnvx3uU_K4ObJ5OP9eK4HSj0_bJ-ZlI6Orh8K0OQk-rxsm6s4MqJWGR3HnUfrtisWgoD0BgHIuyRISNY1CS_qDmVtVq3d2oXR95XlbyQ_LKEw4rEO6qaQNytxz9Pj9e8yivdwH0OUxwMXoD1hckB7hzgkb0LmYGgdWa6BANkepZ-Opjj11sgbjqi6i3gjorbNldbRll8DR7SIKOgSmOyqronELSTvhyJxVuFhEa4jehuS9uhlRaSoiDtuNXBALIrunhq7kZiV3cgErN0IDK2g5mCJS0hREXfaa-CHMBiqw23orVhP5n-Mp01GHHcqrQyYkY9u7MZfXPcBsfWfqroTeP6iioTRDVNVxtQgNXpTlbEuO4IB9G1JkDVidA52GJJGjFd4bjUI6XMlX4gmSXhBFn0WFK2W6MlB-rxtWi1RpxTfXyTqjRLqlxReb0FUeOMNBF3D30QiSGchKb57bDhkKsPlfdQOioEl9nqmIkvUd6RQqPNIVGkpaz0ZjHbuie5dStKB0q2H0raBvqSoUi1od7cOaM86HND-GhySle0cQq0jsodxfr9df6bdlTygoIwo4oujmkJZnQ6gd6FP5WKZMQiGFZRqyENz06fN0udpd0EOKNiqPhHvtKzqE_GOyqq-Ny-p7d66iKguyaS6JNPZJZnOLsmkuyR5F2w7Ifl-pGo1E8ZzyUqYpatiZUzAsjLggwsWQ1K9oUn1hibVG5qO3tB09IYm1RvK2zctFLh4IrCzMQPMt2nxN8mgf_ymTgav56e5mww-Rsnf3WOQwlPllURDKIErrchoc2BIiWg_vfEVNyJ6aT5YEBKPU7mVaWHtJlRyYELVaUIBUlTjKjnFD5uyZVylYBlXyZXPYxLG-OmqG4yWPNJnNhsoo1PuP_e796WU0_M7aeX2L0N6A3_-rLvtERoKWfDzccxU3XnkFB0oojI9QrJ2y3t-4xihxPtD3yadu0rdwIvgw34JFwLyxh0sDKUDHFY7URQW7wewhK7UrRLakx2VOoIuV7sjlCV8K8w4BeOff0NR8SKj2qobZq6k9WGk96Kg4LLq9M2q0zfvTl9Wm60uc5TV5XhuqbusepWz6lXOm1eZ12aqmwwJV5sIzo8HNe4-4RzCH0xkIiIZVssa_kIJmS9l15rDInEx1SOnR2aWuuTjjVUhwZgJIqN8ybDYFl8JwVlyYVAEOVpVscKXidgC2sDQQuC2NZvx_bct_y6QV6vOOdLJ8syW-8AHERnuakXh4I9YOrZc48T7x8bxLW4TJ2SMBFQQSQU9d6ZIYqxdNpa4VUI7sqM_dwROruBLwamE5-TKnbvVbyL4fnj3HF5xH_fk-H7_lu_rDsj66RWUwJxGo6czN825Qvj9W8P3QyQyhI4aMorG8I2-TYa3KV84_vm-TL3DHvAi7k46fVYV_RT3z6hAmXR9izixSLtYmTbb-dqw9duwVKg22jm8RO2OZ30l0t3S1yJ8ujpLfdlS0TTbq097bmvvKfmt-fkMS_E6i39ruk8nfa3RJhcPZOXDsEdj5cNXDebGGT6wQgJZUMa1frv3CQKp5b7Q7ZGO9XPHtcapA9B2G-7oHUgS_IRbQYpopSPpXmKbfD7Mj2bCD-zvDzyn33OkKPURTnvJOEs-89bNDzxK4gfquptKplxm8dRnVvoWv__sn9g_ntbEVApZoXDMG2rz3dWL1j2VPBNioosueZHSIJGyPCFQX-d18Br6l8IwPxoEY0v6pKUCZOD54vpJSyWwsyn-etzG99WAKd6ft8RIxdllDSV-i0c9MWE7VQ9RXSCuGTZ01gbyhTHndstyQ4uOQ92w6zimO7wfXUFZXfXczqrndlYjt7MauZ2VJN7hoz1ZckB6imU1MjN7I6LgQkU8KUL_L93iYX0WpdVoYVC0gZUlK3v1_vPcWguDon0fsYN8BGOwWhgU7SvbHZwfm4-K6IkWPjbvh5zyU0neJj4SrttESM9YhRfKhi0MFUtDxrsjDsugXMa38bQv0g4SVp4IXXrbSAn6G7AG5HHg3ZNCRaAvO4PrS4SyDgUdugQIjk_dFzP5vo_5tOcSqIFc6fN4lvxXJ7hAFKAk9jJd3EWEJLGXmUTiuW6VNPYYy3-Jtcy6MuXN1hVRUPAuyYYT65m8VCutJAYa5iXKubruHJVVN0UF1V095lFpqrt6_KW_XXVjG7FnSCk9IKjUAwhPe6f2AH9r8NJ7UHoGl9IDgko9qOjdVXtQ4XaxsveArg6nLikdkQ5cCyKGIWJtevS4GXmybZ4T3bF5XHrTUE3ILB1Y2tsbWN3b43WK0g21zaRor6nIT9ozUTiOFgZFGyxF_NL3S5L3e7QwKNqQ2x00cnqlkefNLiMPaq8ZebtNyBMeWBW-NCsnjgRvVnxbVGetSvPM055dIrYHCTgkdgy3sy2WrrWK-9T7Qy3b2Q3n21kBpttZrXRcxm1b47Pnu6JtrV9wGO4LJmomKkZc_pkt1SZiwvIHndSIh1bQmsZK5A2LS4m3L68F2o0YNNXYrh5Lk9UCRYUNabsCqJ3yyS6TryBMeg4F9dtJkluYqmwq6Aif-rwhHZL0StNuHxaXP39bsaX9PhOGGC3pDVHkPp_p7215RPsJAplwIJYD3adZCeArOZ1bLyF1FTyc9SqTD1Ie-ZJmxPpZXaOdQqa0lIIFpZOeGgVtJykiCaKeUIQGHGlA1GHQS6kPowHB5cYbFzOTxLEBZQDlRP2E5aQ8HsBWi8LTbyeXc_sy9o0Uo17D-2514jL8TtXFZ41DMh262YM_F52F3446v7itsanpZsx5Kbb4rLHE9Cy6dnvHi-yX3AoSP1r3WN9tubPHGotPp_w7j_UY0sCH1KXHETs6Lb9mmXGDt_7XMdWeMy3fQsLhbwVt2LZcNgdVWHBMsS1g3gVQ1Ffq6WoDgH1OB6SSuRwI9VRDU2pIhawl_Er_GXECXEiZZ9h9e3Q9v80p3kUSacmU1kd8xXK2YPyIMoBBcoWCVOGQoMcREpQetiVCRa-wnCo0uiEoWIAEBYuF9Jb07qmIoK5Rwao90MuIvd5PAvG-iYMQdwjVtsPT4ET3ue6ycezZuDlu15VuXD2oYbIBy6qWzWDWI6CIBkF_D2hHNAhcmG86nmV4U4nf9jfmmLIjo1Vbm2B8K6vPl2YIoalA32o8E95qYMJbcYzv6XmdUdoyk_cQsGuvE80VN7J_BawJX4aVk0cfKUEbuMEY3lgl3xqHNsvHoKCHyVkxYSAtr4iYcMPFwILaO1ZaVKGw7KX6EWoHSNpXkyZ3SHV7k4GeUL-PDHtY6KMGjwR2b-PK-xuz-kTGtxcXm3SBPYTPFKBEYSDMpcJrLXutMid4ELWx53fdtoUXb797arxAoNUwRB5md6GDJTmNElkVXY4_GbR4A8JaxU7p2gG-3eMSDl1CxeGhgb1_vX9c6PbQhNY--QlGowXVw7yCBTVWzyUuLjad5PkpnMzgkFGhPqtlSDhZwiDRYHJZh4IOXSdIMn2gQhHKOtROkGSBQYUiFHToOkF6fXon9J6riGRoOS22gIte7bWOSMaZ2-NxrAciskcq-JtKpp4Tont8TEVIP69X-wUvQT2PVz3J3zQIELuTGelk3nka8xBNJeinepoNGhH6OQjib55vBKmbvI1zNy2Lbr6IgI13EDDhwhGO8SwIXqdsvc3e7gaU99p4IiFp44kE8efjN4I4nKwN2c6rpJsHM80nrSdI8OrcoDVYBpuvWlc9fVvTnVZzWPxpvRDxp51zQLGh6Ux7kQOKhUim3tscUCi6edRe5oBi8X_0qPm6etTyPUL5Kf6HutRAGHrhcAxCSYfqARJt4zlrdkgIYoGZCz2UbGTooQjVk_kMvTHNZ1wIoqWMK14G69mMxuUscCDmWP0Fv_lW6ic4v8Ycd5zFHCWYxBy10nh_9XgJ7f5qan73S8t62G3cO8DNb4HyiAxozGE-KfSS60tB16Hd7lO3293IbX9SNyOMQXc8Ecm9zTvR5oXPK0JCEJRBomHrhbAq64YFSdarFwL7vBsGJJmoXg3rYTcMSK9PRSSz0mvhSEAkE9FrIVGUpvowSAaiV-Oo-K4ipMUwsSlpY-i_e8kAc2kca-NQv71B1vZI2W-02yjhTJFuYeMU4XaEhSHdBrA0o5rOwSlmxyAIFzAPDM-cbM_EPDpepZyNx7oFFNHMDSP4o9jSQQpxIlb2l-KYlb0EVSs5pLR70pYELd_5AgIm9H1gQt85Jlvhs9NaXziGBnAQbufmmJUOjM0a6cDYurjr8ZQh7no8bcZIyMXO6gm54qsySLSjQtQht0NgCkGFhrEE9VoMdzan8K0towgGz-a4o-EUQj8d9F0Dn-fIy2I4bTg3nASYGk5KabBzwhXGXjHujsodpj-dBZznIoYucxR_qEDhzXRv5pNiq_hDBQoxyPrNkLFtv9YScYweuei5YbWIFJ-2ZS9ngQQhXMvrMjFxQx-EiO2CGSJIOtmAU88QajokVyiFKzlmiNA1yxK_HETgjXgPcqTLoTaO6JNYOJLIXDmCClo5kppdqzlT4knXuX1eSnZqKHMWs0eirR70WH_QY_1Bj2UHNQAe1BB82MP2aMlE_RBVjfrxpxrJkQXRMIsjzmoSLJ9npOFh2Y6J49yFgIX91QaWld4Nwgi_mgTL0xlplFZm0CCt9YJWrrs4BgMTxmBgQkbDwIToM8dkf-l8Z60vCnbtdaL9Fcd9_QIWhO-PWNY-IRLI9VwGwQqix0v_giB_ehc1_yuSskUCCzGSy9NEKzNm7YWQEIShREy68Y1hSjIo7Zzk_wUhJNWPkB7tjHoMfw6HAok2b9Rj-FGP4c8B2KxceC_J4wjvJUJZh_QMAHgvw-aGjpo2Ny4o0UuJU0QGswVaOQS4lCwbHheTnENQWfe2X4Sldm6_crUrp8WcBjN3ZxArNybF7QgdtBK1Is16El2T8bkSPdJba6gJHfsvaC0mNF9D-uSVIT3AH_UAf9QD_FEP8Ec9wB_1AH_UA_xRD_BHPcAf9QB_1AP8UQ_wRz3AH_UAfxTiyrxCEdJj2FGPfEc1yB_VIH9Ug_xRDdhHNcgf1cSAqAb5oxrkj2pyRFSTI6KaHBH35AhcskmL4w6CEHQeWNXt50TD5bK46FWb7sjePclVhzZi0mOzgbQi-xOTcLUUx0wDPCX1HXn3JEMfrb_5AgIm9H1gQt85Jlups9NaXziG1l7Sf9IE27NyMpNxLBQpepoHWIowVlpSJgzXBoOVloQo7cDUzIzB0I_F8gZES9HTV_zt0Ewv8_OhGWjqh0MzUOL3QzO94D8fmoHi_xjiT_7HQzPQ3K-HZkCWJj1BIOkJAtLUY5BoeCc9QSDpCQLQlmRCQynDhE56FgCr3jayUVaI1jAuNcsaxsVmcoyUArSYsZ43Efv0XOAUkuEgFijEdE7RdhCn2A_Lfst_8rcMs3EhqTbV2H8YoJXPFcs27_RpIkN6ZD7pkfmkR-aTHplPemQ-6ZH5pEfmkx6ZT3pkPumR-aRHq5Me4056ZDyp4fSkhuCTGupPaqg_qaH-pEbZ0x5l5--qD5Bkz-UR9RUwEjQWjchMbuIzCJY_NNMgosyggUjZGoOXEL2i8BK6VzSTWxwNguUVhZcwvKLwEsbdFuJLDCwa2Oi_hskG4-y01hc5RRT6Kbonc9TGEgkjkm4SrNzP3v7B99g7cvA95hF7VlyLOWq9RcJI5dBqN5yHWchGQEhIRkBID5jPFpUKZUgPmEMPJZMDeihC5QDZRgaOqWhk4BeRwXIEzWB6Dle_arOkj69xC6ZvOA-mCzANpiulh52SWxiCSnbw7YzIKriOh4sECq8iPz9v8MxZJbtRoBBbKYfFzcjnuj41ZUg_EZ318GTWT0RnIcLOIclgyfop5ayHQrMeCs36OWq8SFyHJIMlq2eLsxpazXo8NgvBetYJsb49ZYBVJyJqeDerp7KzGsWGAZJMmULushOtjCIE3QZmHJMuxjHpcj4mXc7HpHvPzMy3Ypx4LsaJ53I-8VzOJ55LUN9hUGiQQXSXFSEgOjAhwjcw4-jzfD0BM44-zxfS-rm40thA8MPo870Ug6cY56WLcV66nM9LFyPXACn6CXuwpIp8pnPA9nnq8uY8dTHOUxfjPHVRD9IPhpWMQBuwnHu9kh-P8PRCvx3hgYZ-O8IDhf7lCE8v-s9HeKD4P_r3SvjxCE_RY-1Fj7VL8wchPdZe9Fj7nC9KN2RIj7UXPWpe9Kh50U_FIyMcGS_i6iieRJsaF64MGqfnBye84OgRdDTci3c9Ha61T4llM9w3nBvuAkwNd6X0MJrL-VC_QGF2d8lHH6VAIXb3t49_JB8lt3_E3HiH81oO4TN9GSSzq-ih-KIH1YseVC962L_oyQJFD8UXPRRf9FB80UPxRQ-qFz2oXvSgetGD6kUPqhc9qF7U6HRRo-BFjYIXNQpe1Gh7USP0RY1oFzUKXvbIOc7tejrMXrWTdYNgBLyrcVknKy5aGbyPlg-xno6619NR92pEvasewuT1m5uBejznPt9CwIxz7lLfOSZfuDg7rfVFdmLW0zn3at3gWwnlPu8lmr692zr85gh71bIgYFVUPWpZ9ahl1aOW87WVtkQTpupnl6t-drkKAc2h9fHNRdOiWkeSqxSkxMAhVmum2tV4SrXbGUyd1-fCx77aFYtAoBB1XtdDwhhrrLn_pEB05VNy_CMdSYH1YnzeJE7I3A8uwu2q_Jd7Xf99rtrwaNn9GQt5FFogj8r-qBcsaMWtZIag1GxGcKrRiIooD5sRFWqOLG9RkkmlOZa8JFmaFB6B5dwwXDC3QY3dOvkuc6SX-TlzBJr6IXMESvyeOdIL_nPmCBT_x51liz9mjkBz_5g50oQwDki4JsRPhpzCycDklGNrqiT6rOyLgfP7Ylg2cqy0xMD1D_Wn5ZrT_lgNt9ERHMtpQ9ZF6CnmyKmjJgS-MhU92mAKcSGsUgiYYZVCpGpWKUSTWC9FLdmkGBxssVpOn_a8cf70H125f2D0S_s-dH_-u6__x39mxki70pNVEuN35H3r29mLTPAY5Ot9ZbS1ZqCLYJNUB7yzCBVdq7TQT6qlNOdGynTZtYQX7iY6Vcnzrijada1apV1hf5T3R21_dCnqCKoUkawibUNg3WGnpVWLvZexbGBCzIG3Z0gCfEGLkY-MpjFg3WMfYTuzAV1UiIIIhyzJhjOOmgpnG75McUabOJGs4019ZUBrkuxBSJQ9CAqyB_omVykczZlVCmdgsErBR4pVCs7JWaXgG8QqBd8qVik4NWeVgkeRjaUkdPF7iZDqriUzkYpqOgN8S-RazxDmduH-zemvEZK37gnyEbsnQlmH2g4luoQ3yQlF6PNM-iY8z8vz_rP3zWG--LPkHX82flMCuCEMbjfAmktHm3YLmEChnwIm2NBPARMs9A8BEyj6rwETLP5vZu239NuACfzaB3wydIxtgCXgHfNG7cDuOOEEwWWxEPasK05IXvDNQPevvlMV3TecIgZeF4rQE7C0vl0xLC0Fbfi1RRTaFhyOy-tLFyjyjosb9VgSmWvBP2oQFho20KHvGor3T2w_P8ctfwhydya3ahy66-4WvSPV-pLps-6KeBwVqxeNFZMpjq6blMpn8cYVSqoWyVOB4Z-fiqBV0HHJ92_VfvmOOZeuSFp5bu25a_yK0TH66VYM3__nnf-Yar_w45nvaX8C8G_mTXt0hZN6TNSqq2zg0jMdUNm55JhzrF2JfYunOuGbLTx_5NWfeGq77oqZNnyVz5laT1TQk31JEj2JAgB9rPfCZpUSywe_Rx9tr-_DYeV2RqjCygUoqyt3EB1d4miBYwOOiAasSKijb_gi1vGXV0ITbGdv1qFmHU8p7c-Gh53IBA9-kyzICx8NrJzKmaKoN2uKIqDYogi6uEmZRju5oiChoJtPA33cZUkGQ_TUEpmsivR94_Oz5I80E7pLp4Mo9sDGg0l7ZVUuzmkNcgumJU7-SRx3kwER-7Qz3ZkZaONv6pz58GI3k5MqJDIzwCq-XL8YK5dPzY9zxkU-QZZAPMoxP7P174W1CzqcHjohngjTiaPJv8Tm0FbTZESlt0sduvxeqnohwGGEgv_sb0jnkBS_wtETIcFPC6Z1yMcNy-qEhzK_OuGxqfdOeCzxsxMeCv6rEx6L_-NupV_b9t4Jj8396oTv6zFgest8MnbPmp7tDHAUcT0LUDhayGHcNLVayAH9frTaQNX3opUBrIpWHjTqv5o6l7ZyUb2dUHeTutBUCeslS8NUiYEQ1j0UslBthqBr3ZANrBlYOGpreE-Z4uka_VJWVUp1OrRl63R4SUWnw2tqaLM0PrwHNC9re3gR3Sqgoxa9oMojHfPvBlZX9rCWbqkpmg1DmY5Fd9L5YeZar5uUyZm52ofKptfuFTEZxECbnkbB2FLv1Jn9_f-W9m69tsS8YdgfOmdB9wsO_GIgTl2gKWwndfK9FF_dhwBNEaAoULe_vjMjUiIlUpq1_bIX9pCi7rxLOlTuJ5qCSuIoSWVwHG3hqPa3rLdMPaFz69k6CpN2wuF4eOl2cppPis0jAlXE0GJYPnxKm63Ht-EAmXpANFxLcCHJ8hVdSPdUcJ83i0YbDGOrur6FG1tHSCcETxbaxoT16SVe_QrvaDr7tMfrKhryqSZYAFnD7fmgL3C7e77jSljdgS5jJTqfknY4cNJrFwCM4Qa10g0tKpZeTTHHEWWgSbhGWbi2lR66B3e1z0OkSsrkpg3ovyWqAxSIioYxFbVDp0F9IYwrbbgGEND1J4jpUE6wrZQPG_cgSPnWrIYCfgQQvGHxCVLgG69iUByGlL4m2KFdsmCnvZPFMYqS0O9KmlyCi9Ahq-WEOS4LOljMYVw5dKLZs2AP8jaUqfLfQu2e9l1yb3bhGKgHUXSBChjcBdrup7ttvHGdCPV_NrlSHPEAPGkZd9uK_wRfKaS6dtWHs59UMvEN4HIZoQLg0AhQRUzoMYE9qw8vvbPBkRHZiQRc_7oDIIzMXQ3j4AqeSL1wEUDzz1w7qEmwOOYiaw5iiApXFVi4MR-dBmuUsxX6LsoJFX0X5YRCP4lytqI_jnJC8R_6DeLyVroW5Wy7LZLMM74EKHw6y2kpqNCQXmzpURchlh6F8UAZjPquDEXxC1MfdMsdMLJuuaOMjpgBun6LUSBsKeHZlQ_QsHflc-xLAq5ukwby9eg2ieRiGe42gTkxcRkWJDu5TRoQmrMqNai6xDkNcXZ1xOnmEgl6coTETdgibkITMO87NSieQxORRARWVSWKUQe0-aEFTrX5Ec9-gxf8GjBgFUoIqE5E7v4XVY5Inc9bKzvOYYdVOaGIG5IoQuPJkY8jfkCAWK-IYE9VbBz4idaRduYSVLNxuEd0nXe2msipaTnkl9A_vn6TWFTyOxYF0AOLsrDWE_H9Ah3YRsnouzBt3Jhp48ZMGzdmOrsx086NSWvYbn7omLL509Z5mbbOy7R1XqYXzstE_Ksb5yWgGT1g2fd9StMmVY2SxN2KMs7sq9OcemncC3FATMxkE02blFjLZCvI0XqVTjCcYSUqLsKps2NwI11rm3SRFG9FzNVhK7k6LCViB6VUW4w0XEsHtCiow_lPO8x82b33_qe-PvsxnuhUGq4luOhDfEMXfYgyLvgQAXcMbp0GV5MeaWRmy7OAnDuN0IvM2tPJb4k7_kBhh3BybKaqIHSLLX3hBUw9VrbDTbT3Wy8fjLWRcNoWy93LASmrYGPkcrTclnBvK_N1uBeq-iLcCyW-D_e2gj8O90LxH5pteXmQ9xDuhep-Fu5tE9uCq_SLlFaFlt1YDKJlJ4ORC8lQ1E5gbWxisoCxicmiipTRg7N-Y5lSWHVz-UlBbACdg9iZHCzn1ljGg_PEkso9T5uNNDK4Xrs10OYu8ldooblLVIPLJAd2Polq6QKS2kYuEpottOx0XS0TN7KiiOWNLztvfNmsyYcUVWjirW8tX5WiaFTBjGfV6srgoj2GdjP3JIuybmy5owLT96siP3GLoktubYqo_PTBIW54ngvXhQ-i6KIMx_2NKMOJFENVjSEV4siAL2rmJ-6KwtI7J0dHoWmhevSmkOtu-WYu5ExpJ_oqi9MzCnNGqGWt55yrrDmeuFfLSC2TOUgxprd3-mYls7PtsWJZ_wXnbnnlyykbX07Z-HLKxlGDKJtMVNaLA6coWVv2HWNJJpVTQGFpbvJGyjGbtG_YMmdjalZSOSRjzojnyBIgmhcsqe9PNWo0NY9y08AnaU6mQDunmPTkKURXPinaba5EsS2nIXrDcKVcCQ1XypWQcadcCdofMVhShLTKvshEvyPaEOXkBSuaD4sjvEhnLeEtYnfdnS0R4CeilcFxotfDUaAl1zrp2KvhsYaMoNDQ61-EjKDM0K7fhIwqOz34VcioFX0uV_lJyAiKx5_ZHvW5FplaLurBOBgdPFvdZXMVbjejELi5TTQsOjXZsJDBuO1lKIrnNi5whZAu82tPQJllfkUTnxL1hCg3GeoxdbQTUdNQedUX016_JcH_21p1x84k_y9A415PQdWiys7pOahTiTd6VQRQoah19AMleN24kevGjVw3ruJ69gPXsze5nl3FgLLPe6nbnFhc2rLTmUEPJgtMrD8FlOopKZXR81Y1gWrVEFDbwCadc2OQ1s5KqjxjdIfzRm0hjEpRW4RG8bMrl4xo2ZPOXVy1rtR3mTnI84R7DfhOdln1O3TEShHB3EGpj6v0lMmCC_Uo9HE57pMQ66tUxdp9r2cFAjqyUQ78fbHABTPw5osZXLfc415AGCEGPzYMEHJLoRSPw7LrYd5O1Q4hgfSE48MdpB8f7jQKsHTFLYYYlxQ5uMWw0uji8m1cWNglQEefT39wE_NC606q2ZC8YOoZ-V6JdtKzMWMkIbF7bKECO3mi-silNPr-bM1OeNXie3s2oPWajE6QgZoI7UPgZBGKBNB1Pn9K60WfHcQvjMI52TrQ2MRtRVfvr2o7925LR8X7HtDPTPbVnw6-NmwKOfItm8EdUfWiDcQ5u_OAeLLAGeN5hbhro6PbSwmROjqEp7BbHxnbEZWE0t7jjaiNVkKcxejNO-B-Lh8_wbuFq3RevGwXW5WUb46VXmH5V1jmDVZ-gxRe1af41jLdGs3PGzJlaM6t3_iNzhMsbWB-AzM6LOugsKGock5L-Z5Tcd1SAYNkFSJc_9xBXget11p30HqFdu_LBiQSjHozot74qPY4rqNkqLZxVAQH69dCAUOCnEn1xHJdWxySZKMthrYxbHM2lSf5gH-rN4dxwO9sz42zHu7v49__gHMEP21dTlOsu5cZHqc3se5ebDidDrHuXmJ4nN7FurHg4m96F-vuxX_kb7pLT_4mF28_WVwzlHl1I3fgXazbw6Ifsy04mDRwZXXPUBRVVr8npAO1e0JQFbWWq6ISSjij5DOKcJsqr0GBCbd8sqFtkgg_aZcLdEIbkKQ_A0ERZA-gWFSCMigfQKLKbYMOsieQJLJs0EF5BaHmbs-Xq_TeH5Dya0py3mOfUdG51WdOCjlbxrfD4mhqiXR3Be2O78ruXfo9Z9N1bJRJHNtIyXcXeikt-a6Ej28JdQhJqb1WnK7BeQSN9Wxg57PB6J64veqgERPHBAS7LrBrryBH6z9-DUplxpQUXEtwW7DrHd0W7NJwe7Ar23n1ivpMGwgZlE8gSQsCgiLInkCS7gQERVA-gSSNCwjqIEkZA-mtN10kl9VCWdONoXV6RSpEZ046bxK1S-QVTSWkgH18ExHURM7B9faezrE_j3j5K7zD8fFe7xnv3U1AfUTWcw8D43Dt0OC_EkaizZnPCDKUIKKI1PIJ1XkqFSF982Gvz3eHoa-hRzp_VM3naHAv9EU0uJf5JhrcC30fDcaiP4wG9-I_1M6dn7Vzfw9qKYt6jvp1nxpZv5bBKMpkKHo6nHrZdbGBdBbyxPCbXOq5__FBc6QoZEDitzopgEuhOulfA8FSqjau37JeaL6rWEYgeiQioBelV7F-E70uvZkSTLg5eYIVHZY2NCe9FVQfZ462i_MqSi2VUGmx-v6NHpjiDwBVIGxBnTP-SRjt33P7Hn36rKVTCqTOYN2HhIUC5RrBtbfuQBsjBR7eFoGOa7f8-5qxNg7OzVdqp1vZB8Z9SOSAAZf-QhVTmGjCwi7NWGVqjyPNrXRMRCsPQJK9BmtAsrxg7kVQOoBEy6sRlEHmBJIUHyAogtJm3bdgdGLfKnzTSDV-VSmn86I1COgZEqHxa-1fBaPN7XIOOtI2t8HynbdHEhMgeHft2l1xdoRngFmp6fljJip8HnKJcQ5HQqtalZDto4HzDB4j1LbsXcccul2HwSy1sIbWbSPSqQ1VbsKMdj9z8govvsQrCh5uhosrTbWuUKGusGIJNYVFFyCcSZavUxSXF16l91Q4bgqvGsdUmK_sQnezGwlny6pfll8XcKXZ-s7ZT5RH0PJK0FezbGe-O9eVCi6Y21UzVqrmr2E9awt3aUtPyCZLho0W9zDwoSYweai5N2EqnE6F_aawPxU2m8LmUDjrZVfFkxcNetFV0Z2avGnxUpQxpZWxi3wdWP4qF2bJLHqVACT5hwAkeXoAJPlfGkjyv4Bqp5Pj8sdDZ9KvKVZNVUi4Xa2QLBXwFnRt95fiFnH7E7IdQbs-gSrPe8cF6gNHPPMVvaPDBOid8cZTUHu8-g0epDbsHDWOHmMXIC98PTBJeZmk7lmBUXjjhIGBfYVawXhZ0_Yd03mJo6atWY85Mden57Vv-u3XJYx_N5UOs9A8SYGaM-8KKd0wZDOs0bCPNXIfJxEenZ1QSwZUyaBsosYDT-oHn7El3wWDocy3wWAo9kUwGEp8HwxuBX8cDIbiP3Q3-fhlMBiq-2kwmCxFyVklgyure3FWgabp3Wq6dVjZwCBdMSkBYl9kOPZo5L8tICHflYFEyxnaw108oH9BZYr5CPVtoQeT0O-yD3mPJXXBlwNINO0bwclxlQrlTx6e9bTOUaYUhW_c0GSw-YFuBgscZimsvWTKvo1rgFSScHsQ-za751jTXX-vig2ao7mnXrj4j40jHwC-yt3FH1WDGlBUixngR0ME1sdiiPCmhLqtRrAxuMjqSdk7qZV7S4WnqPqaxAzvZbKC_BlP3inT2CtdZ9JGbiX6MqrRJkTfMc12O0IGd30u-Hss9uU5MA3aea8MNZRHdil7rbJb5b-l0NWSqwOPWDDlYk339Tvml1nLelI29bK_lcKo7eTWqrstGZeXbZb175iaSxn0RmRtJ_3SHy4X7XiHe0E7XvkK79w-uAXhXT90PZnjnfVuH1_SK9_g6fV2Jdz3gxL6zdq92vxCu_bH-1cnVFERHzjxBU7RFXpPxcNQ6B3kfAdIHzCYLZlSW_whEgA9ERN6ypZ2miZYDQMV9YZBdPoVRpR40pvSOhzcUN_bm_fXt55lFtpEDiYJbqdGuYnb_m0cdR9riOHDqailTM9sXURQiLfd347Fcq8mQNJq7UwYVQhq0eER1b4w3-O5gCTXRsh6qXwoNY0bA0m-lzCuDVBAkp8njGQMBSTpKyGcQJJ-HPQkwSBlHYLDNTiINFYeacTvcqQRlRJcmLJqHQIE5AynIOL4FzjpBU494_QX7SWcXbYiX8mSBhLm-0-WNamDRCMg6FmcQcjinHhKO4wBah1wNy--od3Z4wLOnnOp9Zuk8rM2imp5GMk-cqd3IJFgXkulksWW_hkBgJuBAUOQrz7k8kNVsKkIUZD47HQ1mk9Q17vVORIx2JR0xZ1_DvTEpjYjmtodMCtPhoYzVKWcxbJ87ljbqfo-FAFJfZehqL7LUHSDhxAWrrPMYlPUppPOnY8c1dshqg94L_P1YKSPajDM9Uu8sxoM9Hbu50QR3yik0JdXqFZVJKeRYX5vVk8vTOY49rQRpptRza6tpPiTtL_4g7S_-JO0v_jztL_4b0v7i_-mtL84p_2pl8DAdo8jvjRzmTgiSwpIUhfjuMVBAUnqIoAkdRFAkroYR-hIAUliL-o5SlHPG4rC3TgcJA5U0gcq6QOV9IFK-kAlfaCSPlBJHyg9LSsKaVkTC5hyC_3EHgSYkCExlStSSAik3gYF3W6RJHVO3yavLYNlHcanb4L5DSxtYFWHTZc0cFjcwERvdtQz6KKeoxWFbDUOEpd71Zd71Zd71Zd71Zd71Ze7kBTLQeJyF1Ji-XIXfLl05S6aHAdbRc_ka1vD4htIUAupZGnKYP_W8-MEfExJGxfLiwTdihEhL-9-yMDfv8u3x-q8ClFAMA-5AbGgsbbh50msDMQ70GEelVXSMpNpOWeWb9k-dn3_CJnFNtTPCKuAzzlicPk-WeXuQw52eJ9D9z5n9wsDmuB2dnC4dBC4vUKWjsZzzszaXiefljbqVRz0VpJccTMVLWYxtPgQ2DqNQftq2GCFZVTgVpBr2J9Dd6V8Ug3tWsi2t_FmxkCQrX-uYUr5E1yoF3I4oTrvPvf1--nXhiy0wVn7CXdyAHAyijqtuxabq2wY7iJ-_uwK3YPthvxIB-r3c4DPrQB36XgAwXOIMbpPeNjOx5vQjhKawGp7dOz5Fn24mJ-uIhcHJr3Ow__p13noiGalh9ecqHQogllbPu4hmdrNOWfo66d_Rz623JhF4aKkR5TUo5sRzZCFqSc9_pz0YHwaYQ4ZJIo4AEkiDkCSiEvj0mcZJIq4NAJUCkgScUkIniNICHpzkDhQUR-oqA9U1Acq6gMV9YGK-kBFfaCiPlBC_B1BetpC0nM4UtEHqugDVfSBKvpAFX2gij5QRR8oPQElCZkaKInaarvF9rTHO0IcomqH4FZZxuuogrCzOJND4WiCskNc11HAeQYqReqxslV2tldAmETk7MieGVZZmB4qd0hi-STc8d0RYq9WQehsclsrsFoZIyoNR911IrFA2ProE-BB-1nm0zqz4pWhM07YRVBOcb68oNmB_pDgbe1bh4k1bHWNlNOja9wqTDKPVFV1GKT76CXPwtNRgeyj7jR12ak6DCzl3PvBV7IzQ3UFxSM5IB8uraoUCin5edPVmvTJgS3SuEwYX79BBEVtDSFG7kfouaDu8EDhilaQ-7H1HVIPWAt6Aa9uiS-rde68nR2z3oNtk97HhuB7-_miBf9e7tGt96mrucdAv0pdzf32krepq1Aix29TV2F-h1v3u9RVKE6cnt-4TPOTkfT6zZ5e3bdv9kyrK8Z1Wa58mCOEjQDJYWBs1qo9IeAt2Z7XuzJ-UqEoGEh9G3iIy1LnFeQNgu0Isngb21Oj3m8L10uXDYZwpxFv3Hwn6IogHWoudKHZITg7wA6AfIo6SwFhVnojsfWWdQwyeIrLio7eFmVdIbyWdYXxdk6e25DVIWLwsMKf803GupUODqSIERQMkONXLx9Ba4vuikDUCB4Oc_ZwdLK2gHqw8bIg4brzskA6WG7Wdyifmh8HyDOuGDzOrjaViRR_0kFA6b-zJ6F0G5H2iiE0o0DuTBBSZDDvMpd2o6jn-TVY1qX4kbNxZwLuueIpshB06afhZClc0l4TKfWFJlL6OdGNktFqOmgipb7VRIpj-sUeaygZsiwp5CUFmU5dGSvv_c7WaeR3wqzUgSFehF1Mt4b4J4ldT_Oi21hIVZU4BEGWCH19qQi4wPYIosTtIyuAaicrm2l0RJW-9bWmwjbCcixnbWy9sqb42O5mh0yf0gWybncTOJ3tMKHSyaPZ55XIzZJOUrfUgSFK9JKUPnIKckoW7b8slmn_FQwykYpUJitlU0nQW8gWaBeT0uhJ4pSO4RlvJ5jHYMuCufhmiz-CeW-LlwjGstnY4jNZW45CHOnWresABHMpO8Fc8l4wlywI5pQo77wfZyASt8A1giitmycsQyyEFUHRW56305rsJmtwqandHNFEMydIIXNDmuhnSW1zxU3mc-3gT1uMpBIMXCyRCYsRkNLPhysRkBXBrPXQQMu-JueOsRsd8TnaGujmBRZGZF91o-OqelC7f2eH1J9POOkktTt-3mGi9kIB4-5zWUOq42ptyW0y0TF770rtrv0vctJqDxW8z0mDioZv5U1OWmUnO77KSavz6eXvctLqfGD6KwdLjbODRctJQykNs5Xi-s2vsp0vzI2Do0rnGpE87g9NgaLUZd2i7549CdkdMfaKWk7QbFHwUgxFrJP2y4NTpo0kd6HvIw0hjmnaIQhP9Ex1CE_OTG0dxs7yxhd2Ga5mqh_XVG4mibMnuKF-Uqv2Y5o5HVRc_zj4ft0WuE-z3sAwY3io3q_MPZhcbXhQQX7X-iRUF_qMJr2wuJoKXnn3yewJjnq14751zl3NareQobBt45Q_C_fkCK468T3TQGRwxfuSCWalErimyBBY2fiMgqvpk1wH05INnDsYoxWoHAg9WaByN8ayeQ4CDvG2LCtrWARBgHdPj26yA5G98X_poUysyVsS27u36RHrYNNjlUZBg82HaEnbv4jg1d0JI7BxAOAYqabeIKGBJOdBxxBcexINsIJEZs6aKHJzshpkQ2lDgmNI8TQyzNMSmubBxPVbXWXkNDbpiOE3jUqUBSxs15rUWGm-Oenj8VRZ6V1bu7nu5tAPrsqir9FqzPRh5xo3RdzYWPTNzjUeDZq7Mc0nhgwXc7YAsCZzAWBN5rIXK3s48SNEAmG0MHI6JwaEF5zYGh8YN0XdenBUgtI4audEGAPupPwjU_xnPUpLaSEOocOMQal7C1Rlz2g0Xb-m8-eNcQZ4enqagGDWel4k1iHmMbFug0jSQKwlRqMkFqyncIXf272L2toXLmpoyd77DM154X2GNp3IHZ3ZzWAAal8FuaHMt0FurOp9kBtLfB3kxnn7YZAbi__MBrtKfxfkxup-GOSG9bVx8cPi0uPV0IItCS1KMK04KUyBS1aNceDqlwsbpTBK0d5_DZQ36klnA5rqYdUYAR3WTVE9_jDYh6YgWMU_zOGii3lCkUINhJVseyA5ufkY7FtgRi2KBji4n1pcj90DxiZYAPT1YAF0dEviEG_AgZbSALB_MkyLxfC2bZRffYQ6hj5LHeUUz9gtNo6ixjOkzcSbuCm67nKMPogLQIhSsGWwxXsXP2Fr4oy3iZ-QOd63fYthNAyIW9g7YrGJW9hb4ixxCwiJWNucSTwkgkzavXB5W1cokgB_6-0GSqACrsZRRwv7REDr8otIPmKdrH6o7eAeh17qrlPo3M6md4JzdRqalVtA7N46cJ0unzbZV2N-NUnlhkNWQVB8ogIBpQlrBjMH6ZkAZL1otZNR27V_O0ZadvaKoJKPEmt0R48za7_MvYXh4_SFqbFQ8nWQdeAe47x3ra-TqqwzL5KqJrrHHLBOdZsDhg4HF5vDIddPqKkQfwLMa9L9CRzTC74JdBM4SJT3mvP3wogcg5UG926SvL8EvHp_Own0_CTV5TFQ9i4Pl8HlsfN53MQ6kur0mAZugciDTx0RgHeOEiPmMUq8QTRrzcc4OODpcXABob2d-vzjIQnzNhXhbjlXFhARFO2r7ttocOeiLGPB2PX9dqj3YVwo9FUYFyv6KoyLhX4QxoWiPw3jYvEfuhDaHZlvwrgWRifoU7wz62GSd975TkQDbQzcsYYUkUjr59XDvQnQsxCXT1PqEwVxMTrBeD5au2cLYONID_8uXboI98fSknArIQNMLzcVujrGaVzkmD617Jvk16ekZhwHl9_J5qoX7hSrRqncLEUIB63VbesVUHjzn1UaWd1-_eZi5sv0NkyigWVSzK_JUfJAA4FaowCi4cWcz6ReP7uuHch7P65p3ICtwvzDlkifJXtyO8AGKcIGTYy97yx3YQvDFWQXqDbGIV7uroArMjkRCsqVN2_y7TjuNufPRbKvPHlFwU0i8GSOjzGdMHBMhLv6GIixntIu6rLBUEaAortDK2EwWHogtWUeekxi-t7DELwcvCszyvXn1C0vt3BfszaKfp-6MhZUpQjedKYHZ3ahJcCtDWnE5LihICuC1vTWXjk8miS-wIS3fdjQTmm249zumUJjCcThpWF36nEel1d70Fu8q9nkuxqq6geaidk_-qGcLip_SDtg3QFJkqZqUbS6FSRkIoH79ihE9-mhvAnwrKMbl60SoUEJMIlglpoJC0aBEcpRpszVEypoArBJ6R_5pFBIUiFVhchzsdQnYfSBnDq7XBcY6eYKnsjBAfMbWNrA6gZmVPk71XvASS9wqo7jXaHtuRknvngGmOPWU0wPCKVZtLaEz6UrXCsocZI7rNgknX1Dydh5KzlMCqe71ZpyAzzRXRBAtBPK7SVA0gB1C4iGbyFOT4X6LdRIULznHddVt8pRtwrj3rKmHbH8aVChcLstGhZlbSHtwX4PNhrYsLqHgKTt8xMOzwLnFdWpItQAlV50MB-oriAGuhUkTSRIIaVA16gIkoJUtC6rViVCVgHOKmIQsG1jv6rkfcZB7DeffJVxAFV9kXEAJb7POGgFf5xxAMV_6C6I5cuMA6ju64wDkMnRwBuosb3wg5pVu24OVfNJswKy5VpnpTyXs10TSs-r2OgEbQg1lRi7Ztk_Dbc_11IZPSqSKTDOQIlC2WlXFXt9NtojpPQ-OBRsWOdm9WntIdGMFpoUJraJqCvTWFOI3GeKIQ_dQl3C6JpAdLpGAsMhw8oG5s4aCDR-j1POWkqr634D_FFSuHZipwUKugIK1ei6O2PMDDotGvAW8jp0lsdwjTQO6ogL4HfigmZRBwZffcEdXt7Ae8KzhrA2oCsSreVrDQMhKlXgTdnYCNcjAUT0eM5hukTvHhWKFN8glQ0SCveJUgdjZCDqkYGw4i26CsFz6xg5BacwnLGMCCabVC9Qg4CEUN_dP3RbwDZy7cWZ-53CXwUnZXZhiLgTM-10o0R3oifhgJsDfTLALmYc4r1Bh8gOcyxElxMRgys2jLSjfg5kNYjb5VRHiBICq5YhljNiOSCiowV6nZunpSNPXkgmqrw3DJEixI6gU0PGmZ7jN3VtoqNstHXEmLUjFCu-wnJfYEXEkqtSwUUBwy5vec20CQL_h-RnrQrDlx6jUQPthGQDjDW0gooOwvwBhZ4IWdMRSoMkCKgQvbJ9srUrE_zy33LrmL8T1TJtey-43QCM2tyjIM_OuxQEpbMD8w5oX2iQjfwJKW-QUKcarcf57JCgQvIestPhlJYvLZK1PGfZZDbpEZ4__pf5q_2r_cS_mr-CODQrctPHkgIAEcXJz2IzLXcaDRTQ9FoXZG0SisuwvIHZs4YJg7vH0eNiXQsFnJ2vDFWwxA-lrToa7LIjwnpOqiPkdwinKnRFMylJC7yCE1xXhPuS2da_0WOTdpasq6kwBlFQUwdOUHFQAYWWDj2Oa6mF8QzNg0WxFlcZarETkVkTRh6zB2tR1rSEUZkjjjKrtYGGzSpNxJn2O-irE97QVxPduLoOOquFaoGncZ7iGFB5PqkQHa9WGacSf0VldeVFv4pTRXsNBpmToj-kMOsPqwoCONGccUhjqLUaaFMkjSJJV-oFOociCOO_Cj0RsgZ0wamW-zsUX2Q25f6uxfvMJqjou8wmKPSTzKZW9MeZTVD8h67KXN9mNqHylz3cc4MDdAzYUp0v99igoHflHiOkH3cB2ywFbNEfmfk5DArZnL9g1SrOylbtwQ-Z3Ta-2NHsEs9lWGZpsRDXpV0V4rpqUyiO0g6iYE6TRyFqdDer0d2sRnfXsZfLbh2aeRO2zZuwbd6EbfMmbItTsFMhcRa3OC_CtlkP28JLlVdVdwJPqC2HF5RRGx4pu8Ztc8yf5xlwGz4ps_5BzFbAYPFanQLqam2ANtoo3L55RNDV1ZxeIej6aO7JMQq8HuDm1ADlGDTv4gl-asBGIYYh8iQDXMLCTCQZqelnubSwE7lYA1QtRPBn1RpGTMbh_GAE-VGZzeOJLO48NStS3SOZrfpOZUIPcc9IlDURJEnFRwakaOHZT60RkxknLLlL_aj_UoeZNP0q1MBxiKIfJrGjesI9RVRy-JlDPPsJdaXJuEHVaUYrIWIvapiHaVZGsx7Tz3pMH1e0Rk-EqIH7vAbuGWQ-vtR6WGigkphU7DuWm9MbSw-DcB9-_z4Tne4P40bZDB3bG2bjqvY5yPPbf2zOcYQk2nnIRIyugXoJm1zQ5z8QO837tMxLmh0VaC5pjFKhUp-nq8rHuftNrF2ZuU22fpKzbocJ3URMoX5nSe3zKKCq3q-klFV1WVPvhcllZf8GJ2-RQ89cUy89qDJr6qXHoRbNOFAEdsIAVetSQaU1O9WaVs50Z7O0gIFtn9Wd-u62WBRFaSzRepWxpJpheRFyxw5vtMfSrvkJmamPv_2zkYj6SOc491S7_r3w7-iNKE9UhB4c8JTO-j0q34vyHe_AYaOIcrFRW4TaBF4dZLRPWqIaxSEqgIQTX-C4HU6hzV1i2iDBJxILWB4L3FtqIKnoMauix6yKHrMqa_yJ0eMQYFE1LNytEO6GuVbw7iRcdY2laFbUZSUlTKRkSJaSJlyt8uAP52YVnNoUv4eEKB45jaUwxtrPT6mOirpEjsbY1X4UZWWO1XjOb2b3hGGdmdkibZngkKB9E3Kva1HyrwDsD64Tp6JR8FwJhWm9J-x2mmQKUSN862wsLBpHbsei6ybW1HE2sbK6iZVVIVaG8atWbp9FRVdEIFy9WuDq3AHtCXDOnANNHnvbzUj0x6M5WnugQLhja2D1JSljJVgV-UPsgXGgG7lu5eEX2V6EAd5bnoCkWZ6VTaNipuEi1EJGlAkIpmBly2Frk-K62EmbmufWCFbjVNsCluNX3agcrOOQhtUQiQGo5WEBYlpavdipNTBM0e5sY0CoaQZqpfFZKbUL7NOpHzgg0UpUJhFd9aBN1YM2VQ_a1DUAw-iJkDXQ08ILzjy28FfJ5VDm2-RyrOp9cjmW-Dq5HAr-NLkci_8sYuPupfpNcjlW921yubWtdPM_hPqs7PYpjU-gqGAlgjrkDD0NKcHHZXdS2ji0QA4AQb37fB5owQlJDjpIbdGs2UCa1DLjA6mfHbkETccZiLpUURFyBgI3VcxlN8r4USCdF_p9mRRaa5yy1YfitDaYwsTWUoSZ8KQ-zXNNIacTf9osU4xTXEmbX6I-YUN2qhk2ZY9zjvNgc17QEVU9KM9gYOFfPAAt_NvFT6382cgH3dC125GLG4hZVBB7z7qGCHpeHzqm59EVjXj-JV56iVe_wnMv6z3j-W_wqEKq4JmXeJ5oKMd-vMCrX-Gd25e-6a8-fqj548CkreaPwxL6dU_U0EhMtE0oIrV6QgXlHOrVHE4IVhxO0DdF5e5IPMalgNejm4W2YHf4srOSneavdaWDOY0O9ihaIJ54yY_2zIYn54Jj3iJ11ufsGzwmqyVFGDevCFKDP8hCdJCgP-OqUxshQtYoU2o3Qzq4GfKSAXDxFCiC_fuffuHQ7V77Bdnd_BMY20Prs44qgpF-3AsoK5wsAyFkxyFC_km8Xha0E-sV35GTwUTVtP081qJFMZBQ29a_r2ItdcsdMwvGGmm4z_1uzjW6DY5ARLmtaCIj3QMBvNn2Izh7Hg7L5ig7YHEeZQLQU_G6TBijOeUm0PpI-6tMJU7NV6QB7o4XgsNm5EwDdXqSC9i23fr_M91CmiOK7TPCvIV8gpnU7JjhY0Gbc0_NfWmPuYOPcJVY39vLmHE-o98S2nnTOpYcg1-kxLlYxEfzdgTQsXiop3sWx5w6xzirChKvmgaydr527Fohd2wZHwFmyPiinFiCvvp7sV9HK_HrN_7gksUrvmD8aotwu2Q-wT2Rdri4k0Ty7hygj7uxcvg0IcCQBqk7m9TXT0zFP3V6kdRza1W86st5wupy_c7RvpDq7br_9cgUjI-XZzyqgmUJVqj5BS304wloT8oExfW1iKj32FAs3Ej5l37JlwLuXZMLB4DaFvK_b0-p1fdN-ox0IiuSXNgFmpUt03dsrRrrQ64nOP7c4xL8LlsbCn2VrY0VfZWtjYV-kK0NRX-arY3Ff-j7u_SMX--ytZENunElpHS5tXP0xNYEyoei3U8fVgHK6ffIjVLJici4N1O8E5P2Ei999HNJmXI-Uc4yAQ4nNfOOb6RbH3lV6nQS6rAepBsf_EM9q3Sb5ijiHO1GamBthmtM9zRm9lVF9k1FVqoogBCBZUG_hOVLlnFcTSBiOabw3fLvXfw6OJTGrSVHKlFgeQOzqkifaB9w8kY1aFzf1a3QByT_QjNw8Y2S4Ypp91tv63OvkGy711LUMUZ9e6zR9rNWcw-DrtXQEX2h-1xV_rol1E7zmXEkvWdHp2s9M5Kq83BErvGgleIq3LDSfHPx1--wWDINZ-TOT0iVInmGNPvECkVNE6pgQ13L7y2mN9RyQ0whJA-o3cCUUQulOtcvedi2g9SRpFYSbxs08FHenhx-9Og3WUNd_xT10hAJ7m-4EnZgB5yiZt2WD3l6geifrsf_Uf9MhPOseqmbL5hFceHXmGMHKTHHDk96Uelm2KVWXaPxnkueTf0bInVDpNrGNTy-ANO257Xr7M1N7a9_-Od__b__N_8fzN__3T_-97_85__xv_39P_zN3ywt3Cg9BEEUrmR0uQD3pheUVY0-Zaoy0klog7bxKPh66tj-_S4691yP7OtFb_d6re402QdljS-JQz26suZ7rGunQ81Ym9HQlDVvdr2adsOpOf5Nc7zUHL5uzsNn5OFDnQ2WH_3ily9p-VLlUqtOCNO3fq_Kd6PokN7ouqD3G1jawOoGZs66I3R6j5Ne4NSje8rnN5qjjy-UUG9fkQov9Esf3miOd4U7zXH08Kxf3j18oTlexH6dvWYrlqQ97ml1_XFFUzXIGVXWIb0TVT9BS_PxNWYRNS8hTxNRZ9VPisY21En1m5U-oLdospJnXqx7QZJ6vWh98AzGO1XOx_xsDWvsJ9YxhkSb8z12zLU5X-fvmex_4buZvoPLK7ijI3HJIGxlvs4ghKq-yCCEEt9nEAYnehHfZhBC8R96EYP7MoMQqvvhg7hYXFKNAt6kqOjJoehFxx2AoiIUeoBvo4eH8obIRqMkCKLSQjrI1acwjkXKmkofNVWX6SS0fu2oH2NrQY-thVextfAuthaU2No0z6fK3LvK3KGynn2wU1dnrM3Max5ImDv6JS5fyvLFyVoi1LV-L4r22AjJml4jpsDKBubOmh00dI-zC042rSeYF8pYSFskmHD_RvkL9oUeGV5FTMM-Yor9e-OnDOmNtnn38KxH3j3c-SlHF1_QehfJvRr26-TNnHEkfXRHp2ujM5Kqi3LEWRMFnCCqgoIPEjB3nj1UGkN-64QM9lR91_aCvSeC-fh-a06-kFs8OVFkmLhFMwztDd7qPsaPS9mIXhiUKHOYo8z43U3fbWOjEU6d9GQ2ZNXxENmMb8KT8RCejGt4NAF_j_0YEPL2DsGLG9dvdc3-4u3dqBoEQRQ4ZEC4qhFPocjeS1VudhLqEB7kM8dSZ-sgeGeszThofiI-42uWFqTtxHGGl38Sk3yicJ5qgrEzuPDQM8JczwDiAP3MDO4KwYMVe2wTk4NiHG_YLjSee6dFV1icwpczuZZthOPbxXfcBDfjJrgZXwQwsckb1SC-iQPGV3HAGFKTdu5TXc2_-it4DAmEcMgfk9tDRTql-4Ve2-MsEpmraZ_KiSDbb3dJLw4DKfYEqOY1at5iFkp0I6DQEyFWj0iYBBXDNglKBqNeIEMDY81PUjO8_kk3FHsWlG3B3xM_mTHoBi6UJ5i1qpZzDscLOHbFHL2pRCUsdAjSuITLMtkL9DutZHWkJH90pKwZWa3QdxlZUNF3GVlQ6CcZWa3ojzOyoPgPfSnJv87IotXVNH-5XSqLNIIblrCN8Rd_xRKLukm0DzRP0Pa3PgGSfYPUUtSPWEXGgouheqeWT0t__vQ7qEYD1m_TRViWd10DWUmnSWq4j2Og5GbUKUKpH7EzjIi9zAu4aW-thwRMmVKX-k2DCW8eRvbHQHWFZCyj1nezotAz0xewplsthQUoKTvpeKB2NLREvzzDlCTcqkIgziZsKl54j2J0FDjs2Kg8h42GMtKBaQc0x5Luuati0XV43Scks0Hqxngq6bly7FJEPjm52ZhO1CjXcC3BReP8DV00vWXcboK3Nhi6ddqROnosJVXynSrODK7en4mTcigPlyqKCOkdQjw0QaDQD6Wk-Rwtvy4FG8ngIh3lttOporC9BhQ34eaST-yz10_H4LFkmL88Dhkv3gpYK4__ITZSlizpsUoXDEcVTInCo6sZusBM6n4rDmA84YZXUJOTSU1N_5QUh5TPzavq0vzJ-Y3gz-Q0nCjgKRo8rixJ5CzFWWSkg5qQ8ajLGemkACytXmBLx4n4HnVosKyDRIWgd20Vi7m_0qVqBNkxqaticE8Ar2BWrIiuAIXDoysI4NZAu9U3oIkNKYjeGtaaICgmKBobLUn0tsI6ZCtxoZs7iQtdleUmdFEBYmRhJy-z2yAhj8499Cbz8NwPxihw5f09Dj-V30gZ7IXO23OPymoIGCLcMX-oJ7049wht3qAGyzofPt0HEtjqMJHNDeXOUImbuXOHQ-Crs3Vk2xkSae7z4r6kYeGVcLSblwSEVubrBASo6osEBCjxfQJCK_jjBAQo_kOj-RYTXyUgQHXfv4_bSufZ5G5ftiZ3IX6hjeQt6FdSJGHh54ck4EGWlu5bfoGlGOYoVefWLqCd6T0aosKiDhMlbh-AVSAWEpMQpWnpjvxVfgHMKsKygYsuBkEYFatJMth3apmtjIPW7WRco6KIsVZeA6IbfSfjStggdZuwWPvYYyF_gr9vVkXbjWjZGo4lNuCODtp-Mk5bvSjTYFBUmVcwQKbBw5vyG6GKw3ZCWG27LlQLBtI2ry9gQyLBUi8egDW6s76gVS-srzECAfz2LT3QEiMM4uHUGCu-POGN-xWHFijJgkGmYDGjTKNEJDjM4jDMIuXp1O9dWvcruATghvT03KOeAoVZh9dcrDDYJLLSX9XzDBwD4gtzVJOi7F2FQEZyFSJbaSgST6pehaiutaq61urZb1bP3rfqzyhn71vt3jf6ced1a_VqwJ1Lrr7xttU3frvq3yDt_XbFLAuYCH0r4JQs4eBYX9r2E6H06QP3hFA_XY1kI2u4luAi739DF2WAjNv9f60Nhu4HnZdX_wauy4qaDuX3DwnBmJ_gm_oPfkxcHScE3c1Y_Qnh5Kis9R2COyFsenHyplavIHRhWr08EwPByFMxEHqs5yyOG25FccxAlYHE0uYkqQeqf4_aRtmc5T_smszF_-SCDZQ7W0esdBTRAFJdpNdvewklXjLPVgpJ6bHC86UNxhQo0ymoGZCYPzAGljhwQsT8gxMeX4Lr8oBwe53POTQb15t4dBss4XYo9FW4HSv6KtyOhX4QboeiPw23Y_GfeQ6u0m_D7bYJNm_QLbt-q6u6BUlbgJHj8omZsRwkZYYBaEoe4TBmNIPyCcNkQbOZvhZ-F_JEj2ehgRLAio6AEQcONXjWEyBnDGdPy0sDuORFgBuUvWneYfbGJ3x0fnOtMpAm1ypDKX6bHxsnCaK9M4JNj2qhHUi4HAkJclCFFRh29zpzHHyiYr6qz6wzsnyfHhf50286noa8rQcOS-QWZrgYUGm3gCC9qwFM_iIf2vNWl76XnjvjPKxv-9hwBtKguNuHodyPu-C9xfO14TAAA3H2IHEMsTZczxZdBji6-F3JxeVwKXzFKTe3nrjerdVWqrX6wrLwJIrfLCybAGeDYnZ3TXIc7SpJ3iDMFlWwoEmu7tb6OuoUSKdKBrbRZkuSUZ7e2SHLehlWCpOGk8LFsRSI94RaqQHTAIk404Tw68vttHK768XOSxYgAabmiZNXYM1pePcouG33tpEGCviHLpzY3oaNpr8d266riPQVWgnvtncZqrNvUeFcw7XNb-0usDMQrY1dpQXtwqWjzjaHeqDMt6EerOp9qAdLfB3qgYI_DfVg8R8qbC59F-rB6r4O9YB4cM3VZXCT9u-p8XpFqjgDxTaSp5F2wdALODl9FEvbWpDCRnbNvejf5V50cKevibzRzOXFyqX6m84Wqx9g5-cHeGvkMZ1opEhGZNbIKTUb6ac1gZODfsswc4JNckIi_QLF2x2KUVFQQ4B7lOLyCY5sEPHpcuLSgV4O26HzfdmGVTOLxFHXWABE2MxEBRBpzUCBjlx2SHrYysW9bzfF5EJ0lNdryI4gg7_xFS74GxXc4W90rMHt8Vbnrj4d2yviys09oo7WCqhTY0HBxmv17kyfXC0Vhu4JvTvHpKELXBwiv_J4znH9ZuZdD28yes_O9z0PJ_r-JuO96ZCDt1cZke37yyZ4xHR2n3Lfcgr-j8crUW_efz-Eu5FySCAaRsDessra8BCIovhwEhXPqdwi6E4qqY_0k6h4iUriVB7V4CpwUalZFWWukoEkkvOW9vEeCXu_xMt0BNBFoAhxBaFjprqpBOohUGRoFY_HKdenmjIVQq7tx_nXhaNr6wMZn2--khQXuTJh-A1GfIkh1dJcndBxZ5_zNOmJdGVOyVG8MPBkjxHgZQ0PzvoI9f7eVeyNRnAstkkBfSbw9uMJG-ZeY_CcBilMtK1bWS6P67Y-tgBfAHzwKjRtaXR6N1j-3VhVMlQ7cho1FJ5-zfxE_5OHw8ftfCSITW_4sUn8nOTnJwJdvMRL5QVfFGvReiiWN6u9RsUFPraN3vjOWkB0BNqM9bTn3K1mPM44Zh3B5Tt_2oK4k2g3xuQsCF7wN9E5WGDzRAgw_b3taQgZCsYScGHJhxAVqMcBF6GOick7-7wt2ktKPCMSogT-80RjBiCAMUf2QLB7fhZypXhBxEN3bwC_MCB3pjah9brDy7rDhpcudYd93e2s6UITpxA4Vchz02QWAngq-57wVHGArGuul2CwsR38fTsMKhpvly4t-PSnsa6m5hCWjgPMSvTxZcuOsOh25IsRJZWxhZBoqUP_X63AKhFcaL0ayaMIgwF_tw6oCKtmWZ5au3ldqnwDDh7qPl7gRCzuSOSUNi8NzTi_6YNFPjWU4p8z7TZcZoihdmwkSVGysI7koKnoHo-Y30DkZVzed6TA7dOd0PPo9j5tszS_vzlulnZLPtq5Aik0w7vBQzO8F8zBGx1rweYeBaxhj6O_PcmnMcg3f_CZtAoSnUyLKgufz_yhd3UsM8rBmzl9dAixEWxaw1Qbn1m7VCZMLqlJn9_84cf9-Qznpa3rHF86w9JxYZrPaHWLZilSbdYDbWls_oXG0OJzGby7vSn9CqEcmHehgioTnyuEXFfsNRCyH0t1r0h9f8T10b5X9wmxXSTFuIUUpo3C6XEEqe_84e4QQYJ_kdUlg5IOqisI0vZ9KswU_tNPJsyQe_Qd2ZZJ0Nn5xk39_r4DxnpoDXdoYpq_sj9TTVwiidHGpGj0KClD5k0iO1fqKgUL_SSbWmseRdGtDdzVSbOB-i4EjC1Pxp5seHJCc7MfwVcnLrxBsuJdS3yCjywYx_BRFsQ6p1n2GuI00XYYxmbX_WW6l74LMy60Vph0oanLvO-5ecLLGPdsOsUtGh-WkVrPvluiPrrB3BNJd0LbN5n6fBNNXxGIlu8L4HPXjXDtjwLm1a6F6TBWJo9SeOSRI85uns2Y2LK4hyaOJE-4_YLj1BG-42jYQyENC0FSSlCgXEgEOVU0NIIyqOhSI-chG-COjrYQstUAYQLAwTNSAgRMB4QFkITKZ7mU6X1MskaZhcNmHSacCuswO0iL3CcnxTaa0DDPImyyYzLekha4n7AzsGynXjyGqLGOIYU3SPN4bChJ5xkXVHtCRV4qThbhkeqA0oGqikXE7U0VbVkYQRNGDMsuTk62goIKxvwZ6WGWaR0lwQ5QVsnqdJ2mg7dXn-EXeEL_dXqSdacvmgM2XzfrRAlLRxpDYfVIJpe8gBTjbNnc6bPVfFp_pzzWHPCjWsIZURnmFBQkVCpY3f0jr3tBX2guZSUMy9fMrFbA91mtQIEJS3ISqmLEDlZ5YocswCyccOqCQweSVS3I6l3tJi7oUguyiEbXHFijudx5rsV-UogkJJSlBM5AZ0IECeevGUFRDWgEZVAWQMDmSj8-pC7V4jfGXUdaXHIS0t41Z_QWkTXMmyMhzG61BeHoW8M27Gy08sL_Vl7438rZ_1aI2-w0T6qNxmfqjHZyuplt05YJE00jPmV7lBfusoJvJe0NrPLOq1beedXK3quGyUJIbToaaT-Bph-tWJZgYTbRnpaFu9AFtHEbOuBi0tGMCvlGWG-CyX6MrKZ_Bvo9Zfl7S1lYvzdjzSqEJABkU3RAH7Bx_JGbkH1h4vdA8SXmWnR_XtH9eUXw56H5VXn-CrGNJggaR2iFVeLTY1bYAEzmVBWOruBWrjvvXEeKb5DKjoHzpkiWXafzUFCcjqCg1bT44yg0vzLhOpHJOkP2xYdtBagWj1o_xVl7oKL09hGWV0_5FHzOJIRyQhAngrK3-sJdWXV35cApRzFXiatPXcZHwdUac0Yre_nG2qSZmvN61jyt02rkDkdhVZ8Nyr5w9CyeZTgX2NYw27VFWMF7J6rY3GWpbxWAeV7V5b5H0WZqXfNHyV_3jlq-8g_-XKC1-G0ZlJhl6PeskBZTznG4mu5gWyaGLou01R49geFIbK3c_xiyZDlU9qdONf8ZKQPV-XY0sNaPMZX1yD_Xs9EOedYfax5ZGAz4Qer6zQo7B4QyolDZmgTAJJKDmT2mIJIJgItkAOxMK6hyL3SDyVurCZqnnQljPEFu09inOpmxhbRGUwy5xWRT9IZsRAZWtMfJJ7GCE7Rn4VDZSWxAfSezSK1zGUaB0jKQMs40lCc21Jf3CS3v0ObldnTHbTHJtg-Gb3uBgQUTtUQCDmLsLVW67QcXk9xDOJRmXOrBAOvlIp06pOXtqffHdSbiyn1liS2mQRxSy3ibdw41xHnjUMPW7Hx5vNd4fmzneMO97Lat7EGQF620UtVwiX3j5xYu9Ji_XI1oC56jjX3VLHYOuI9XXH_8L_NX-1f7iX81f203K7TzFQKVONWp0EfIuwoIHWRvVspEY0pssNSlJnm5kfpeENkXmXXBaolvE5rdpXkgG5UaTsH7JDy9LRRHaQjhwsrgUAx5ZChftbqbb-Cc3YW9z1sc3X3JV40mFPnCUZJM-No5Sk77Kouvz9lOsvAlpOaQ8FW0yRVRR0NYS2pYyZ0atayovexXx2tdVUehbl-5MnEYjmgHN2tzPzKW6H7BHZ8TlAWfKVKg7R7Bdw2F5qZ1HHAe9m7NXsVgn1uGJMDznCIB1OYn5aTojZYc4oddNVYfzN39qtn93aEL8KGyfGqnA-GT56cDL3OqtlP9y9lALBBYAX8XmA95wdFALEEuHLirqMvBsrYESBE8nfIcJ6v3ycnrc32OcfJiOQRazL8sBiffsBg5vPbcllXL00R2eHE6LYpF6yhq7_sXbkdyvbfn_pgoFI-ONPg-Z3of7tEuKfBr8diLl9iPuV7qqVg-rOVLO8J768Y3R_nm-F87WBAcXvHOT2_kQqCPogQnKOCTXT_9bifzbSENRLq0QjxBtj1cgQteQ4PDFVM1q0Tg5H6r9PhwQHoZOa3BB4Tl_fMqnmcl5_M-rLGMeFNL-aBNtau9eU4NTlVNGEVoDJu0oSGtoEJAKPadI6UkT2Zo2tJJfZPoEFkpEKFixk2e56WBWvLr1MajfqBQE1rqZ8ft1FgmE6l1C33hbjrLrVwPqCY_p2mq_-QYSXBwiAcxpVIBQz6mAuWc5j6E_ix6wmIu2MVjEj8Mf7GX53rlyEqDzCEL1NtXbMDbV2zA25kN0DNWK1jcVd6-4xHennaft9ru4y1iO51zE98z8DRuMpMgGGBG-nZwLtFPzRtBL3WGVeC7q2GBCK_eGpMIzOHR8whmacjPirid0vdh8nqrDLApDKmtXTrhGSBzAO7rq0DzzluUUbiZvb8vuoj1IyRiGqnsvIUlArTm-xnZVnrlJL4kKEsJ8o9mInOrcO3pVRzaRJtOwUEBs0X0gPtxWzLZQbj0u7UwtJuk_gzfd2i66OMYQ8d2-1Zov9Ej3tERhpo1-r57WfrqRQCuEGwLtVhvP_xezHYNGLTcQuSTroML0oou6yC4exrdZh1MZed1IBFYRiPy0fAcRDvbvTmXPH0uKrtFRbr1CGo3VQnlrtVMkNQh1otk8RTZUrJCtXCqdgJ7twdHDmaD4fTBcNPMs3K_q1rud-WDCCbR1RxUB55lUskypstkDHoz4NhgA6nqgG92gf4sMPAX35ev37I6gAgN8Jnexn3vwUA7tGxRODpNwB0CWzB2XW9WPuCINGAsiRDT8WhEi4JKGgurg9Je9dhOb1UMUWpFp7W51yhcdosCi5UGxZcXYzkqU_-K3tQidV06gmGbQIsgDy4L8fPcxnCXvyk8rDKldp9Sec52__M_V_v3__5_-f_-i_u7_-cv__wPf0PJRNfuWDLhc9-qtiPzv__7__rfaNFkn6LZfWq-ZfdS1P76x3_3d__0T__v3_7TP_6nv_uf_uM__M3fPKvR4tKAc9XGfspN81IdCQH_mIOPIR_p0C9xhA6PB3iR4X17xH4Rr3yvObTAb64-h0ZIKCi2Ust6nl6_wNWZ2pNbVMt8bjVCxky_h86jZIUSaa0WJe6gBNc7UG0o9dTdd9pQLKRbjI2xdj48cWoF7IDWit-VDoMl24xvimT13TmKrbs6zQdVVth05XQGmPsFt7UCu3BkUSW7X3TJHtYULAdxwTTunOui7nZQ6soxjtxzw-UQ40tXsznA_WFVkQo4O85cZnJGD2p47u9RzAp6rguk69OtlKBot0Kron0VUBTtHE-K9lR2VrAkAsvwbRZ7NmOxG9g72cKjfSV_6k2IyHuwfgWUplokCUw0rLngrGEh_NrWWHKGBKiSK1cdnLdgbxi4uwzaK6VPgi64DGwdDoPO-8GIaMN6Wxr3vm90APKcpfSlfCI5HFkEUuDjzjX_ojcHR4MLsEVAbMofm3G3PVfximiuodFLgjoy8o422SVgXbhvi1O3dEsFZhCwbgq6CSnIEpD0Nh2A2L5C-2cmOJs_nermxb5BX0KCvV2EhwVwCxdMH5v3dis09jbaXQVS3GrkZhfu_YugYlyVfDKuprLz3pcI0JrdxsguXjCyp49mIkP3SzH105Qi__HO8qdNlq3TstvpDiqXmfqUv_alGeXdwOxbqlwNenb2fXtk5lUtO4vUBMZHSdT4wH1TPN9eZAaWPdZx-x4jMbi-xzyMoQXk0Pft47zrrFNA8OT1N-QLAhrcX1qoFloUJZHDRS0z0Q0uKpEgRCrm3BG-UO3qBwGI9ohjqHmG9LyNXkUXYIZVhJBT3oZABwaCQdwS2Kw9F0kPflZ0eZ5QwhlFibLS0fjeA4h9XJhXDRogc0C0vKcwiuyzFSYppSfB1DnzgWtvZ3fMhEJEc4fEDgFlYSY7KwsdXqBaLvMR7O0eHBgY3SM9idoZYV9VRZHmcF0Rr_tXM3FidOuw7p-8TITK1kKsJ4U_Gu0FxGiaM5R-WV4-BE00mjqcOsxRAWUmlyu1QqEeEQNp-NXSAguBla5mKSS-IoQwyTxDWOGVVdqZOjV1-PoqHY35raThh7IEbb5bYcEiyR36tZOIVuL6bbrkgCLMNzIpCAKFTBEkZRBHQVAGl1KTnjeKSnoesjdYuivfwzUhABIHgDJ1fQ-oii3KVDQ5gSI2lKn5o5nIDFF_tdL2YSDq-vXd9YVLvKodDNlIjjhUPGER2HuRRYCvFXHMHO-AcAkOlKEDTcH9JCNZx396OAXhZoaDL5dUjxDkJDauXKcNrC0z17Fu4TrAOSy5F5v5bRAicoxRat38Vn96DGEi07CC8xRsiGhNsyFSFEM32B7qzEJIXCGVjJIQ7rmpyRZJvG9031okc9nJIhEJED0h2uqaj7WaTzA8koDt5ihDXnfIrQ0ApOkJC9lJTxglI1TLFIEBLlvwrUYQMOgJF9jC4RrzebyPVIGGUXuSAKhaBGIeZ1ZTA3B-dfhBvYcKdhoA1LFHKRpKeZTa6NrxCprq3b8tq4ABDhq9QAW63gGD-y3KNiBt1XrE8S9w0gucesahAl3FkXIsZ5yysSKi-86KiHYaDab7k5bjngcx5JqmagijwRna68BYlQ5XdGAO15-YhQp0bzdQ2D3zDp3YRFCihze9pwhKc6YCVAhEIKQOjRi1Q0_Uvo126Ou8UCQFrWH5oWXJCIKK1xsq67S0wScN1FK0eFBUG5qn-ZBYVGgkSHrvtUb2bgjqPkp8WprZCH5jB3jBDuAwz3Rb9GhdnYGnGl36VPveowVKl_fp82iG1n1MUnxnMCo2NFSfPz7Z12461DG9EQP7HQxG3ayC4g71-_fEcdh1-P69cKxfhyuPUPP6N3DlleLOYTyaKRsrHFqx5TKtIXuUpKGgLj9aO2npfRhlJXwph3p1cLOmHbRwQAyqzx9Aopk3l5rNvLBz53dJF3rOwlf-Mig3jD3Ux0Pa6-Nh0cdRtw6atz-Gk7d_Ljvr1hIBWrPbGKjBCwbq9NFMZAhvDSTLhFtRQc9NIaVW9hg2llIQLCUOm9gq6pXBLmp2yw15hvaJOtryKe399IxoT16Iq-bv_4d__O9_-ae__b_-8p__j_-VsICgaNgdrmjwnUWE8oJFBFVNb4cpYwR_I9mLUXPBx7i64GGXxqzu0rimpOIunUvNu7QXVZwxvAuLPcAAJ3tgpQITQQGrwhw3XvdIKQs-oRhkNhGLzCZw18Wqb3mnYpA9HcNmT8co7Onpo5nIPEoEzsbzolipH4-GuCWBpTFgyrqPivLe1308Kd9R1awx8hRTOxlm6LJPqjs6Cbm5AFnc0h1SF9cRrtde-bJgOeS0YgU6MIbJmQFZ12wD7-JNgLKLN2Elu5WfNDdp0tykKWmAygG4lK8qFsFoJMhkdqYndZSd9G4yKYX2GuB9vabpqQs0SUVCIUkqM_jP8CfNBWd_Uof7XnKGJKiSu5I6uG7B3jEw-okTcZdTP3Gqop8Yd2o6KOHpoIQnRQnvOz2hli1LuMgbvxODrSlbV1RrjWyPtyWTVx0VIYJPGSCrBxlYUKMmxa1yZBCprKTJgE6V3bvSsoealmb2at64p7OUEQwWfCZpzZLLAdFIhvXGgs-YZr1-0yNJeb6rXUFwAgKd334CjkLENzW6DwENiczP_742JLLjXG8AogaYTA_kk7lsRH6ugsifPpqJDOETWOdirbdMmYvSkynzp2e-YPNt_tyaypwjY1sqCkcu9uMf3ClHBpAhTSZmnz53bbV-wnMWb2R3V8rDBTzMliGcUcCCZJnsaQtv9Z90aNMXwOy9mbLqAB3ZR4FTYQJnKevpMNyPRUrzBfZQyDkkbnIVIakY2cIopdYmcoYiJCOjyVBoLgg3GQrVxSWQaE3MBGdrolPd5PkN-nqeH8yKkN0LkyJmAEKpNbsXCi0ZgJcVsM0AvAnK2b-xnI7ZzWVnn4BEgGz4Ekrn1viUnCc2ACY72k-6lh0crSCSvygqfIfrDvhECGzdWmXjgofVWTHITLZVNeqOq14HJR20prXDYqlGW0bVqxAtsRzqmZde4h1dLA4OOVkcAh2YMQZZbYVKwvWaPdFwtlZJTdMOXk2ONqjCLmtjKgGUrHsY0SXr_vou5r-aCThsjd6z2daopkIeWv24wtPbnNFQqK0xgYmtMRecbY0Oj73kDClQJTcmEOztHhwYGCVq7Z5zbmvUx9aA42GrrVEVh3s3Ayp63HdmQFUd5rBbL4tD0eST0TzUyTxKGjtsCtIKQE4HTacrk0hwklGD6kaQDfo7Dzdgfa2Ywjit-icM0-LTSsY3YZatKMxugrKDO5lwcHDPZSdhJhKg9M2asc4_phRop8PzwHf-xaJT0KSUPB2GgfqMUTEynbYPql7SOVJwiXMOOwoxyt6OAjQWCcVvXH8iVhAihGEF9Xa5qehQOHGxCm3mpee0R7eUlqrk8o7DgggrrDl06MgBVTE4The4OEwMQYqLU07iBUWU6FjYHLcYVR52br65bK6XHRHtqmPhItN0LFxnB7jOcaECXUlDjqkHFmGiJufAnx5fxNl3MxwCjKQ8Qmp7sD1ZYye-du1WV36Z-0au58qnywDzz40n5vr_ksfE7EpWi4Akqx1CSHY5hBBhJi9Z_aHqD2WXdsqTHwAlaAAtWAz8i5Ju4N9N-DUb-PNHM5EhjA_qZDwJOR00VOZ0S0F5b3XmtxBbMGEbN0SZ41g7tjgzcREichormKpTbSKzARhjNpBQkXoCYC6fqzt6PsU17KblAhr_Kfn9qacKxT3P9K249FoGcDJ0PCmXsPvjBjghOnyfaoXr-1B-U79iCXL4eqNv51I2qAiJUKgbUxLZgcTIcKLJy3o827mQVkzXQ6xw5kYkvMWplmdyquWZ3Gp5ot0HxR6HDr498yynqkDbHVRFhMKtIe1VG4UymnAI3iVFIs7OWgScnbUIKJK1GC1HgA6yzySuhiq7M8SO4yq74_nmqzrey0rqOK8g8Ao8B53LW7284tdCSQVINtJv7ADbH_Q3wJoUPBE4fhJE8lHATibrhk9moov5a0PFKWY_Tr4AmMx-tGAcXoTkZH_cXZXsj0vOHfxxc9nZhJEINPEBk-QMxP_8J7jXeX849F7mkRyuM-GxjxR4leGdSffNvDHegcrO5wcNlXg5apyu52pKx05gA6x8HBVOx1M9CbP16lH35OMKAteBf1RkdmsX7uUGEr0KvixeBeRDM8GZDXWqO9dBp6_7wKG_0ib3a1Ze31veKZvOR3nT-Qqbzih-Ax_1w-jJJ8G0nz6aicwyztIFWAOkZCa15eKzbXF3mz7O9rhQE8RGQyH-vRn8p_v3loKTf2_AQy85QzJUyRx4HRzMHuwZuGuyd2bwo3ReLClOp-plVfYyXKnS7L15Ym5tynkWMqB2vuejg8DbtUzqi4RnJNCH3z1adyRTA6G6a4r9k-Lsrvbca2Yfpezotl79T8Jhfh6ovJZhhIsK3MclOM-_BiolvDVQKWEJgcrkQyY92nTmwRtdkaOUyN9hV-t8uSHs_K6hqdgS-wxBB-UFhEpq6Lc1UiW1678zmOu_K9QSDVcu2xUZApa00jC9p77B2SrAAS0fTbsdRFCZCGSog8C5Q1Aha3ong0hyIGhRUmiBLCFaqbxKCOivALAcgJLh-g4e51kdC-bWpspGHZvKzuqYRODPcM0FX54b4y6t4hONa_QtddLhAo5GXdvR66C0gNAbEWuXS_Lt2oDhhNvS0H_SMNr1orxUEe9Yc7zUqWrpojZYGHG1fxDiVUjaQ6QFGFXbJa4WVDdBIh4f-tIEaTSFlduaIQGSvKQjnPLOkrITn3PhhSs700czkaHKSqz18zgE04VtqEREZWTCIMrIXHZWRjq89pITxDuolWsbHRz34MLAXXgKffJEakbvf6FBT4KRuJZDV8d8Y-2pqbaukM2Y2rt60mZMbmyJKu00oCcgIIV42naUBA_KNIi02ziGyAoYhsRO4JJ96GOhAZAE4cvlk2NmCRFJCR82WPZjKtruTkXYqbB_AGaWDZTwKIxZhEJ6jsFYu5EKc-lZLIgkllEY9gRJEzRsfQ2FBCRKumPtN6jkT25qov0YcoAqtTflqyQu8pophvZn7keLVysz96PJKsxvYEaHSTlfAJJyvpCi5HdbOjDbsqMXO4t39OcFlnmDpXj6ONI-F230e2OHZ1X_yquWxSDSpsprTl7fVEDPrOIqywlsKUd4ltgqHrOroOIxy-nkMZvKzntRIkBrtqFv5VWUZpdwI1OS01czU_rTU16vrtdmVwbuqMvJgNUev3HUgWmcPdwYYcwhnpQtxJNc_CqeBDZMpmGX5bvuKczD5tgieN1WxAW9ceIBSp2ceOikG2CEwHG9a0PBvV7BXXZxG4wIFrX99S_-H__rv_yf_2j_xf_Dv_7P__Ff_rUpEm3uC74LRfhqUVMFAV0G6QGboqUKQlXSXi6qKltU9bfoqYKjo7hjusnMIYdUQYkOLAEGcYtdW5aLgTScbfCnoMGg3mOBM_G1Pl-UFEOsUwBosYYyxRqincYABo99J4eWcHW2nojR8CLfjbCUlqPitDTTLsfgaQTl_JtSteZgHk7p10ZsU5gKBoTWb3oKEyDQFCY0FYp9nlDgSZiZtmhJxelw72AspNxFXC8qTyyH8Eo5hFeKEl7hcJ1pl3SAH8JDxbyBb4QCWe67Fp4RVrFSV6azbeQGoSpt6FEswPD7MBay8l0cC5n6FidtcGCzVTxjRoRM1QNVVQhUIahooqmqgaC6BoIQItpuibd5EUAcchJAAh2YSQZZJQeAtw5awNkKqYpG7Qlll88OwyuIjho1QBFlSqU2NmHIdU77JLKjknxTcOhkN0FWFl_7YTcVJoqGyjNFiUioPUN0m5pZ5zRUBUFPzaw9c1RMzWSdmN4rAZdS7bEQ7lKqpfTOsSMzuCiVGEqHK9mWlS3qE1xn0jUe4O4NfMcegR2YPXts7ewhfAZqVbiNZVDJMzxSwuiAIwQYXIbX2QXel43qP8lG9Wdko_ozslH9GQvBySUwqG78BoO-7jeATgnMGUZCdKwBbGU6QG5xAWQTyRm2P93-vvFlyz-bk-U_l50sf5EArdnqCQPZuDVhYP5oJjL3MIB5fzUN0zjr5x7zrX3_pxv214y1oLm7ximOcnD_e4FmBLguslAcxab_04O5V6P84wR_nsYoDhXcNew7MKP95OLUwC_EsLOpLRfApvopjbIUw5bw1hi2hEVj2MBpcA2qjAYQ5Gwi3NNWsN1TWyB2WI5w1psBJsdmam2_sx0KivvpuS3bogdQ3rf3f7yHb-1pqNpGFSpChfxO2WW1NJ3vdgbH5bO_xmkSkKz255G1mNu3DAVzb5HLiVd-j5Sl3a9jXKZYAMNoWtFvl-VmZPsLL_Ts38pzxtq3bw5oVdzi_WEzbHXLYcqDZdnVoYEQzQmCsylBBGeLBdC1Mbp8ulrmK1ml8XYi_Sf3d-4v__S3_-Ev__Tv2itgJtHJ_9bZAI1ZDiHm-yEzlY3dz2MvbGz6aCYyrdKC3byZUUof0wUYzVpaEUjOEgf-6UHCqdAUIkRo7aXY9_vlw6cqFv9DYNwBCwMiq7NwTBGZ4j67Z6B3zqhk94AZM-3hrm44XMPdVlQgiUIKhRC_ZosOZUfNPNBM3BpSQ_WDPhTOPokaySDElQw32y6SJtLpilnLwL7cNbb-1xIyoVveGb9wDWfazWsjjpjdUELjrzJ_b68qlXXDO6KzT5D18kHcgC4OZkfyojO-ck62o8vtyZXnSaVJ3bnfuWlqrZc0HkAyiSANeEqZ9tvlz9PFPLdG4nJutdv74nWaDT5BDja4RAeE-IBsbllGrN3Nx5m-8n7A2d18jDjKzccUx8t3XnGc3dVZiCP5BrKt85QSdr0uOmDu8hJzuyVGtHRXlZsmkMKy_v6gX4GMLXwHWwx7oERpCBgh7Xt_Zploa143zvDdbOZPsJTSE7MB0x2_mfUbd4AwkOStyMJL27Cz8B108mU1-LqA95pJNT-7jYzHW8108uZkOk1lZ9NJIkBrDhudw0dB55g-monMMpxta4lxAcQI-7gAouW4fAsjXWHy-yCCWY4291kQ3GO49oQ289J-8gK4pbRQpZcXZMCGEhhaiHcq-JM5fb9JeEnMN6nX0REz9DJj4ZXc9MnBT6nXkT5ImO_XjVpl_nM_i_si9boRQFHin-Qr3-9LfWCZTIcrSsDl6qgYcIkwRjW3wzCXTVbz0-1AVfH2zvN7VRyGPnh9JQQ_5pIdOUWIuAJGqXWig9cXAbTEiDDD1iTZPwBB1VvbP8Goe4Ui6EE2pCCkyZDz7RdW-YVuduoQtbSLjxRgX_itvwQSnQqJKqRokGRVSFAhWYNko0K8CkkqRB2Doo5BUcegqGNQ1TGo6hhUdQya6qeA1FGwRh0Ga9RxsFYdCGvVkbBWHQrr1LGwTh0M6_TR8PpoeH00vD4aXh-NoI9G0Ecj6KMR9dGI-mhEfTSSPhpJ73LSu5z1Lme9y1nvciKT8v8DVKUiYwPFBwA=:5:2:6:2073761:8:10:9:30:10:108975:12:0:13:20:14:11730:17:1:43:0:25::18:10:19:11994:42:0:45:0:15:3:30:0:31:0:28:9 years:29:9 years:35:444085:36:29_566_73_98_29_29_54_424_29_29_237_132_177_29_29_29:37:3:38:1:39:10:46::47::40::57::27:0 \ No newline at end of file +1:11774780:2:Dark Realm:3:TXkgYmVzdCBsZXZlbCB5ZXQuIFZpZGVvIG9uIG15IFlvdVR1YmUuIEhhdmUgZnVuIGluIHRoaXMgZmFzdC1wYWNlZCBERU1PTiA-OikgdjIgRml4ZWQgc29tZSB0aGluZ3M=:4:H4sIAAAAAAAAC6y9a492u24Y9ofmfbB0l7DhDw6KOC4aFD6xncRfNk5OABtwUgQoijhFf3zXWiIlUiIlzfHsDcy8I1LUnaRIiuuf_4PLX-Z3Z3-3v1-_u9-N_93__sv8Hn8313X9nn43v5twQ_Lv5v-7_5kALREsM2L5AFju-t2Y320Ivxtbf7n3V69bsK6BujZcd2Xr4139-dlR7e8P1tDUQ83eBe7peskE3U999w66FV3D4zTN9fv1R_tH8wn2j9cfW8Wn8w_M_Z4L1rRTzXj90dz_93rWPmjmht99i21endBmrYhzYMrToH8avLCWF9v7eN7iOxvWhXfy6Eo6ZeJseBvZrFKgjRtOIR9RiCIFVx4K97LtKIgduGA511Xj3O_fK96uZvrzGsxSX4253i2zqzydB3cjPz_c82M9yf-6KTZG7Ld_9_7LGOyyvr2mugkPXFl33c7NlnA019ZOA34Pa7b7qm6eq-vd0GZf149103PUrXmq5nXVIO0qW5nEzaGWdee9nF5m-azui76qLG5nYx_OaU1c181Tw_73Z-ofNrPp87SjTX46HdJ-R1p5Rx8e35tXT9W__vkvjfu6nl-h_or1l_-6f8YvY-9f6Ss_RenLPL_yl_v65__gyvPrL-2LXSnk96d_f5ZK5aq_zNf1m_kqV_yyX3c77utXDJ_wZc1XuMtjtK08215urmwmgP-y9svfUFfyDXRP8Q0O6ZNu-HX_-4a7t_YVFASgYm8sX5KK9MBLKSrcvK08iCsC90zar_TMlnHuy5mvv_7Hv_iLxWx8d5Y0fHP5p-vmeiA23z_v1fwK98TZEeoMgeYb-sysiQ_IBPyz1D_fgd_KRBvaDfjEd3Q3wr0dPjE-o_zDf_qH__an_-uf__Fv_-rv7T_8x__5j__lv__9f_uHf_fv__E__00dPdT1H__UvSpdSuC__rv__X_86a_-5b_Rhm7Ue3pv4P_xd-Z__Gf7T__jH9zf_z9__b_99f_893_7d38BHbVW6ai1BZrqi-DMu_zpnYXrK9xdiPeMvgv_bq9kP5d7h-fdJ18Z172SvCt-zJVe8L0ksdy1f1nYp3crf3J_-Kc_1ZbujXJpBOuCvOv4LkXtECzIo3V--XTPS_hKb_efc1a-fHxqvf337yr60AtcL7hcK4gF5sjHOpUdXhocKMSRZGkFPnlSgBMGpdAVOpU-lgZ59xMU4I5qaLn11JaPDb0ssrI0kksiuXw1clNp5KXGZ4rbzvbLfwDZjAyoHsgB2k9kBdwb_Gnll3nareDsO_g5NAitbCt716Hxhdp0daZGumRNYsTFno1IYwdv3bj3nHXtnk9otfirzXjq0_Pui2D5VD4bGEvJ5ni5ZMjvLqMnMz9LFwKhQaD-YdIAjffwDK1ZXor3IXoPjPuEfnJfiIsAeSvehylT0vc5unrLL7d2pDiPxe-0A3aSd8IA7RONw9O2AsCLvBUq9OFq4lYAcFpuBRlp7KEt01YAQAi9VR9aOTCYilnX9h78SzW0Ij8WcNZ8BboB6AKRnh3zw6oUBJ8_xb8kr-tTItEJ6j6c4VTai2DTlAW1dn4YYPRwrtsJiQnn8N2ETepUzGdF7yUppLyKANfLzSs6Kr4znQVTyr8IE26YpLz0cj-Vp14-FT-qQ0N_uSegmz6gytAp_j2sXk6axfJnmWK8PmnuqR16VMuTy3R2cUlipWSr_gXsFJhIB9daIX0683o5fh8JrvC7P-oKRMaJTKMz869Kpi6M7d2PjRnFHJFJMRXjhRVX2ZTCompnJhaFMzEWJ14c63TCdu3Tlvp4XjWAFvixwISuT_MVeA5WFZWXZ-00Ti5ipB2C3yEYAQEWOyayF_pWePmGsIOAJwzj8u-P-17zWKI-4TEp1UmOEo1AZ_Plv6-K-Hbnci9zv4_pzXgfMfb2KD6dTp2h1nXzkwgE7go7EkTLAPMCrPLVWs0LAK8BksKi27TLbXCA71sRBBdu7eeIhMrlbnGY_IuRP_lWp--xeI_ip86ojuPrVSG99Nzbv3uZ3HM3tfe_SkycHjRayufKz4KUm4eb8Bw65xxvVMchjVbWnq6ZtT9zkhxhzBUAFWJnkay8TOUvzwdCufP8jlk5CTDU5Ch2b7Zy8nQtAY6o0ZU-cGRAfbk-AOoNgJYkUhJ7ZSIwar9ocYodu9anJZ6UuKnkIiVxHB2VR3QoWJ47zeuVPn2UFRZF2MtF22TJq3ozh2FjELFmTGktw62qd5EK3j6NMC-mFcAk5AGDzDwsTq8yyejkhmsTLSCzZgZdYC61pJGpoF2s3tsllNrSD0i_sValp3ajsCu4I0ejXPyuhhPA5Xkab3C1FHfuUNy2TRP-th-w6PtRDH23W0sER8hj-WMJ-nq8Jb-hBSFZjxf-W_1GC0JlJOW3ZhcQ0WxHAzNSvWyl-Hm0hviKlcaWymNx-Id_9_f_73_9y2pi8X1jEFlV2WW6L6zpeljrfV8x2RfOCV9VNuO9pDGfbHEmGfN5T06-nvt519pkWCKwqtLRVqoEzwkkeGn7CAD1NvLcaHsZmN7g-lT6qczDzbSQMYX2d25_152Zh5srLWUMlDbixlZyZ6uk2VrAKvaGWHGSel15Hm0ANDXbBxa77gYlbiq5xpI0FsDF7lWY2voMCpNQ_ipHQvmrJ_DyQalrvVfVvjaaHcZC-cTRbhAk5TPQ2UINL7BpbnrfeyeYSdGbBKxfbFwHVpjeLWJOfD_-YoTSyzcyGECiY-oftPLWjX4yqbTWPK5cX2k_XilqX3sxHV8TusoIbelslDbdypXxIleWhmpj_GjTakfeXe9Y-arGoAgmmEHFJqO3ORF1dZoFAexsouBnGUDTR7Su6zub-4RmQBRuAzCOUPnsPXrT1O3cLTIX2rU6MM_ASWHvK7dCykskuHoAKfeOmnYHx5FRNpJxsDHeh-kxzyF42eXqWTFDH6FUGYHXoEGGsoEV-wmk17FRbYDZYJW_Z7B6uXkxXCiVfuuu4qckojemVkD0S0KHiB0oIHpWEYxOvQW4rBGNtcwmpkjK8yh0y2R6YuXU9PSK79JsQAvQZMiaW69acEl9vKykCt5X7Sujzafys9JNH5WXwNzFiXnCUGJjWaXf3UE3qAyrhNh5c-toJV4NRIT91OYm5gvdGi3RFXvyiVZrLweGPkTDZm0emEHWHNjOjjkPO6eQQdo6SOsqV0Zebyps3Dy13EMNahmgZv0SHSNKzGWlOMbKRZsZjnRk1tXaXaH3NZ_6SkPvlx140sg7gPqay_bJ3iNZncv2s5nCjktWaho_LH4P1bhlMTIUmCJMqMgUK4LfIdSJuIggQTXbvVszhk95Iw-MvxceFraQwxLWWK-vP1ROdN_PbpX50UXdJ987_5G-mdmAFjiT4enZqzm-G93c_yqJ06ts9z62V1PGDRS5ro1Xa8-rKSOfhxJiWqh3sIbEXKwAQpISjFEqnZKhJa7dB6qhwVyFVKvxGBc64ttpz6SvaWTKQNRPzTAb02vbARLE2gONsRJqdEikj92GwcYR57Fe08hi7qQzcz4_F_QP4fDE_NFmgpRQ-4odZotYNaA_3OBRBVTrPDdX9ElDueU7eSLzYRBCiQ9jLU93qEP-XnuE0jv33nZbbtMPEEJNnWzGhTszNDXdfbF8DAkwl0HzwQI0eq-k9nFDUqsnK_KBzPdoH6pSFGZTv_nA5EpXnCpnkYKAkODUBy7_qt93EMjTihEJizBJxCJG8H3hOH2QojgQ7UrUZmKGv3IL4ZP6Tw72xPIn2YOITEhO0LUwxp6cYa1ldpsVBcsHugkyHddrFfSEJxKzHJZXNlWYbtNkjblS-MTXoeTLpy5uN9pV-XYvzn0k3lCndAtBy3GgcxoK9XBcL6rB8IMuk4ydZJKh1g5SjJjD0TR49xuKRyr1bJpus2P1GT-OpD3C9qeWotxS5SYmAjcZDd20ReKOxtkJYSqyU1E1cWKRnbFyL4rQzXafb3ZQUl53ST0NkQ63AqqRlQFqRyMMtgxO886ITeaTgDWuWoNeExFEgl8au7Web6HhKFiX2BzK5bmVA19uS8HkoBlNrlCflVilxE-1aAmxtWIX2oigayBDa3n3VdS7NVAYbcq4WE7ZVJmuFZjEq9RhEw_2b3oZrmEsvJfPTDk6RObJsJcIAFEBm0cUaHSFFwhZQ6hXaDpF49WUEIg9pOI-q6NNj0gmo1nrEL429uGIJckWCf2F8GqY4RjTHgrExjuWYoos5gorfwNrIz7JQV5h2W9gbUQxYRH73uu2zIvM_3nobWVuFsJzyGGFokDdVKYaSaXjTinUA4oEeoEfC3rEEdK_EuHN3SYIqG6ixkpGwyGW-KmWHztBnVDYc-JhgqI0lYCP6fUZ9TkYnEwzQJg0cFRJROoG4IDB59P7_KW5hdoYdhiq56hTYE4iXq1zO9tNjFhS53q2RuJKjfGxns7fgjPXplYIaXObaR0IwuGCC429upuKsFmgrbJh6JwOV-x8HD4ZVYGuUJ54eQ3hMtam96KViGun6suj44asM_VUQINzcSLFM-OyaHZQwEubXd9bsskNqC-MctDAHuNEulnZwqd1eyeRrD_Cat6LA1o7rBOrb9uRRy3usNISq225NE_Fb-0eCnuoGUEr63bdGgYFbiyIY0ERC9xUx01kWcmllPiplp8o-6l1kCL1ogiDMBY9EYNzxI9IzfwP9S8GmmpelDyNogdWCSOZA4dxRBIkqpCiQq4Nz3Zuh6BaoFjjEgLySrcxDjnVOBTIYi3g1wl85uGVrlAelfIy8PwqsdzrR0iervSzHwIEW6UuHlx6xAM660FCTAKi-v7btFg9TCB7ssokpB1fhZAlpu-HUBnhaM2epMAdwmlARUeyhEiLzuFA14G_ZhI4nc-f95CjOJH8AC_eNfkRSzzBDSSfYOlJFDJTh04BRai69ghKBrs1OK7B5QCsynvY4Ouu6eBLBqP8rtRXukBtYIlxbTFOjcJOMAqrWBuZ7Y6Mw84dYV3fwNrIf-SQR73fYa2N1m37N4u913zNfDFZ7A96ediGmDCosAlUnk22cryZOww0P7uZgx3cweNi6-wnv-zJ-sEOrqJMkf7GE1MshvpXALyt6xE1YPz2aH-cAIQUjWpBwBTWAgBHDdsCbjUE-KS-qkI6xHDORkcqOFoe-cvoOohuai9kuCTQhnQR7uN8ND5InRb6Ns0jB5AatGtUxfTEzAsKDhQJF2og-5oa6y3dk-hf7inDKs1AKkXOsC3CjJsIx1n2MMvzBdqDtbO0WCIBx_FQHNnv5x0LyhEQIKhPdB2SbWTrlZ9UrLFxlNQbF5HmplD58qqdlcJnpc1Poap0IWoUNFiKaBGuZwv-6ysB-9CDlS6OVjpKBo1TrawbnKo5SwBclwjAdmt5ZiZDD-6Wplv91uxsHl0HvzWrmideObRKNTTdcNWqaZarjmCr6YrXlIxirEdp7pBkJmN1PFXJgDddRCZN4q31RQGHA7CqGrVOr8C6yuiDAkc5CQTsHG2Kda2sMhHyi6A13owUY8UGstOp_JH9vrNMxUkO4iV-ck2NUj4lv5ct35R62H8qzhSKdbdqPuZxj1sfP3dFLvNBbIRmyOFv7UxAO1AX1aGZYGhQTH1t1_FHOR6aeWeIkAokApP6rQOxtvMiKvIpWfZoDekm6dUaVGPP1sABGYi1HifH9yLQAeockAdYNfAndLs-ltA3rL-1OKvQXxnjQLoDAEt6PDPVZUIAf_zkqQ8XKAyzkhNIJC8tGZ-0zY2mqXd0PWlLkJjIjwPO44DpulPdqC8KSgifaeNjMX2WS-iSqGq-IYL4GIvVmt5FklqG1GLbwPqupZBdk4Z9zcpJ1BYoPJ1WmDUe0B1C8V8taoBrXeABDlmIr0YYOBxI_hWy75vxuMclW9KvK7aZAdUqQNSxvUhAsq_KZXCm8tWviAsKqkqdA1ugaujpByDaJ0yP0V3lLO_747vVTgyOGAXA88DQ-V-48ufpjDH5k5Lh7BQiQWKzcAwhIgCI_dmBiRff4LCs8eIHp85DHE3IcTQhx25CBhUnpjkGheLWIAhKjpW4qeSaSiYrdZys1HGyUkdipcY0ZDC-PNuWrAAm6mZ0fBaxmEROipp6VE3DUTUNw5REareUqC4xygLDkq7H8XEXzqTsC4RQD7KpfpGHBCbaxVM2qEVfTEA_58tBVCy9UbEM40JolxCYUx0eN_AiwyFzG5C_aA64alh-DuY7U54Yln_51K3FoAG9Ods8ofbeMGgWISxvVwGWROiBvnOfbdcglE0u4gU2HIzLmVB4H69h55McQwTNm6VzMbATxp-Ywcqoen-MvBoWr-2sUbHStm7XB-tEm8eKLYhaCmSFbbDRvmM8wjqyjsYj62g8so7GI-toZ3lL0yKgSa9B2NY48TtD908wt7ZnMFfiWDVzZcS8FmfmSjAwgHB09Py_jiV7VwwaA3jntvYqpyp_QCu4hfVdCXvw6E9Pm7bULj-ZOZ80J8mW9GQ-rJP70EnEtABdS7NVJRFrAxbR9-ZVpUjsWfdUfnUNtIrdNBoVUxgL-nt91pM0lXiiZiTJvJLSFJGFELACJQ3gJwCYasjjlwFC5B3Et6ZuN2Bxr6nZMoRi34urA68tV9uFPovloNKlyfgMSl2aDMp0CahKlWwvYZWpjZZWpppVIsZZjDtlfSWBqlM5BLZO5SCdai-CkEKWQ5pUA00shT4no32XTLKkb6X53T5Csgohr9Rl_Qo6FGd7chrXUDIEp3hiCE7NittfbVeFJQ0v_lu5nCAAt6ZSritGOO-KYgQzOKdN5Kva8rqNysoADiPlu_0x3yFC3Kg-pWaE5C5wUinp6kxg51owYzYctETqps7OBxQSLaKX2gPH-jvdIB1FD_eDs8RaWwxR_qZt_HPD3MZUcynlyOuYFHN1-V2qV1BFmQ2OqQQwOBrB4Ii7ra6Yf3bb3Yk07-OKYBNFmHazhFTt_wR8s-MK7CrTEwHWd3A2KBCg3mtmcT42ss2oYKvNIdUHR_GtmJ9e5a51EKrPQjCdwzSV4zGwvhqH8Zd_jLkXS1G9IpJeeVClTEZLJH0zWVle7hkSwHSLJXMmYgDRB0WkBORk7lZEkHF5tAJWIZWJbY92k1nDSsfk9uKrU6YWaTDIZTTIMZOxJXWmBAzQ-vxSMgfpESXt2OxLzpfoS85KpzMZDXtdmakxO2lt1sNAB0Zfxfb2uGm0E0LlARZwSD84rU9_VQVqQKa2VyaP85wLAkQxbpOZBSECdqOL52xHu-UswvNgcJQtoPlq70Tbkloyi5HiRsda5SI_D5HSkHI9x_Qxr0XR5ze_-XMJeR3DFl0w5usP17_89R9YlvO7nv-82cWed4pvNrO3HnCemhltgfTqdIV0zJoeXhGMq4BS-W4_AJaeJsgV7lhRH3iE5csv18yN-8E6NiYYYW_Uw18IhQBT5N9s_xdluSCKgasUFk2DzhnYWZPFGTZpufoFC9hfwRx5_JFlwxQe_XfYUD6YJIERlavdpWriQSihzpTSQ4eHksENQonlayAGAroQmzPcDaGozEWum6zh7GMTA0toHWrW9re05xaAgp5awNAZnuzdZUwiX1kYGTd3xxUzPSOlXTBjF2gBWkh6iZtK4lRSppLJEF4mY3mZDOqFGN1tVXyBUOwbFC62Jc3RCRiGCcPSAqkpeLAh1tNWeiYJxhwR3AxpiuW8NhCFuxeM0OnWb5iVFYZbYNhOI2pm8MxOM6rpshusBPxAhnzXK5AqwNj6tAZ7ULes9TzFG87sdFWr86KVPw9g5CsczJYOdxv4JcPBAlb8awG7WX27zGAa5ckKnuk7nEItv_0qUgovxhtF0Wy2pcdPSyETxQng-QZU4hla-QYauZutGt2jjXZnBW00Xit3MxhD4inhJMR4iuhOEa8t4kWa_kY8aeWELn7ia2N17uPfHWua-QBul8X7j6_XJfvxVaoP8SwqCrldUh5OjWdlclSWyZlZqMPzYZf3NXbm044A7gvao_v015W9xlTk5qJCi0KvWIU3FhUuCqFyl8aARwvyWDBYh5H0WMWMVYhghDqsJE8lxMDJauWpVp7mAxM41KI0xRMixE_vdauht08rNwB38twCzJaXeiM8X8puK63lQi4H0N7nOo4tQC8v4wZDPazqAbhAywdZDAlNGpHuT_5UKxUB1C26dATJUfXifXTSBCugCDoCdEiE5DVkoTXgFlphWBUDxGUfFxejQFwoz6tyVSxjZ3W41eCv-MWDqMVE4jhU8DJeE0eleWCB-uKdCTSgYNARlE0uQHpaF-KTcME12piOYUVtjzaaemWZjZ3bSlgcrI7Ieb4b_HtQ300s3E083BFbnDXwTLrhmB6iBwWjtQ7qTK8JoHy04gFFM1KkBWg1GupQoWD8VJIUnJlO6BcgQErUFIUQYtaSfFVATeJR0PKCA0FP9JsN0BioW9IxJ_us2hrqbVfjbr_aWBPGNw-EA9WuzpwNZm4uT2uOBpOjw83qomLrVwCz2YXqFEt4DOzFVWQ7jlNjhX2jLMA6HzZGgeOJBwQlAD2SNhYx6mTTUkPYYNKkPd6yNlyOHWszc4ICFkmChC6VpXm2_uYibb1r6eruN-Xj3stB8PctIb9YY37SBRaJH4HZOk9Eg9X8N6vleu7cY7_MH4ebKCTYwvQBUFWwbpWT-NsPqwasdU9VZWFv6rlHqcU6Ayt0Ex92EyPGbym8Y7AXKqDPx2wf1bhecHr5y1EeF9bXe1EDS_OzG-8a5q3xLrPtn8vY1cikhddMSwoEtwNAUjfo3kjXF3qEf0MjLOD5boTFRmpJHnHyNeCA5o84TSk3tINTAAgCiEcGu9nyTNLIb7grkClmd4WpHO4KU7mtMt5CWgbXeEhzNVpS8f3a1LO971PkYTu7pzfvJnf1PZojPXu_W1UrtO0_oJeL4qeGf1-UoEIZapjKhGBFr1bjmdX8OG_uSnaqlekMvJ-qqbWMx5E8zITU-TJJqBh7xYA9fA7RUO8WxfWrhKxy6X192UHwT7PFidXhM8pkE0TCXWpXnxrX2Myb259OybPTH-cRH97dRPgK12CeARrvlx8ZI4tvzcbPLhj7NVe1pOo36vlCR_eNerHXc9-p53o9_516V68XdvXI2Xbi6hmGl0S8T_C-PJ_8rotdXYrWVutsqWF1yUJhwMKv9oYkxtS5qy3NHkBAZQAREZxMmWtjqn2AlRHW2R6hTz69BeSIswcYGvlGFy0xVBlHSFQh8-eyPTDGUuo3r1P8ZKKmxl6tgFXj9ZVW25J9_K-v5mDs53HbP38QcDGf_GbYeLKRe4PgQFi-A044cvwpag-GRWWubZ9SIyOl0oF-AanU8vfR3vMThujeWXfwojG-XUSzDsAgq6KvsCHoqzbnWugoDBHL-6dGal2-hRzFeiJIKhIVc5UyPnAKVTVxo9sFNo_DfJUrnJadEJCG-1qkgw4Ni0ZgWIcP41UyhXboWTkyd239XKY0no1RFSSsdY0zDvoaTNq7kgEij9yrC1mCTLKgvRwDyT79vKoC5Qa1GUymFREUNNO2BLcmuB6Td_VVwShbilI0OrUxTHh7TZR05HSCG7srdO4iXoL6QsMlyMlb1RrD0e2zI9vEP1EHD25XGB1ay2BtQWt0gWxRsqMgFevHkgqWHK1lcTvkdIsHMuHmGvfkBG27pBIme6RdhOg2EhDYFJnFDCFi-BaiP0VMC0S8YFZMB7dj1xh3VSFpzyZYHmGUx9R8Ukgg05NJj2S5KNv0n_GEPCjWdZRoPmE8sO_lzxjC2ey4tUhx30CFYneGjatrN6trd4vWTpYVJhG5ukWOTxl6V1ccPs9EhcWQ74bZZ5I1Zca9gVFLjYWjtCCRemqN63qAfQflyb3KXamDIeqfOgjJyW-3VVxP3wfORTabE3etJR2QCTOZd-P5hCxPFNYUpwyyDUS2b6aelciuWI6LbNiHPg0URgmbSTv-laRiU4iYdoiwo4HiPHbO_4CejuYJsRzGIw7gpIBBhGNfRBGOOGmBAxzCY1TRZxBiVCOAowttLuR7w0yHmkAludEEfIvCoz0cRDLFHHSG0VlRW8bMzNdIdsZJy5YZqj9AjUnUG_gxrMLA0ySliuaAqGmPCjyWUV0KPEZUY8f1ElNR83R_T69BQLqeQg1ybXfMuNDxkoj3KTGnVNpttvbYUAZFWCsHDHYrGQjGKxmYO6grYcjjMKZYKuasj_AhI_IhwljMEWMxG85hllwhEpzXE7PlCub0rJvpGE9AhRFwJHeCdB0gJRkHIiyULpHYDKU_M8bYmQlj7AkJ5dBmhpx8c37yzfY8g1YHzcpXXbYhVU0fSKiqXmtiq-o1TH6HIsO7bOcoT6wIM-N_nGDIB30OqnxCV_88eCCpAzJR2Z56Sg_QJyF7HKQ5J_zQEaMOOBvCazrJ5qST4AoP7Rn_dPJBaapoUVHOBrQWcHiCRphYIX2Jiq6EWHFoQ8ZqMY2KgkZoEazRlZzIvPIm-302uJEIj7sFrPbsXWtKISZqljA8naMDnZ1GCb3aopUT-RBaflFR8QxxDS4auOr-wb3v4oLp57najLxhF1QQRjgBKzUWR7_EKXuhBjhHQi0059dO1YXenWCWHSbw80CzKqyVvr5f96g04neDWvZUkTX3w7nEjaQL6HV3dA66WGDDX2qnbEgHmKB7rDHjMc143M-yxYSrc7hGRk-vzqF5UcWLLzLVa-CDogE6XDM_JAzuGvkgtf8BdMdoLoVV4Jm9Fme2HZrr9NBc39izbexHNllGer3Q13bzOLpGxg9HALxFIZePeRw7pXxSYQlPq6grqcHzVdxgJ6qh6zeV9ClvDsWYP1dNu_FEbWG0LdlTqNHgRTX1QyOD4gwC7SlkR7Un5q_jjZFyp5THsdxCG2HS0EDtqlWo2gWeqfC-HOyZ2sCYFtpbuaqOcclRbwvhSUb0mATdPY_5sqgRBTIo9J7izTCG8SQTQ1ndBBEjjvF8EBtaHMz4rJhqeIMLKhEsjQOAfhfzSIsFrsQ89oB02kyHmoFV2xxi5aGDot4XR-eeqLJFwZM2q2IR3Xw7Ix8gbnhb5E6-ibchlZXpLeKbhIU-Uuh8aW6-mDedkdx9hMfGlqh6p8LEUw9f5JHgsIUiXdeNi43uE-XCD_a0GL_h4ItFddrNelBbob3KFPMeFcVP39dH4of1Yil-4t55yDFXgoodwvm8s52pmgDixpcXt748Pmm6W6weqfLaBi13WdN7R90E1JGFjJdAZsZEQhHi4O5ixYurLKF_wobsERuym5NvVyoWThq8lmKOfoo8Ofqhxl4pQ9IHlimGqu3IV2LHy3XlKNdgmnfMlc7jYHtdaME-SQyazsPSuKQlaujqQSQeX4sz9y5lamHcXHynDkzTuoB3K7nrSzAbJz_vvpHmQmjmTsOardurklshwlZMZ26vdOT2SppfC8CaVww2dDpwe6WV2wu3cDr2ZaVTX1biUfeTsCKTOco-jIkjKKMTSxJmKe59WMmde5vSubcpHXibOOqJ4SH5PSrwC4a6lHWs_RPMA_mZ9h60ctHzGPJCy_N04f209xGcKHjMG1CV4YTvnFY4nuIox10L7qkNqPIe2teDe6BxVSFonTtbq2PEg-CeNv1bXYRizroIImG7ms-C7XlFsWFHSMMxwAwcXVaQ4fR7dJAwoSk7ifs1ubJzEQwSdgkGCA7AfQzOVBkIzlQZiCKuuQgHAUiqSueDDGcSaGTvHvlP08Z_ilTW4uXUKdqnY-GtTCd-z7RyaXJCVBT81vydUlfY23LwWCq9mTEkzyk4NbVhky1_4NTkqEdSg6KuucGJp5TuuxZaKbDUtZ80bfykaesnZROhshK6NquQQBiQy5_e5R4MCH0uN7SmCLtJuedNmzWKnq3jEkU7JbJnRjvcO885Dse_8qm3yxmyhUzilejkGcP2J8tVIiR2XtHsaC8GCYs41HP6xYPXshthrCuF9FSz3yEWZtGSsWB3Znd4H6iNrhFpuyy8b9Yy8ujaZR6EfOb4zGeOz3zk-Myq69KTtlSw5jYNdAeubyY4nCXOypOJHCMfuyfzsXsyn7on87l7Mp-7J_OBz5Gjnlwo8oF3kg_ryJGZedJ4it3Zdj72Y7JpOsE8kF_52DfKpvMA88SLmvceT7gf4dhX96Ni6Ql1VpRLlp6x7s9Zhtdg7GKtNMcuHrzNg6rffntY67WXmcdvD6Het98eQr1vvz2Eet9-e1jrKS9HP5ZhJhFzeH2IshXeUw-yNXXYLugo4wtp8Y4MwvDaCcNCiW1Fz8YVjlTWTH_vCjfQeYzB8U3BgzicMQwHed-B85yjnijbDHXNLvYedlQ-roX6xNZEVbeBhKputya2doCGqV_xI8Fzks7NVcYx8AOhU2AfVxt9HXewnzfJgnkSOJeeW_Jdk6rbZ585zkMFNPbyWwsdyKZ-ItnE5-uLSdTYmz5OTePgMc-XgWZM_Lg4vEytNcnL1Le0sMeV0BYEdhbqT9xxdUJNMW5UdlFGL_SYTYShabb8Ag5x9taIrR5izBePQsAbu37FOlDPy-Q1l-k5zFZbOC8F_b1MbnWR_0G3pDmkGjpQU9GADyK1lYKMpJa8EgmVT9gqvkhwhQuctbwf4ns-Wc646hTdCLyytCiBLVst567mQtOAbVAPvNIcddlX5Hsl73sAzJRNwVIGsBk4wTyQK2Xv5uaYJ63n_YhqoHepLyDddzTRSv07r2igxvYVTXm_FLV_RQOnrPIGszKoAI4alQ5x2YDGODHhMyMTEq0JZfSuj9yopkkr3B8-oUGKsWIggX6MH878AhwGlc6L4yvOxXU2HlQDJNYcCDzwz1c13JEHvrD-HbGAAxd8GUe9vwEDrnQDFphAW-Pt4Wp75Qzz6GjvIws45upog4kU-plkEykESVYkMUiyeCFIEmGX-vykUfythUIWP4dCIugCwLtuZNlodKShfMf4_hy6ib5CD_tbr8c-ljxocpX1uYu-6zzR19w1Puqd-A2iBEX_qdoWYDEHE4eP748nrS93NBqiUXrxGBWJRLWuIZYSFVJVMiSSdqZanIgFYmHd2mhnSG-jnTVqC-2skVppZ43QgXbWCC61s4uu7IG6hVSXqJUpcKor9sGJakavSGmunqJUdR5PkhIUAG0KPtFXbuJhUBxTQFzx9UNl7R6NtFVfP3Zdu2j3oZ1N7DHigQufta258Bk5xTXP-qa45t1VCooIcnN4F3z2zxc_7hPFcEBWsETkR8jkFFc5A69uvoWgKRdXAG9e7yHW5kkdoqmqW2FtblmVOWFD5py1mD2_aLvm-DkuJ7ve2Xt3dLUCuSuUT66vPm720m1k3Qh0owSGMtiAqn2HSGTBI4s4NOZtMAK5yxr45oFNH5NajpY6ItNybkLxW8eMzkUWk_I2MtTs58HrwBp8ogBBvTDrx6gOsN7sne1Di1T1h8Nl2neu6c0CgfgpEsl-iR2JBKcfN-OGgyR5haGBZYoyoKR0AnQMgy7GnTJi5EeuMyMw09ta4aaHWFnBKmwudooNENPRMpkztU3gKdjmSv3BBhWcoLVGuBKSOFGOgNQZrtvj1jAhPCZDmBAHLoOREGkZ1oQHZUOprJBiJNth7DACywQc9zzHlCOgxBYHVH4iY6RzkdOHd6-GXWnz2cOptMns4VTqmnQBZlr4wlY37idwj1q-i3ryQIZwipMHMnxsSynKhrbGLN_E3JsXyLB29wPs58H9wLCoKIWXA5sQItQYtw8iBuXLS8cb4GjXidoLFeqWlw04mvplA_q3QHDr24hp-V6PlvEEsZwiYkzZWdMn95uGuY4-ZqNe46yvQXRShoyGbB6UsETEEcMS6w7lL-O5Zci0R7zTTYXAuXmGbH50rO_MKYC4u3wg2vLygUgrcYwCxMCbxTkXqrAQLdRgzzcpqsaPEsNc8aN0dVSaltT24uXjhQFHmX2dQ6zd-gg_OGTX6WnEoS6mL4xNqwfoWhwysLoDUpyyilpPoOkz7c9umq7-_ftIw9XMmvSJ5f2C-dVOFUyoKZ97YZ7b2PW5ku84U4jvArcHDJAjTC-LYG-1QzpbuC_ZTE82KR-d5uzxOlSbMq3B_ceOj9RF64fNpEcCOMi7FZiK3T44BwFuN452OByWP_LW7CN242iHG48dk1SLjjJnvegom-9G2Kxy7_G1zcFhJtyN9H6Rk27PnfutYwc2HnvsgieLsUc9iAFAod23woDbeY499ZJzYieYB7ouG8wqTgto7k7HQt-0y3fnQEBTJu3yVTrS1nVFq-WoZvNwIEvaKM4QTzQ7iqkJE9o_FWf1oB7uIYAj2p9B6MDmiXJQLoQ1QIf-nABbrPrdAFsUKN8NsMV63w2wxXrfDbDFet8NsIV6BwG2N2YSMYcAWwOsHFzbo_0Dnw0CQhIV8Qqbjz0K-XWwB0rWMdhDetiCWNtUCoiohuiirAO3O5N1A0FP6GnBvK29pSy8RllYZjEHdA4iSBB3F0Hy6G3mE_oHYd2j_0l3C-lqgY0cXC0Y6ur1HGLqr-cAQ2fmdsOrW-DCRkuniBND9AW5Xf-M8-vmYIm5nC2OwKtrxRJtOQJWoVReVbjqAbF3ZP4cjHu4bIpPPRM-xeZmSYSNx_PCr4MuApHDc_yGoy84j-I3nJLWnTAHtw6ScBRr-ljLu0RuDBL5RZ7WIXTjIHHslb1sMHOnkRjuNBLDnUViuLNIDKS2ckUgqaUVApFOVGls9ECVdueRGO48EoNRXWpTjOhST3XLhAlsNyxYlFvGUbh1HIXbxVG4TRyFO42jcGmHiEpmw9yYD2nTmhLpFkEM4OSB5rovWCu_-ayZAWRXxxUUeJsIjfRAmyZIH0i9kDgWV1bp-RnQ99CrBE94k9e1oQH7UwJ6shcnpEaBALu2YyrBRICTCcWN4RtyEIgb4zdkRogJD7aM8CyAA9HW3Os8gMMdBHDgKdkHZdS3iw1zndulolp4bqGaQIncFSIEYQ-ygBzZiuqU0B82_zozWuci6D3Y8hrsxhniyoramBKS3DGlVRoE1JjxyOip4mqoLyCuP5CU5_MNZxDZSj75rlgyE53fmsnWxfwp8aVsPj4_ATm2huN2k60L5vN-LMPk-Hm-zdlwZpOtjktMto7m-ugm23pGPXy8Vbop3tuqowyvNeubLudLn5H98wjSYuFcDkD1W4ULmPaYAVR33lUYKQQViTCIKRJhcN_146s-ZqT2LE0EVYTfbeypx5XQnL4FUCjNMTu_BNXv34k0rL7rAMHgxyT4orrtxywVovjwAT54uEhbTWc0bzXzRlG3I1SW68e3lcw3CINkVgbM1Vz5bfuCkjynDEuPuCxsTnfXBH8W1-SP4pqwzdVlAhtc2e6n1nC6iXjBtk7uG9jmEe5BIBTolH6O3BmByyRKgOMPcExRNV4fFw1BjJEX4n7kwCUfpy7NcUu13ax80oqgBDAJFandh397o88ViVuSJ2pCkGYJ4pqU5eii3p-HNfnzsCZfvot64jTy8Rz1OMUKdmD5FAwVKN-yh4xxVSOHB3-z1_OczHozW4ulyuePw7DYRJzQPME8Dthic3vS-jHmwVs4IpjO-rmkCcosLqj8bg1dEhXrG49QsQZx0rgv4RHqjZdEvOERaj38SjYPcrX1LcOEuDEdxaLfkmTqQiGUdlKc5_FQgpP9WZgQoi2vuIh0IthIOIoqUlqglJ0DZT0JQVmHtHqMSOIigjgxkDsdRCM17tSW4Cjqk5FeH499dhGIc2o0V8mI6s2o7856feEaEZHK5MMQYFwPgSzC4c2n1on8evOuRdgmukAUPS9O6k2ob4aCEg6D4OnLFCzkJqyTXCDWOmogHOTAIHOyT9-DFDcJ5pDeOssPYikZ5LAt1W-IfVnp4diRJc9AQieKMxI84C9h_hhD5x19F-rAQZGVvs_gQlRjHTtXCeeROuE4AwZSPXhUj1SXqMB8WF-XbIp19QTzQNtgY1qyvoDOs33IeVDy2fDDt3CXhGXwTlgH74Rd8E7YBO-07p9N84m7hJLUrJKUmuYqwSO2iLeBFVWS4NUFsLPVaZQIVjR6IC9XozgKqSysPwWrpg7QpcIY5SFbRMJZ9EY4jd4Im-gNbG8VvRGE6A26Ab8RuBFM3VanqT_ItO251EG8Bt70APfsptdXeHuE7DGn2keMQHgEtG6EVBWe6FdoWMbjA0LsnukaqHzdnM63EIrqR4vVBum9fnKiksOBgRdfR61YQ3IJuKjE97MgT-XpbSnwViC_NH-W3sImrQ5gqV_YgiMYjz4AAn3bKUlx-QEQbEs9ntiXlZKEHVkqSUjoRElCgvsMYTfZs--fgk4VhW99TJe2KHxYZGKXHHf8upV4i4vxPPar75Qt24nH3_tAqifaWTz44AgyMyC7YWYNO32D9bEeLxka6_AJ5gGTZKtwQPOERXdOsYuViSw0QY6VqdRWDwnjKuKGcRxNQYzreJq4i6eJm3iaeBpP04ZyhnjgDI-n3yDpJDfOcDoYFWeZw4TS0cLOgfm5b-c5sUQouOXLJZig5pAG0Qn2kVpeBvcviFUlqAKcu5xklxNVsVSgsBPHPCqiz7JiHVgjAHEnQXmmlFlEmhPxZ47v_ZHGAGgOr0gDRjdISlYAcCspzRG3kUKGvIfXuky2cwt-2kue83wuDHV90Gn7a66rxPMgO1Vy9LAtpfPKTbhPo342niPGZVaMhE65xpBK0Q8mBMvEEj5vFuySPtFGYEERGQNcFGIi20T65ohLY3AHYSvJyQuDYCGugDIfwMrSlwCB9rYyBkPQ-wKNiE7rlDaINX5YhbMv6Ms-5CKdZoJJm2AIpJcgZo4MQLrFpCkCQ-SuaRc0weZs571BaquLCJKSvwJcYzQS_65Is5uDqaF-KXL-di_EXEy1DU1CiuCjEAoc0BHuQbhFYT08YbTp3Oudzn3p6TxEIJ2HCKTzEIH0jcwnSfxeieKdh5GdXVxSC_3asfJ07KFOx15vtgtOMA9EaDqON2DreoJ50vpxFAHbKktRn1jurpkfwgnV07jgeddvX0mJOUTmJ2dhYRxUhcr5XTjPWFzL0i6DS9pkcEmnGVzSaQaXNltniCfqD8XU1B86EBVnSriicZ5NIDQgLQOhPWU3sufBUcneDag00ha8C4lmCBlFOuo_16z_EIVlmUwEjsK101oKJbZ1MCS7S4VQb2eJhYgQXeA6uJUh0onnIJlqaTz2HKQWLLMXWTRiYmM-A9xDKbQPxeCYJxKDRmIs35oCzYVnMq1zzKRNjpm0zTHDJnidEglwxJRIkOsz-fJJbwzVfX6fLPJCss_kA8d5SJBsnziy9Lm3y8MInptTIhem-enAApncrhJNSjferuDJaoaoEXMSCvOuYW5RCMKVLLNsMezCFEhz5A1XXfY85mAR70mZpqiZOQ4QWT4aJXS2GV_yJkdLob3aXmvy0RdYGrXVtQZJLVkZEjq5TiDBg-tEn5S9Mt3necAl5LRELv1MMypLRsT6tsbcZ1yBiADEPMgEmJUALdyeNehioRtmJfqKbTRN-8vrqI28i9rIm6iNfBq1kU9TrmSMdzujeIx48pSNYmraG-2finOQDIbO3JrOOjgFOe8iOKU2tbHjQ5Bx_vNTwuQ_MyVM_jNTwuQ_MyVM_jNTwuQ_MyVMPk4Jk-vy7FLCgF8nW_zs1faDgoCPGplwC6AYlyzFR6OfGENRsXR2V4ns08MA4lYw2hOhZ88F2UFwDkc9ebvMqK55FSW61Jorpig42BTqvN9uWDvGk531ecWI2Yxt2CJpVWV5Vmd5YIPPCfPBK7oIJWTnnC71JJpsPxd4L3_D5CzITcMQP16-k5yl3stL9XTICQJKGg8jP2llfEc5OiPe-SxCLFHXncuY-0X0lJaDvCtkPLvnGoXHGinhTeUsNUs5S82C1FYKNJJa8hIkdKJAI8EDb25JZPNoEUeFOsbUgG2KWjR6tdEAUd1jXJIUalTOk8f0Bd7fBvqWOXpewnqx5EqsEyeYBw9RWut7tb_IaUX4ucpC9onx2OXF1aBMQTvj-dHuBWUdrFN2wTplE6xTMJTybPKPEQ_U_bIN1mGIBwKuTfP2AtFGswn_oX1UcdIBDpnmNc76okFHqIURlWpoHkKIxggi8zE1goiw1HWWDioK7SWaqFHirXPeMCxBbuKxOs2JU85y4iDaWmyc58Qp5zlxGOpSW6yYIi9ho9U5wiYkpZwmlinbxDIM8cBqS5tWN7g5OCir8Ba6dSaVK_zWkgjeV-7PLdLdk6rzEzJJImhBS8wdHmsybktMudVdUcxFqbSEhmDDZYoASdlNhJSw_98AHX9FBmbJX_zVHDwHOmzVewnBQ2Mx1CAWrZ4Uxl8vl4lSwhh_FRFmL2Fcz7DrtOYeVLhBKuIMDUh1lhFp_XgTsQQVPvfBDtIfYYrXuer1AJYyrZcO3oQYIRGhe6V3T8trFUk399FFMBWrD6CTNtW3D1XxQaz1RQOb3EQgIbHNTQOpbdGOApVao4sLSWtxibMKZgLm3Ro7uLS0Ro9wTwKOYDvHs2-01_sQbr1l4CqermUILGdPa6Rlghns0oaQ8K0qpTUlVU2k52BOTDb1R3z4C_0Yq1cWwVrgV8ThjNfYX20Ke-yvMn8TgpR0psYGq8vUxDNu4oMLKO7hI9TjiDFEPbANEs6zRz0Ir6saEZ-Ble7EJ2CNefpFLT78k9ZPME_Dyoj4OKS5wgR7O6g7y_wokeCdZ17xz6-DjCpwb-Sjc8N5bUhtYNs8QVyXA9bQNCUexaNoSksk1JTWlAyZ5ClzdIW9o3h40PS0EnWQMZeMlM4OsJahR0hp63pAxK0EP0gU05D2JkCczML5OQdu5OG1kofI9udcM5P-hHxfaJR3Hbn73PX3yKWRySujINyQpo_ZMM6DaChkCZTqmnnsI6EK5QknIQA4bCU-jW467fqNJLTrd29iZ6LqmKqJytLtHbWcVERDfb5K0bgVV4RwqTVFyanwqNRnCg5uuXY15vXEBEAwMuLAgcEYKZkPMCqTBEbFr1ymJfUQORVwIcBa3Zfq5cVMH6CSto05CjNCYjuWhtRWFw4ktWR7SOjkEoEE96YxJHvCHczxt6DITO9RD1LcwIlkfV3yHNbVE8wDdYl1dOXT6P1cJdeim03lUWYdK4TwZefJLP96ZMtVqeSH8eSnLVtJ0SiDbm4xuxCF0rG0p3sDmLlksIG9DmGOwhca2vowHYcvIOrRrj-IdMCF2wclcMzVtgOZaNDPP0ofOn_6VtNiF1DKNfJtcO_IySIao0dM4f5auS3YiJdWakpS_yIX2Q_je2kCeiKM77_rXBhWTo4BhKiRLZ9bCe7kng5qEk5QyGSHvhRaCCFjmbASI9PFBZq33DZcEAVzjy3-Ms2a_cbA30K7ZtJ4EmqQ0mJbKVQa2UTwIPNrruoIOW0gnRA9uZf5mq5foMGYFm4FGgovVzUjgKuakWlBWgo8bOoHpX3QrEyg_R7VVY50NSQ-BRH2k-dpjVlG47Y8ratTUxwtymhwBxmmRbBBDROrY4z3kxljjKsgdxylFTNhSLcguHwjUyHA-fK9RsLL_hoLmXHLXMcMjY2ZNvDG8MDVlJUFBi4ZgOm0S0bse23-8NZd23xceufy-WiCRT4PrOF51VPXarAp2BY51fM4VYCfAZnUqHK_waq-YFsMVYtiN5TaTgdANPk5MuvxltRBBFNr70Q3R4IHqoc9jg8iM7ZHPQgl4qgnD3rJQgv2NtjFDGep9bDxnGAeaFJs4CeYS1W_-mCsPw4YLlW5sDWomoRK36081k5bHuH8WjRvTvPcua8amszqkVBp3-r9WlbMY4PprME8Nhg3DdqBFbz1qjH3Pmbl_S5Jjb1GY627eo0QWo3ndpLKXaPYoUK5aI3caoSnEXOVt5U8VDJ0N7_ZNKCZUN5r0POhFevcUCt5T2r5w1q5kEloYed3B93TwWKeSoaN6utmrVPNFhL_tPfUvLdeeS9zY82vh-1P1V2v7t_ePtUfri9Vd1P12KtH-zSfnuolidX9VL206vHpfHwav39IlaviSdbHuNTVbjumHRod3Llj7dMOIeKW5x-EHzWkEz5-EH7EUY_4-EFiH-Rn-xw8HPOE754EStFNEUhJCy-alpJkVZvMCLAbAilJEyG21OoVy2qxV0zR0KtrsZpMudCr67GgPrBxaRblhjgFao0bZL4PMpVBB6eD2n4Cs92xDwRlvTiQyceI-7A5NoSDrX6CuI28bWrD671MR_nj8mt1qHcPa9pZwpIWr9o3hyVC8hV-00XctoyYvlomGiBbAqALW6_6raJlFoBWzWomAER0vH5rN2zaDbsG2Azdox6nSJkIN0yEpVcIE2ANbnaDIXrQH3s64EsZsFUG3BCuoWfO1U32vlEziTgwqi239rS_S8ep8KUyGnQ6xD4-8j1kR25exJHzblcHcS0vh4mkrxVaPe1XeqB5gtYYB_PWNdcEvkjl0G8MpF24RKf5ckybrx_EKxoSxNS9lIxgYYIQQQepCF48EnUI5dW46vt1nkIzBTLSqffTpGmaEA45zuaJgm0MBHwlMJWbmTDAIappWj0Ov9bwpIJrrhy19RecVfA76qIPukZdrcHGbuqbsEFwQgt0R1jXN-i88rZQqLa0NvXdJ4a2wk6ObSfLWDRQM2ls0NF8aWlcDlugDaWeM1O9Ok0XDkHplol1m4wV4zy_cM10ZryelqPrKdQj19N8dh92fmzwGc5Ji35s8RnPUZNpatKdNZmmJu23mizStJ5UjNK8HswOaTB9az1Ig_E7G4A0GA4bNGODft0gP-tROetUfiRZfoSNhIBImQOkXxoSO-VrieDXHP2X28gTURnw1YlT-YexE7i6w-oHalEvJuBqvAYuoxGvEtAI1nLaRh2eCRpWJnzdCMoFzHdt0-R5QIhQ_TZ5HhIiZLInjDDftQkntEDhxuoItQtW6AKjEHYUwoZC2fQxbhqIOv3qkdDhmShg4iRmornpCHkDDzq8PlLf9BDWIGrbANiHEQQ33W829T0uo9VNnvomF9UA2FoXUtNGdSEdbVgFuz0J8JufjsLtzMAMFalw21mYsWacmjxTGqAibfJQa4CaRNqYQ3EDNYm4MRt5w-eHNukOm3RTk_asyTJOrD3TjaAimVh7qBv5a1rLM4cIVLzEeV03WaaJPdRVoGYUt-xBZ2mT5lvzQ5u8NuoKY-lrhq7z6_fY_yqyIA7ULm-mO3-oxpP01raT6A1gyqvgSUfYEQdjdmVI2oW_dgEUDStc-GlPqtixUtx_5bahJSIZu4QIdcR-HjEiQF6Secww4dCEE5oAhNqEEZqgCFmH01fcYgtvH4vexerU0uFgvF93IOnto511OQNWp18_rrHp_24F9DUGx2bfLPJeqb30fecttpQNfRuLAhyoJaSmDAq-w2QFCQ8NZWxIacJduyay3kSdeDMjABMLdpIrZxdgqEjlyuEFOEx-dnsmV8LkaLeHcgVq0qxrh0weahImT8TnwfxQuXJ4z4eaVK6cXfTDFE9gz_S9MAUU2EN9D2oWcfuc1Izi_llJz3pqnNmcml9mzex-CezEvQb4iK8zURZFMMNW_hRbcXt2jrFQtRgeJr16u50u0pE-I7PaHRiaBFaFd-DOo4DKNTcCxtFKAOR8_GrP7VMH-hFYp6eS9gJlCk8buJF6Rho3U8_ouBMft0bEXh1PssgAlttgtQdduCIiv4_ttVRF0xDsDiELCKy_kfZ3gpYBCucpTlY-cviXJzFOZj57pjrHyc5nD29ecTL02Z2lz1py5t74Q7aWkCki4lPw8UUSwvtr3QbyiVSFrDH1RVuidWo85OsW_hK0Vm9NR46koFrZBUzIJ-A3tESMmztxXvXsiSreXyglMENrqJYM9TVOAoD29VpiWU_TX3uSRtity3wGlpZGmgpOrk5ZpNPCnyuaraFQ3nUcZzpOUVA8R6nTJ6OmY1R_rVDhpREKBwhC4kv4WwvWiCRVQBiL6wBZCM1FtuoMxtiTiBkKIEhFxMGUKwIOeKljz-9Qg2A83ZbPx2vHSFrK58BTpfO5X6CVNhnwSoCEoe1vX99pT2ACra8-3hIMuu6cIeErqfboOvRigWHA46qEDwgIpdB7ACwgsadQ8jOK2nt4BJDh4EAjVuEBtSkJkJeAFddIYYthVQyIyU88Rr6zlUuHQqS-DAQOAIsrchCYdhmWd7A154E5WePYPXdK-FzuPf-cOSTyOOrlDa8AsvRhe6JPGu1UbkMPB6nLweDwEEOCh5P6wFQpHB64pMDoy2k2sZUgUKmtuBo30_qBHnmER4BntR062mQ_DW-M3zHCrDUs8oKFT7gMzBwI71aEJfkN8575dJXPfVBeRfXmCPFd7VfVxQ9VWHpcrvhq_HedUjqq7VFBBjp0-U_MgOuN77iGkPVwRK_77zfeKKZPpH1wBBkEBnJQRZ6kfAD2qrhJYQMnmeYl8vTxMAk0Yq0PYKmV9nCzSTQJNxKaET4Ys8IMW8yagQSat55vRITCO7SI5Sie8Hse7AqZMaFtF0q55cyVFd_sO3x4i58oQkL9dhYL2Sgyp1aVAGkJWMmg7LcYRseoHLtiyFIBhivD0g62lhTQ9zWOWeCANMk8xTEXJ7nHiPZ3gAJGYgii2ppJ3O0BolkggkDJNGxdECi5OQqQ0Y9PJukkhYYmiYMM0ZZEbIhoEC5K5AE_JwWavD7-5cdP8OYz7Jo7HZIG1ZuRgmQgNcQSxZL7l47Uk2Caq7I5Ffede_hwgueYF8Fsn11S0dOLbU_R_ffQzRIdoqszGvJ5Lt1-bULOd-G1abgvZRLwLYiSbPqRkMD-AKwTJwHxatvipSx2eLEfHcNrGKwJXRSyju7R0hqtdSudCkto-ATTbDFrTHWO5mPie_nKn3yhByO12yi0Wb9RhtfQV2SWlhdOFpmlWWJHBRMRegq6CVQ2tFvGn-cWJCTISZRMUu-VDM_4Jrwr1RnPEXrdVCXTvIhCgLhUClvSw-KbhgLiuVYXZH_tgwSIGkAzkZVroSRYMuAbAx4UWjYR1fhgib-rA1quOuKwuNX71_To7n13F4LtEUQ6TJioNlSyCiwuYAuTGoxeUTPq3QK36IPjR7MkxVuZ51BcFZc_6bl_lPJJl2UiDW73Gsp7OSppRwWFnYYziZeG6AiiKowadlxhu5L1PjRFgBNc0QPRBewmbCx-dVVVGVTcAVgVUSWuweUArAq4sjZKFrcGXwdg_Q5Z1ibNSPjIQsLWVlYyuI5CwQj0WOoijh1NYEGg8LVvAOB2ibhPmkCDVcj-EzqbJ1dJmMgSx3JwjdCNZ-FGGa6WhuMl2YQkAoigEsRZuMKIJQnMcLE0brN8QgTuSpFJYo1AJB-vQfeQeXFvFeGNkbcufvIz_c8Qn6ftndlfxM0EzD40Pm9Nn5JZ5EFfGmCSVzg8AQF72CZI6NobkeDevkHPTPN-Xe_HVtf17fOAzIzjmMUqjEMCTFbnHHOv8Xj6kJvlmAggUgCt4bQal1LDJaWG8x1wS_kobV6U9VWAIlgSrrivZJhubcYVWN39cZ-scfbWZuw_zfLj-I0dUBLDmE0FgOYFNMkOgA1_D_3XHr9KysaM2iWPnOfTzZ5xs1f5gdtKES-43CpYMYACzwe47CwDyYGnSr29tW2xvpa1Hba4QgW6gw7sndj0_mKGrS8wq3MQEW0XCcwTaC4y7TFySfaed2MkAg0Kl-23a-8uM9U0kMyID5LMNGPSSpIZv8RCWmiAPaBFscYnP5Y0abvsrTkShPrlGqG0p8N3HyrPNsRMKgol43cISUMAqVWbcPSDMvYJ2Ah7AVbSe35p8Ad06OfIpZ8h5-k2iqQAjNyeXY9RBA2LS48BkURmb1VGSmuclVW5XvUAByM3oKxbvPswiKQx1LBkhqEwXg97YeT1UwQGIo4yRw2u4LvsvIK_TipEsgr2YJQ0zdRylEmeOL3TSZ-_VYWDeSzPY4JnWh6vIHx7JD_a3PVs_r-x__RPf7J_9xcE8bKf9Hgxg_3kq1hawb5Hx3z98T_9zT-9t_-kkW43e9yhKgrIN6PbSSlXXcpa2CuirCUsy7uZKXB-0NlB-Lgr-fRyg-fjp5hiIdhmyqM-umAjEVAdL3WfHdbUfHZIItL-w7UHqS8NeUiGxi_OwgopVqzcTI3ytasa6oOtl9caIvowTfyETSXdk6DYLkcssdaJgsa6HULcIRQNAdbdXoSJ_OtlS-3xz5GLP0uu_Aw5tn78Wmj7LEiAIonGaips54HFAdX4IhkGhhMZCALQ6jZY3BxLAWpXIYiI4w5w9rZZoDP2lY5RF8iWZfyXhZRt8eeIM4knSz9js4xtbLO3Qq3eX-xcFbxzuFFfwBXYr8FGBUeZeHUpI-Oc-8bg2LgcgRSstx_ayui9QTRYxPQRWqsYCVIJKe3VSKZ5wnqU0jxbE8xwWA1gEhaBcHe0xYLLQMhslft1HQXg2rDNOiPJdauYj1GmAzfT7892Y3hFNHeGFr-DFna3cdtyv-xv4xbTVWxv49C6297GLX1VMt_GHTD49HmdGf5zZUuYDtzWYYXcGMLkLOWLcNWBjItRgHQD3gN0aDt5YERtcc0kLSssAA-qbxTpoGnnpa94WgHLMgeqF4j1SMuJQiYK1OYZAHR-oclgf3SEnS25NgHXn52K2zybwmsM6MuPqSW15z-mljjBjG17tynA0kW0NViAfE3DkmXs24XvvSre3cJy7A4swO7AAuxWFmBQE5xkpQZB6IjpVxB0jhqPhaApgkSk4RimVVtime2VAKzgTBpkHZ_Zqvm554FNDV5yH_tGAglhqzUgNrjy4HzVSKdS8-rNAbGoN9665Se4OuhPCLljk1DbGsF74-aPf7Op3zzR0W7wCF7PWJH4VuIKdDU34sSd2YDdxgbMG1WFE290K8Nc3uJF0uxCKjXMcCi_XDPfM4nlvKtLai_7cUxmWcr9p7NeBRqc9SjG5OKqjuLu3cy-pRVgl37fgjIHYea7wWiUGwDaPB9DOhiChtxJQTMDE1tgNWJKY2mG--TmVki5V-gyeFIHwch7YVMA5_c707bfmbb9zrTtf9a07V-e8nPkfsi03Xr3HY3BSRpDqKZD7-vXJi6ymR9JkEtv5zVISVs91GuFfxP0v-m74LVl3RX1UUNpxXSp47i0glrgjWSUMBRYP5MhOmt8GvaupB54PbgcWcQCtlYL_IEbwB-4AXwPjhctDJ4FhXNbgB_iwOlTJqxOI8AVA4Wn4d2ygQJHsrI6wDUUWfLmGorztxN2uBY7Eea3Ma0gwnzzLoyPIuua5fh5Ewbd9_PPkygPVgTUjRbQipvLC88rqyM01IfKrjPycA2bVgylqVh5iqTx8ykI7a37eEywxW57mkCu1xVeXCKcpE2wntCMfVOPzfn5cQukZL61PVBRedLsu3z6rJVYG7AeNWOFBhpuCfXTOOMXgeg4Kim6CHacnfo6DvhZ2FnPw856HnbW87CznoeftZ6H-DPS8A2YwwlqCgVp54fs4K3b3xCTopRkSyrEWQUlRBqalgBziLQhsyKLm6AHJUNLCkwPSsY9tBRP4cDIHg6M7GFlZAc5FKhRcJRDuKA_5MWBi3egX_MVbt6Bm8dn4zHFiprpOKDp2DU6AycuMJHVqOfNx_ji6hD5eyYFx_TnTCoGec2k40yPmVTUMWK7IRqKqMV3N-x7jT854AEVArKJvFNjvsIyXhnAakBZ0K3ZpHE1nhmXf5WiARnR0hMOrES3moczO3co30HbmhJCM5svzeFUYTgKZIP29wYFWPyeuwLLHbkAEN0KwRB8bQawJ4zEpFk1e49sBIsc_5t9k69qcBESE6cOqHpXtESHlPSu2E00o3oXpdjqeSJpQ37S0UCFqR2ME-OlPQm9q2JPQ2th6AFoU_GyXWWT4D6t4XFTP76abhuGqNLF6wTJb5TITPsbLH5NJNNhBrtqYK1YTh1Z66ltUA1tUppZj61ZKcc4z6xz0jwrrbEt9eIIm2AxcjaLrassMD_uvC1x522JO29L_FkfSfwhH4kh44ckNhA9HW_1oDKf4ZUA6YSCQrVb5Y1E1N4WRO1tQZzeFljK0hLX34gtF0JG2hjFmBEZCpEoMhCUzLhw9cTFI4G4eCQQD1xE8cBFFA8eCUQWq8-NNJH6BxQjTaSOi8FIg_vE_qj-3MWiHOERm2N4CZ_1bw5Xw1NQTGvgoJAHxT0GRn7kFxwrNizJZRb5R6FVtC4eTtDWbjrCcZcxME08K2gQgjIvpp2AcQV0K-C1AA7hMhD2ImyOrr3GzTOPuE50g4uqqdp4pHfKcdx491DZjdtXG6C1VsRcmDaafBcKtaCZbFHrBFUttZcGciQGwGn2M1mrTKYTEtXXivAsqxKmAQhGyFKDbfhOQmwincCT1AM31gcDHQUkQcMBCZd27qq0c1elnbsqfcuDs4r5SD_0CAPVjdQcprO60XDejx88Z1NVSTSUlUqCVZUenFR9fU4_FrGSlERKMOkSYHJmWXpgdOXI0hO8R0trNFA7kp5XCY-fDFu4vtKB6ysduL7SPq8SzsaoEjlygrq2w1xbBGX0fpkv6pPClib31Wx9TOZHfcLJfIcFiOZpcKihPNg41JLyXoOBVTtZSnJtlIUJ3ZALGxecJ93GhRtnJ3hxE-4Ebzr1-cHcuGZlgriVlPznTXFs0vUp7Ssd3ckHLQg5VF9RndF61QrAFPibqUnKAaNmQK0YxLFEIlZqRWqDAjmYtew4IGez63BRludueR-lOIDGNDSyISl3Q_hEB4ykRyatjE5QZTyxz097OGoIxFK7QrLjHP0qdPLcui3XVgsp9Qq_6CPPjEboUVmvkRQEDKk0obz08nEyLrLsTw8g3ALHP5b8cqSkuIZTAz1yfq3zuU8NAt4IkLux7sYkq_CoMF4I2QUe9uRx-kGWmN3Pkos_S678qHKRtVc6WXulk8VXOkhulYWcHgJYUa6TW8oKtuoHHIItWjxDK2s0DE5vnHI0HJkVtDrOZCioInnhes1LNymc0cX7oKy9D0I4cY0u8j41DrZSlzJxVY3qUiY-tFFdglDa-7h8Kpe4u5v9--bzvXgLuXBDDgWR_cdH25BpiG41-OWA4bwmfuLLnSuqECicQ_6ENyGvCZ9HTUNcGicMMjuT95yCByjT0BcCtriun5c9xFttwo9x35vwuvfdTbfcHTDtjlED2NzFKwZW8dmwqdwVix3qlWuomFnF8DT5fJOkvN_O4HU9OduRZMbzhm5cAnh1hWL7zDBdoTRjlmwgAHjYGggKefMhKhUlE4VqaAQMrkdKRUFLj2JnKGDvF5Lh1tt-qd9yD52P15IfEwsl_Cy5_I1rgXIrAEI_c10B-VI0R0ERHAW2j0SqsUt9DzSXGHrqexArFWMrfWpT26t0WXgXysK7UA48COXAg1AOHpkUyZMBsqFQ867iQSjNaa2GeZbJEaG-Qy2iP4KjgqW-kLzigqG-EGuuGoYDg5fei9Y24toMDlE45XGCPqLpCU2wV8TIFDpdqFSscQ2Jytlh0uicLe4cpbOrMsXf9AqRV1DidTp-WeKDQQLlj2aQwGt8IS6vQMvRPK8_0ykbEz1H2z2sKZsHMxxt2yj95MDCilFa5MU-XgZoXrs8u_ecppuBPxv9-lh7FcZKyOQaKSz5ORdXtVS-ForSC1wcCozpefWqsI4Xeipk9QLhdqdeYAtOi-5HBMU3gA3NMSusI1bJmFsFKLTBvopVZoOcDf45uEz24pT9kE4A3f05culnVIyb0E-qGNCvWZOA8csXX6i10BXaai8wjIpRoxbaqZCCFmRgjVlQKlrYhwh8umHG_ZPIgoR55_XpM6VPhKiG4CStVAycpjXO_sFKO-XKgxU8u3GhiiAOUVdGVQS7MmgrkiqCqAdaSxvgXmvBPmrhCWRxF2AtewbMoqYT4SQvXwUDUlecxgCH2lBcRhtUV70wWDsCxbQX8zC7j1-Yoa4H4XbORlKW8HBpOD2N9ZoOaFA60qgK4UnyHVPWmRDRqIg1fbXWNMtfjcTSYavPl4Y1Fc3UVmvI2DZzCGC_jB4je--N5NOIXnzT_9pm0R1SdGtMn4gA_QhnpqmH5aLCW1WvCl3SU7SN6oeNHqJtNMTGjjcaIs5AOdAQsemFhvgupcG0O488Cl1lMvgy6y3XFKeKFaanZ8zbV4W2iQNBSVEzbkCSjDyAFVjOjyAQuzSbE4I2NifsudO0TzZXPLkIB0U13hrmhbvJOEicfscQSJHodXO8m7Pew-hJvQVNxPzoSzKY2p8j96N52PAM_OsV45v6TyrGRnEVwXRKgIWrCIArfdjoH6xFDLfFuFQMyLk6sghUgkHXNLpbBoew1FXN_vUaDmONs08RhyxDcgHhMVT1YUNeIDXeRqNTkPpSYzbtPYyqMZv-IYOdGmxaEMIp6lJjrsa1-Dy4ufJL2H_i439WjWsbXGJc22IS49oedzKubatMKlivkHkFRWVr-M4s8UFrAgHOcq4HolpReTda1nAvBiWKB3fRKooHRYIaxYMHZqd5mYvs2B3aVvPCU7pDi2d922SLQ7TNKzqOdti3rVZoynoMDS-uJ7jhtZxzupZZnaqIeknBTrh5alIdW5p1sKbpAXCKhNtjTdu8haiV1mLiZWSGRhu6ZiMZGgGet4ZGS172iaqeVd_pIegoHxD26NpouoBmVFUX-51HcoVvKNNH92Pqkf3Rp1Mwhh9TkqzioIRuSwDFQYkru1Bu7M5BCd1Zqj_jIr76z8PtzWVs1z-s7nrE_SLD9IdNOMKl3mP3bkkc5SosBdrCKN5Bv3EdwzOMKET04pBExFE5iaR_q-w2iLPIbtOGudJxwJxmCzzL6S-Gfg1nM1GWp8jxGpMT7ZU_7s2yZ8zHx_er0x7UgikoJz5hqMCg8ydWpuapXtO_UX3jug_kN0ifFGPHpaE2NYTnRr5q7Mqbvy8T5Pkb1bhfNe3ALt_eAFh9ugPUF_DQd5tKfi9j7cbVx5tbaABVbloSeEPlpqf8RggiBtEJ1d0gO99Ho9F5oI1i0ynvgwBTfR-E8O37IGxBfR8ECFQ-ieYkR18JafYkEBfuZ51gLv2o9HHKUxHotgQQvF0EsJItbuvQcluHVts1okdLhoLVXalq6SYCg5S14wK-SilObpysKe5mRzkMni2nv2NBqjJs4RFzBx4xd-ARc_t3LA1HlEI4Oz_kMgb3kWuueNm9RFZpBdfe9uLUWBVuNvSTAgfXlTNK-whPDC77vwCpP8odHWC0qY4l5YiPzkLiAhtkuY54cf35ckSr82vgFi10LtWDGB1kivDvPxpJghIi5G0gKHODJY16iZDBftwXLKEqePTmpZ-A4zNi8PcJm47YZlw0n-vxSoVw6xNohLXM4afiEIffkg5adVSkyTzTMH_5j00ldM8a7BO9hqW0hQrgAVT6QpMmNpm-fuMFaNojLliADVhVzdzyARmeNlUzc_sXYihG0bZEIZ5BtOb3GptrDr9tRqMmAFa47xJ6dBm9ld87g6_aNXzmLo66mG_ft1B0I0AwSpASaGLedTKSIuaJwUa0OACC9FCb9xQ-SxTVnvKE-jhoWYH0hdOcL0mmd-4HnknflH7y8Sb0_-fI_awfy_-Qlw2USa-kP4R2JICS_hDmTQWsVFq_9Un5rU_K731Sws5EEUhsMn7hm_J6tkY8KjJMz9aI07PURP2Bv8sf-Lv8Plsj0BFfZgNs9TIbp2hAES04nn8ciS8KJ8q_kcQYi4SYJUTeB_BbeRM_wb7Er09-okB1v9Ua1xC_1Q6T-q22uLPfaldlUlt6hcwrKH6rhn_f2Vb4EEPkM0Qcxa_lZ4qaLJ3ighAOEQ-FWs64qOlagV_mjkRaTdvAYiUpJAPrNiiPsTQLTcerXrSGETcaDx5U1fbE0HZOKu_O0M48Y8hozqjtHvLjGPZfqsCGTzC332SqIfXR31e__CYBKNd9BXCEHYJ9DvZLnPeinZCqBtWNf7hDh0d8MYAhsytiAc3kisZY4WGtMIaezlHSF0PPJymqixUedG0xNI8aToVkaQQs_hFLQV1kI0f_oKCCBulNoUITlKTwQw_tbkI_qlqGn_WyhR_yAYIGFTQvW9C8bEHzsoWtly1svWxB97KBNhMW3rOw8J6FhfcsHHjPwoH3LBx4zwJxlzRNC0Rq8IVcT7dhucBRJrkKvqZg3Se9MvrJyvx-pKnmY5VcTcHg--_nzF2mIc-epnCTew3FxpqPqzfROqmzoyk8ITB14t3HuNJw2VtxELNh_JoThSkZ4kDuhKMPLuEibtGOno8h2i72JFBn47ZvW7SjN2tIbdu3_lm9peEkNL_r3sgSWjDHPsgZ2k8LmV2FLSBeS4lckVIci-2iWBPyIKlis7uvxF8cH8KtrSXRjERFa0n82Vdn8WcdbvFnHW5Rc7hFzeEWNYdb3Drc4tbhFlcON7KGhu8eYngQFrkZHuagkLhwieHuEmELl1g8cInFA5dYPHCJRf4IbHGtj_wpmH6tj_xB2OJazyuIBoNVhXPDwT1dn_p0xuZHXC7sBktUajbYIFKrwQ51Nhpsakw2gI4fGL5iMujoeYUO-kocnxGtLvpx7ZyIa9dGXDtGouLaYGDRHhAJS1rc9YE3yRhkfHoPqd9Nl9_x7O1UPHs7Fc_eTsWzt1PRk9O1bXTr54lpPSN8cZYqRTUDxBzq5i2J8bxOJD8BPJJy8GrrKbYLff27RfUPrqFEUv1J2kOi_pZVmE6i38iQbvuph2BPjVykkV10a0KTr2I1SMMDrrmnsRGQnvunIoNBtUg_-0gp_ewjpfSzzp30nUdKK5dY-tlHSknzEiXNS5Q0L1HSvERp6yVKWy9R2nqJku4lAg0nLdw_aeH-SQv3T1q4f9KB-ycduH_SgfsnHbh_EjGbE_dPJG0sQ26T-LxIQll_ULKaQVJGM8jT2NoGgkx38zQ5DY9sBmUjrb0Kae0-SMoHq1ByVepr9wHwKN05kM6s_rjaO7Qzq386ew-Tynca3Ur6tHlv1OZs-82phrl1DXjCS8qYmA_3h_DpT0-4lPxp0Aee8dMVqCvk6QtUtTjLxfz7VYBGXQm5x8WPshpA5uipdG5mI0XDyLkjSLoMwM1Wl8nd-SFpCFn5Dhbrp6pA5J_yBeQfTboHoxIEYdas7VmztuettT1vre1Zt7ZDVHHbuGJUsQyF0D4ZCEIpL6z4eWHFzwsrfj6w4ucDK34-sOJnyYrPYMuP-7R-SAITt97PPvbKP-SGgnjf3Iy871boG4fD1XjhvjuW8GsNV5Px5PWniHAfad8SQjh58yMlKUS0q6OJEcnZjmhSdHPWPq8EEb_ZHX0gKGb8eNHyq0QNTckTNDSaNm3ix4vYR1F_a2HE83axE1D8GpGwUWbgtQAOWYwgclnYfl1ZzIpfjYFN2YTu5vWzqbx2aOUzp1HeeINQpcob_xgqSkBv_MACKh4Fs1S1gmYxaoigKFRIUoMbKjyvgxuKb0QkZaR0C9NEvoI2XzrAZpoJStF6ChrrxsEweOL9cLTmWJLENTWkyr9eeSk_-oETmNKfI_ez_qOi-Y-K5j8qmv-o6L4fT_flgzHkqK4aRFk4c8rCmVMWzpxykKkPz4OafB40lEKs42O0ZyGG9THaE5h9udZfxwNnSbncx7x5M5L9pNTiCQVnyRLVEGfJBpE6S3aos7NkU2PyfjR8dzF8zVvS8d0SH-QM8lZNzkCIRQkJotlugR7ji-Zem8mcjz-WaD72jd3I18dSZCMEhTw5lr19sc3nodCw6VcBIuCGT66U3Sc523FpuAmIrbJ5WlLQ-aN_hBto6EGbeGBUCVpf8kJXrnbjB-9Asb6-1L7vup9QHDuR5LA5wSbwiqB0XUQELazt6SKOA8ncjghJtbeniyT-EgRYukgaM0kcI0JaptCFIf2UCIBx_Ry5HzXMw5T8lICCuZvFDcyCBFDs6dCzKCxzs_6N1plXiABcFDC4yWSYbufGnSNlqcd6TpI0CLxEUcNw0oCCLFVAHZ8pLFDXzxWqELtnrHzM-wET6z7RQ6Z0Lr40JCK4dBQishZIk7DScUex0zHLi2ia2FHeO_YK8bsV3LcrrPGrJMT1Kor5Hre1cpcCsOZqh42_AevES9-hKlgnrufvih1e9EgDZCsrDKdhZDq5ASUbVLBmNINDbyTApcnCtwmDVuXOqQy3dM8CyUAOjeUFESgLrnVsRs0Hhc1Y0owo88wPpTVCcq_69d0nkHakEv5VVECOGMUcDdMqASZzNKSJwDWud3HraFcR0LqWputrF5ZVqBjdPoxrLsP0T6ZgPQxR4yLBUIOQIowMtTZtRIwhxqcj1CPBZZjZWUatTtRkzOJ5FvGgtsOpMVejJOhhYJU9GpIsSgXrxNffBUf4tbooQA-TiOPJObB-4mvAhPzwlidZYjCiVq9kW7iwosoDghf4DaFck_8Aj7Op0xScXcn-aJgw9PDbb7R1Uj_Xsx-1GMG8CUzOKhYj6AADVLZjdYMPrrgMkww-wK5sv5jO7MqmfqwUbmXJ_XjDViy5jmuo1XGCO3_OE1OdGAivhmY7lm9cF_cibZKyoKZsTfo8Bo77cp1D4tp09T-qKD2zyJIK6tkazqTTIqKhiAsdGPH9Br8mFVH6gR7D0HehykztMiYW96kKVuJxkctaNGvrQS54sEg2kGqEwbbjYIS5iebP9T5hvq8ixRtySt5caclhqMhvNfsUL3knzrEEAIlFViSHd9DOwh0q9IpiWuFrxwXQ1RwXAMbPlyahkYpgpvePSXNiQL86TdmVAfMjSBMOFxKMIMI6QwnAkyQB3dgAfHHZDfWKVI00O2rybhyY9JwGEH5MGrmftTa5H81AAuQOpfi9cAtK7htXDe2m4TTjl9OMX04zfjklmBQPgB7vgqdqhbEKJrWEv4hfXa5BM50FSUEzChSEllK1PimSjjhetV1XKtzCyOcWRj63MPI5PZgVe7WKqcGZX-Msg1nZ5K1ic6At0buFY1T0JjrJ9BVRn2lObM470tiugOiMhCgqZFDBf7dC2lcAPc71lwJcj2NwNR8gwsMGnlW4U-iDvugc658SWYNkQkeTkv3haBPJzjfRq4hhyvY3UKx59cb5EfLqCVMwAwMH1ugUafK7IubW1ky3toW6dawywNVYZdKEquoRLUn24GFXNuHArMc7Px422P14WM6ynTGeiUqgZ5GKg4bowTrTdTNPPlEtyABDkMysRYK25kODS0qUtw0smhG87X1YtR_7mCTrJ_aSvVsWVEzeq6ipmDg2ljIkjfWp_Bo6ZWWsX6AnWqUTfIr4DKud9bSzkRSAiVP61E5be12D8Luo27Z2etIyqe9N0E9vh7FTS-Hq94Gv2LGlAPaiYRZg3fgqfRgHR0Vf_iqS1dPsBSvJ6nms7V5Q4jx8s8KJ7AaPoS_3nbU-oQ0f17_wN4e9rFEN8R5uEKkPcYc6exI3NSbDRcfPDF8Je-n49ySu8MEeDQxXM0fjJt1IDTwTZ2ibBy6ItvtCC54A_dWupwfFyTLJEy4RJ5M0sKfRIh3AINj-9iMHYXbqkBiLVsQDYOWdeACOG_xIVHS5BUOobmwb2F3DKU88NWi23KDZcsNky8W20EymiN4K19--AuX5bQpreCUgwi47BQx3KULmxRCuimFhtw4Lu3XQAxVxfEtJE_ZZJ3CMy6teYIZrfo8L9Gn8UtoE-p59JW2w28fCo1b4xsUPKhxc_FCGtCY6BwVeoMSvN9bA0jDws_cbhvWlJ4HTm8vaXO7jQk0eYSvfrpKkENtGuMrHv583uezH1e-KVGwMGOx5pG7k9CkGkC9HkA0hXaMcb2T3sZWy-VyBIDvWD0_5RTMzt3K0ceu3Etx2G4kQaK6RhUQINIkJiZL3hBfUd4OSKKiZiqDbYwIjHGWcV_B1vKR34mJ-g4TqvRZjNjnMP-eYsDIS21lZXrw6y1NvPPFqbE_kWXEyAQK_jO3mJt-FIsmsLXLkSDNriyw50i-nLtoQEicgaJM4gQ2yM-Z3rt-THdF-PRVJwSmVBcaFJS-uLGHYGzf1BmuvbW2mD1ysvbTmQe2i1l7aC2nPBU9rlD_MSTosVRItm7SfUiXJ5GmxE_XzSd6Xz5t__rGEp6_r1r3CzSKerzt9YVxLePV4d_GKiVV8lDb_VCx2qFcuXvHhsKRieJpM5W0yD1WRKcNAgmE2ZbBVy0CwRstAsODF_t3ji505DncqvBzBZ_pgqouefZjLTIdoxHMKHgTMxLD7gnqh2JanavxljYwP9r55NHYCOg4EY6AwDxPQDcCLHJV7iz48x-ZICkOXQZWR0DfvjnAn3GsPhdCL3yS3tdjFdyOldH2hUXAshffk7AaS-Av4mSWn9mhyEgtJIW0A0gxIk4GKIYQdgvAgzFTmkHKnAMzt_XhLSmEC1G0INeprgElEAk79Uhe2PAo7SxrIVJcASJvRuRKdkdGrnZV-ZzO1V-1w-SIDys2PC1uyU8LdSOc0tQOjz7s_wDEyzjiRDW2YSzo2SmocXoPVEdLF9_ioQ1j_CZZVGJs13_WXaec07WaOS7BsMF0_GQ9LGoBsd2iPVPjuukYNC7h1oo-XdfjM7cGRQzbv1SecA3vgpei7ST6NAkGSBw3NyWgoNlOONfbk4aAXe_aU-r3k_u9TCmLZeiPp16dU7quNQbB51YzaSLWNpRI_7yOjJ08uPCd8VsCgQpJec69Q49ZH7536TsaMDNw3x6sdp7f0Zay52komPP6Kl-Ja1z_-CxtqrDKf02xGjgLHJ_PgSXZ6K4JnCP0kAEnCQ9_9OVXC7V1PcjaUUdF7tBk7ZZQTRvtlxrMtwaQeEF4yd4JM2cg82Ng5FxjXwytnmCHV4FJe5N6KvzULZSYPmv1YzOWIH_fRQ8zTHjLg23gwZM5erenhEsD1jBuBv-4rKk4VB9-TD5PRMRjxhvCrt4CG73zr0vXFYvrcNzLAs9NpCn6LPWNd6eP9c_npGFO7I858gku42s7pp7LUSKYJj-1-hmsjOcEUYPLc2kVfD08VTBlXFqoVsq3G_szsoVwaF2AkjLKjGRXl2HJCTC_i1Wu0dWUExdEiitXOCMdsxby5SVACEymx0-dFUuSaoQiY3aka8wDUPIWdrwhwp8Ir6SEdgSHadGm2k0nNKuTR6FRqRU0Xmy36iIraI9yI8AzJTJ2cJp3VyOQT9s7Sepjv4r3k_KIZcut9rl2W6lZ4vu1xedNUGTBBxjLglfDxJZR27QSaAno1QnwP3e57UfHSQzcek3Uv3e3grD8lW9EvnWzlI_nycKMbi9LIkN5F7_jT_Sxfwl2Ig-atQokm-fTmqz0Bmm8eFEE4LdhwfWKtQeVTU3VwJO7IXYwBYphOGoBe3lzYBAhnL1--D0_gFggvKrze_S7h7PLGpdOdXr37bsN8QPS_houxOOXP7FqDySePCn9Jt2fEmN_Jc7jwhWvsRsmsG2ukZwjT5RNnQrJJs6nK6mTy8V6ClGU0irTURdjJFeGJMWl76vkoGGWN7y0IfVCVi96tsIxThIsWzkRLqfivb-q1LGJAngWHUv3AmGdYPiOWM19_-Kt_-7_--L_-zX_4h__4b__v__I3f_EXTzfqAYqvjdI1TeqmZj7ejAigEc5YwHJMqXpHkx7ZeJA3zLaV7-rAfqhyBti3rJl1YKiSwsTKWKOkG49-RLWLqdhAcTuYPGwJeJ4xMhiJ0pGotS_xvQNrX0RhjSQJA1ggtKMwSOMPoAr7ZPRXiknDEU4MNqGDJK7G6cpaDd13pW0ntjOmpQMWYMjjwNFwwzCiwBSxb0kfGOGXIpMyhGHKCOlw4Jd6KsRdBVzKRMeNPgPrmXgVLBVm2iVsjat6gHiqkyGT2OlkiHeokyH6QicDtmMdXOc4k7K52gVGRGemqoazLDjZtgUMjwcaSc8Mw0KUfd-EtvSSNJawrj4Go_A19e65wDKOZ1tYsMhtGDGth52Nsm6S4iQW86mJeGKR9jUWU2zXNvNUg4Hq8tdKp_cIQN9eT3DyDjV4RN9r8HXW-9DZYpBihu3mg98hdEIc2TepMF_8YP0RsboBiOG8msdgIeJtNQzSDAzBgV29b1dX4_bIwrv2gH7cpFh73p8uTHVgi7o8Q6ru6vJ0UkHwuR4tTgpDNyhSWmIrzfr2GjSB4BC9PuJIEe4NhzZjRuHFzaLYfe0suyCD2YQJYDprMx_hU9eSeE3z19N7KZM4tCzOZPcjb6ZzichazZLhTNy91g4cF_rkClc9GTZdBp_ZnkBrJy3GHJ2_oWfk6eYno-MkOI-mZHC_3K2VTwNXZ-LkfbmRDCSde7BeGu97aE_ndrJWM3C3cYOHJTtfzbPok-kcRHfLtErolkF7hVijzrKrjhywJ_dWFAP0tsKMCPZljFAYDdEbRGaNfvH9q8QWsoN8ZF5_OLB-TocKe4jizyfZa5Gz9SR7r4DrSfZeYDj9vNZOBfn64NOCGYrtDsfTG4JDitOeCZJ-o1EndpImjPPzMJSRB4yTpOHQmZJ4yTQjC_amtyTODUUcJ-iArY19FqaKXJbpfrXXoDr65sUSVce6R_E2LEoFoOCFyQG21_YD54Z9KqAceJFvV2zOo3y7uEsON9JVIQC1MiZfmsmhze3Ei3y7aImch4PnesV_xi7IFCbEqaPWIsrc2zcoLQe4XBDzZ8CUDbLzs26GWq-Q7VGrFZKfk918Qv--F5YUja8Fq2ly4SIbgZ3U4GYIjGe-c8HGq31SmFjtnga8Fhxu7MpwgIP0Na8RR_qk14iD3rrFOW9jlzUXOPC4CdhQYu-HUVdC1RODU8B0TVQ9EYgv2KVIX5zoA5Yamvd0wzZ5tw_mVLhRV14TrKgsQq2Rx4ae4YTqkIyItr1Fvsq22XKG6D4Q1ouyDJFzMu4AmQFH1lDCchhlJx7aSeDioS_rUN4_14RM0170HEixvQqwcDYqxfYisJnzW_c5XIv2QrgW24vwS4W7DX23ob-OLUa4FFtcCFwPOKOsqmNJ3whBrHkueVs08o0h1Q6bo2DnjqcFO9eIY2EB7AQsK6AYyCysux03sRzILO2YkfW8ktgSVvU8k2JByDlW563nGbf7mY718eQk8YG5xDHmUzFIRDEAtK5AlKI1SRNhaKLLpyiGjgLVIFCd1ZBIndc9Cq1utIh2KPlSEKlNpwsQsdYgZEjVX6rIiixeeDmzc_g0AobQPzqnNBKZTqhELIzEZnUgNqOTfo-Kk_FmnJO1oCoSEWEG2e5-8tGzeHq6ta1j4optCx4qX49dWzl-GvtaDeWjMQfYTjRT6K2YiKUjjolWGyJcfGJ7cS3abmJLK8ptNzF-M5621VjG0-J9J6ZvRAyusWcsPWJQx5kiBnNyr1dGZYGpJtHULj1pFQcL5ypNcbAzJ4JuaDHl9YwlMxLqx2KAtUGQQT75CoSTUIdgo6S4JTX2mI1ND0vsTW9GxcKX0wQYxjMEzopQEra7KBoieWHEfjQspBSmYbxjqJ6PpETfVs8JhU5Ok7SJzJ0QiN8FBpxfeW-EPVyHCnD1vk-IPDkq-OujxIDqKuYpThOL1DhNRJAc6VmPs8T-TlG0vWjep5mFqdKivgGgeB0hmvun0AY479cUFMy6OEXTwXnLLMCWFrnhHpmF6GIKyMT1kqW4UyxdhncgjrRCetwp9lyIhOUgJf4UwHkeG4lkhYCG-xwrAQ0gJ6eIBmh7H9GQe5TpgdM3kyDThSs5fyvGFNEX8QzAoPJFJttP5dfEuOoZLVL8ZdXWaCg0i57sMdTkoljv10UORiyrYEQ4N4VGs9ZdUoSAVdh9ZRNZWfTISj5yITSVkaCRoO1zfsrRL6vo26IFyr6HsSyDXcsq2BU7p4epInHxqFIMiXMW7SEbLO1JgGdZBnjittrEmhY11hSHqUaI4pLr8a5IQo8h5Y0IcaC8kUWQaRGCTCszg7qrGFLErLs3Oy0Qf2J75ZtBp-Uo6LSIQad_cn_4pz_99z-YP7m_-Zf_82__9C9PN-pGTi_zGsJJcxkQbKKOJIb1HqbSA6vHKL9MwPJpQujqNDWc-TQ14uqDC8DoAbC803LQJ635Cr5mjKh9XsV6InQ9IjVSsxFX-QNizAGxL-Mr1xjaCwNexskiUWU-COE5ALgDJJ9jpb6L9EQMPdITMcQ520R6FhoaLyPsIj3LJYa48mJhzrT4UAIRApMrm7h7NVgcNDbx6iewYQ91JDwYGx0J0Q51JERfxny-M2PsEKpeTDWMmhFndI0DYo-qhBlFiotP5gIiOuBIdIxwny6mbPBYw8QKtmi1Yy2a1JBw_EO4LDYwhXTSYhaPyAlNwbO1K1MoKC0eyNXlEkMlsXgOlZwrsGLWRNWgYRJHiwDO7WQRQHB3Y0jlsz7-WgNw_S1GRI3WgAlhisJEjHXEp4g1GhaKNUM0dLH1S1cTzhiGDIg9KA54rrXS_v4NbT4SnK8fdmmRWuq1ckMHGCXNjA64XuhVla-WuBMmFGguC831LWzzpgUhSbJsaYcpYud1nD_OF8bJ60b21cx1H_hm2jo5ac76F8XFCaOtjLOlEc4SYcrXfGYrwmzyZIbw2JS6j4P_uBYbeb1HoSYOtLW_z8d9EWxBKe8BlhGQ8se1MMzLDofftq-cCcWz8R7M2TfV74U-bivMiJvQxw3i7CcoNn0_KrRV2keFAvNxTcPEaIq3FIzyRJVyY5Ba34iIPbsYASS6GAG2czFCw3ImmfL_c_YvLZTsPJQg-ocyN-G3zUcNetBwqfmtCzW50A9o6K5JQ___3hGWbMmW5NjJgTyZsZYf4bAlWZK9g3gLBnu3ktaXGNmjWIk7iMyw2soyAQdn66QTRQp7Be6kp2_OomhsxLcAJD7fApAIWAFI6GZM5ImZ_zoHfwQi-XDSAlsLRW-BxytpM1Kwl1atZ3HRF2L3zihfQei8VLbxskqr5GKa9YLiZYJMR6FVUnx3KL78khqR22OsuTif7zUklmfLWkr1UcCe6qOAPRUGP6eWaoOFT7iWaoP163hB-9HCy6G4lqmEK80r2TfYPTuHB1lSDk9vhf9unZae04J_lcYzeVoaz9JuOdBaWWlSdH7w6Gssv34SijyqfgXl9CFhOu1gsEAxZWmfJZ6sqH0Gb-CSd-TTxFiW0fM8Ntw7b9u3mKhByjYQ8cINxA61vmvne75e13J7F1STl31rxG22sRWP2eCAbIxh5Uy5tmBcOY7OWluYiFmPdn4V8GQzJZI8ZmHbEsm1Qof6Bb0caRbvxMjr72F9BoyLKMmI68kEZLpoHo_YtCpgAo8x5fN6doqL-3hti4HJy3ht64FJwnixVShnRiLLzGls0Z9ygVaiml3U_Wax-U9fB89vcE5HG5wM67ubSgoU1y83z-nTWilDB2SFPxqo961_tbscYRyOjfjgP5iOjG-xNQQ7qIg_scQ3VhGT7KXHqusmjp-Tkg6RtBSp642Jsw65xbc0nsrpF5QkWnBYuTOSXhAlGS7GoyWZoKWETjkecEegEmGQMPq7PVou_GqpUiZ9NJj9k6cK86S-m4cpwxQJ7-bh4N97-OOUWvp0_5Dsee7C1Ek0nL4_37x-8PHypVwkR1GS6TIf7RNp1sUd_BB1yENgUalA4xJ5iLyNAaI9u_lh-SPhh7yxVgw_bY-MAEjOeyiDIWtoZC0gDM8oaGTatRznGAinBhFXY9FIkCJHmSQYiYGhzBMvBIIegYR1lpfg14bkxSzpYWWsOZ4vMRqT4HwVXA8otxzUgHJPFnkiwF9aeF5ueHwevF9eRwj8pApl-eeHjVu_cdzFdlPSWNe5oixw6VnXB9kx-PWF6ACpWdZgUZ-zRQ4tlS0GVWacCr5aOYWFQBKXU5gJVgG2oHvRGfEUZypv4kzlTZypYPYat-LLyHbbr1xBkE92iqyTHYc5ibEjeCzEjo7I2lCfQuVCVZLf6beCdpMPLwugzeTqq0k9Gsjv7bLZhnutRMebhx90aJHjbcWOtxUlz43DUzHDx65y9Kou0SvGpvGwOuNhsKOqdiCoHgJB9U0gCFr6IcJVDxGuakW4us6rh9CdgPNFUs-hO9qSEuFaQamNU4Srvo1wVSvCVQ8RrmpEuKoV4aovI1x1DWXhc5wY8BiCUzXnT3UPcsdX1itEvvh3IXrAUz-9vt0h8q3cf0rDWh7tOu8Qgaa3jVg9RLjqrxGuU4GdeIpw2UQhWFX_JcJVf41wtSXCxR7T4FATIlwMmreOPJ-kkSAZTFh8JETCmhEJa-8iYc2KhDX5PngATxGudopwNTPC1cwIV1MjXE2NcDUlwtW0CFd7EeFqdjyrzUATDcKIpZYQDCkqxWmgeRr-4t9lizYxbmNcM8LV1AjX-hWENtUI1yxrvZkR4ZoTZIlwKSXX4X0X4WpbKAuer3ejgLzG5SpGuGQQIlwyCC7NxiJc23M1MtUOka92iHy1Q2Sr2ZGtdohstUNkq-kxK4pPT64YOmpixErAD5Gq9jJC1t5FtNoeqbJoRWZB8GgfSb-CSjRrnyI7KEez9smxgXI0S5h1G7iEup4z84Bt0azvUKjhrNuU0OJZX0wNaGGdS0QLq1tCWljTEtPCSsyg1vcf56jW9x96WGsDmUokXTYCW_gGx8gWEkW7Y4yoEtsauBncGqxNJw9ECm_hMOzxLY4w5xuOvh7hojNIcbaMCajGuOiLs_lOxp2J_PFck92DoMW5BkENYTEWEaJrPAya20NYgrwiPOXHqR43xD1i4c9PUahR5F1k7Lbb3ec5ufImaLWUOUbT-iYKR2_dXOGEWD0mYxoolwKMibS5VFDOOCUsNJBKhZWz4lMDzok-U0NpiC2xNPLY8FrSinUK6Q-uLMdiW33Kjiu997Ik4MXKL4GwUQcPe30fZ5g0r8NSd1WP9_tVUIoVeRdbm506xtZwFjk55jUAJeh1W7FG1GvAjU4YrwWEBtTWCePVUNlenaAvvRZJGdDmXeaVa5GmpX_K7x998S3Y9n1mh6cGQYhPLdWqAbvBIBG7Ua8Q1vpih7jWYKiBrdGmFNlaumBSTjHOQZGCnD0U9qV45WS5fFP-3bOXR6d6QOxb4FVE7ObZITFknGJi9ztJQbHv8wqS6FVQbPLfB8XcFcSo2Pe5GBZDftip5ADX95EYMBrPnfS7Il90j-Tgqg7vQm3Yv2Osbfb6FGyjQ6FE22gX9XAb7Z8ab0OWNBKFDqEBaaIvyOGwZfRH4A0mVXhMrpcxL6S_j2B9S4DB9S5KRvivI173yDwv8S7Sh_QfwmNj7Hd9G8wQ2JgQSgQN8WlG4ueKYozs-1wMkiE_po1KwmTfR2acDHE1YoWEN5EybP9VqAzJVsvHYNmXY0fLRMKyiuLLo25jKHikifdEjqbtKPNjj1c1I2X0e60RLT6mcrxrlNUDXsvI7yE3PmZq_A_3wWM-LPvj-broMI1sHNStVFziXhBV-gJvw0opvuD_Z0SSvszXoaStyNuQ1fIKL4Nz5xJEWCQxvoXPCzX-kxrhQmwejYLpm9SIFmKKaym9immRdoW41Rc1olpf9BDWmoOwM3gdSjScjq64upMcphqA4BNLZqCKvbcQMqMfcfYGBFUy41tkNFmASynHQzCssBriwuEkMS7yldaA08JunG2FucYAJWkUDsEo-s0zu5hpH3epz1qUTG15G8lDNGtpaY-6obAd320RwrOLPOD1RXz8hL6BkRIUMrC-FiSySJpDz1-4-58nPPIXhrRLLRzvjhwCNX2F7_E2TBCKasVi629uzDzQpY6eL_pEky0bYY5shDmyEebIcpgjy2GOLIc58pswR34T5shWmCObYY78KsyR34Y5shXmyIcwR34V5shqmCNLYQ4-2voNoDjUEoOPsxQqyXuohI-wHAiJlQ_vIkBmrQDAZiv_HgbI_-DXz7-FDvKvYYCshAGyEgbIhzBAVsMAfXkWw0tfZC99eeWlL7qXHidX2c6QjGdh3Y8WNVxRtmMs4xnz_5e01gxvaYc2ynbQxny2hgHKP4QBylsvPfJ_DxuUd2EDHISqu-ar4ppfiwjfv-ru92q73-ubgx5f2smVXg9HPQZDdIrT22Rllze9T1ZhWBfOwQjboZS6nSAiz4SFWQ8xiSrEJOoxoFCFgEI9O_zr2Ztfz958GMS0XZgIje7ufHwh455YdOTXl478enTkd4YvbfdfSP78uvrzfS3kdZ-tyW38kWdgV4deW6nPiuorvuHPGrnwh06fJh6UGM_38xBvoN053GV1M456DJTJA9gcNHp3svf0Wduf8VtlFywZWGWYI9Az2r6RJ5g99Kf8uf7U25Rt3_--WuCZLqBjaO-fI_SzQLgLpKXAnctKGr1GCXc38TTglyLuomP7nHWGIt-JVp_3_ZZqbilWYqTF8stitdExeA7bQrH7ndxX_9xdrLTUvSb2osnPot-Nyn0l091i3Eo-P2O_F0-kw-kuft3jeYnFw168juI1PR13z8dzYvlHPOJM6Wdr0Bfl6eryLZKdN0KZQ1TkIqWpFA8CCxoYt4c-Yo3i-YA3GW84e8JYONefMJbmgEd4YcJUCyCt_UKrJ1p-Rwu_0LS-3W5e2qo_8uhvGYm8TF82wFlKiQn7-DZ-Uu_6E4fJEjbHRBt7gTc8p_GSW1jDu01nOuRzRVa0QeZY-25vHs9D_NqeT8ztmbNUnD9HSTNdLHeuwA3QJz2EtC8N129aFRF_WldQrU2pp6UHDTm3LCmoXXhelefeXKLYWx2vGl47fkG68IeYm_5Rzom-SFt8gM-WnTLgd97EIEulr56JS6GLR0SwjbtEXwf4eh5sKKy5G2Y1BkpKk9Q0TpU441Ur6Qa6JSg0fstuGDAUBbcvffT8SsxjluIQOFg2YGv6JQ_kYj2Btb_LERyxjTAq8GRIVPE2ZteJVlltKqFKHc6UYQk0ZPqz6LvoIBBZQ33DEtx3qd0GvpLEUgUjLgRJsiW3MlAqPs8d7DSeKd_30vgsPBuw7ocePH4IY7yPDPcDHBqKEss5TSL2ciJSVMQdRWSv1qaUoxTtDe1SEV52f16U586WktBbHS8KTsee2PbRo3HPbMth3GOJadz7eFv3WSgB1j0UIdZ9vm3Q7-jtRcC6xyLTuvfBTTO9rBsPsO6xWH5ZrLIZSKz7r3K_LfRLNLG3gsS2v6362zCQSy62PRZPS3ddvot7L5YPe_lp3JdnZxHvt63y1iIS4e9msHG9VKlQRhgpCqti7LOzkC0pPBqVEo3oRvTxmi4K1HId_Oumlnt6JJKiSEJN5R4_gq_TLeK7tdd1FXgeGtEBKIAeoU2fF_Jc0mUOw-6KcnEjCqkR4olQ3hF2_bU0YRCcQhh6Ddp4oQGhsTdMd2CCzf1t3D13FXtXP96hzT3QetvjKX9KzGUY5JXqh2mQ98ceL3Rfn7Rp3uEK8ZiGRp9l1JVbbJ8WamBAjad5PF2WEq4cH2jFtMU2VtmorW_LoQ3eXYiOGH2Dn4qU04pYb0Zd2wDwtAf2kuOXKAFFAdCrLLIA8GGuJQ9DKzGcwEC93N9O0vu9YyLSVCSoyHU0EXw4U46eEuicSOGDSrcQwj7DU5-Bss_onOAkjvP4gcePJIzzifmRJU6CH630tevu4Fz96sNbMoNW9PffLmKowFLbDCBYbvvzZvF1g8grniHeD9VzhP1Uxam_5jwW8fwGN-pvMj6Ukkd3SeyRMZmUYSmapGuuV_1VDdXiLzI39Ze1amgKYXldzN-wlQ40l60Nmqej3IQt09h_efpL4nHWRZmXxFy3RwI8t3tdRQXcoM0nM3Jc7gW2qq6AWZyKlup4NvRRb7Ns6ijsPoPAfAa8C090BLooKyXglZmEjHRZNYUq1EukZu-4JLiD6ngLquMtnB1v4ex4C7rjDSV5bwg14CKdg5_LXlaPnfFXpmQ6cA0ofJiX2ioqeU4V2qUuFpH51R55-XCPelg_86NKlmrr2w4MxSUwUbYHxYUZFBdmOLgwg-LCxK1IuHArEkmU9i_bjxBhFxTX2JB1gZ4zUEk05V8Rq4F7zwRCekfQRXvwCmFI43B2lyEzHZkXlVlDbs_PQoUypcIM30R8pA1LnOTWBh3bmYTWHvd0S5_r9pL1rUnoG_fYnVqZCMuoZgxTXNqKo1iMjtRAay7b9qE_WrcPcVyGoUlqYBjbh1EXplWwVU6rckTorxwQmlH1_UXV9xdV3188-_7i2fcXdd8fitloOTri6ujYBVpc3Ry7JIuKGzEqbsR4cCPGgxsxKm5ElHRxehk0p8v1oT6XePKtxHgiHJwv8Mr-RNBFWDx5Z-LJt4LDdiIYTRjeGdYJI4BLZ6W_ztGJSG9H2s1VARaiEwtLtI49ZU6ZvccxqICjwt1mDrmth0YivSBKDRpTWpfyya3tQNykP0-wibuGuZ6IJ4k9IV6nSsp2Kbw_ZwI7oUNl0RmoEdJFcPoc96CLpkhtPJelempKH8Q0I3yVNH1IvGeSdylRvxPf0qQwurfU1tZ2YglLbVSZhaXnwgmTsLbYRJykKYtQOVZedm2LUTWcRUpUTYYxqiajqMWS6ilLqtcrqT60pPrQ0tlTls6esnT2lCXdUzacV7Di7gxDdF5185v29R4x5ry6TTZwXv0NM6bTvVeo8hPx-mwqHyvWNX5nmHu3lGkla_l8NCqAIjtY6UscXImJOn0UV2LnyK5E_j60HqmxbFXEOFHioPmSgmwWJcWJmA5OwqSlp7WlS-8yJrHA-4xJFOE_ZExikR8zJrHYjxmTWOwfMiah6L9mTGLxf8yYxOI_Z0wOgyxRDy0DMIBh7MzT0eGZhi9fdXjiLO0Cew2A5c2XmMmJcK5y88GLmI9exKx4EfP072xmRh4uJZIZ7WlXBZMi-xVBRTur25Xw7MZ24gs0S1a9iVn1JmbVm5jP3sRseRM97XXDM_XyPjRbDsV8dCjms0Mxb7lpghDvJFuIA8cU4lnx3mXFe5dV7x3Mevh5eN_8Z05wuqvNzN823HeX6L0bZGVl50PiW67za4kLPyeVkOmsg0MeNJMha_6-seebv-Qi7vkEWNjzLaxxeGbfd-URvXjJvCwmXRQzn1jdHea6dFPe9JE-9s1dnxSFesnoBq2MTeJtcbf5nJxF59K1jBNhTwlNyALNDynb2busLXGvjzbnGExbcKQFBEcxesxe3GeWwhgU2c73L7WOo_dEJlNozUsKW5eaJEOL6jksquewqJ7Dovv8Ihv6Ttm2H3n5Ohj96B_x2YSgRC3UY6RJ1E6yJWoZd_7qNjj06EojchLaVk-cq8qzeSrx4kueU3kovYuSQFk2DyesMrxlGqS5ELovxC8rSeDi5iuIOLmbWcdP5Q2XZnFyA0PC9xoaSnguyONWh-R2S26h8bBFca2nW13tk78Snsctat-EFnLuB5Fu234_xio-oMxVbCHXad9XO8g4aIhVR1u7GEwbuGYDWx9JBpgc-egM66jA6GBmddFWGkF4NKRDWpZ-HRvzvWCHMi-Igquqbp6qunmq6uapqpunnt089ezmqWc3Tz0nRPWG-jlyRaZ1SiwnWQXTwuBl2nH7rEelZ8oUoV03p4gQRccVIOoRFJNVSaeqSjpVVTwh9eAJqUq61MAVTwnKxHpIl6qHdKt6SKeq7Q2uy_Sa3-BG_07pWhV99HJMP5NaZLnP5qBE4UNpBKNqOBH4eUeVYHgv6nC6n9O16oujhBcdZS1iJcDC7mVhGXsSOqm2zAG_zUuTMs8rKpq6khgD1dT1ip_nF65K_oSAitpdXZK0Z_PS2vOIPIGzPMjq-_eLXj7YB6ON7AhNIwLjRSpxS6wu2kolCFdsAC2pxBgawZdRQiMyjKERGR19ApcX7cv0ctHXwv0m3uEGOqqpDqOmOoya6jBqZ4dRO6eftXP6WSP-h81b1IhzQ_YWdYbpLWrbaUdBTTbjuCPnyPsfVDxNOcbaFC9SU4-b0vpUl38v_stBKijxy0Eq-ES_HKSCIr8epIJivx6kgmK_H6TqBf_5IBUU_-eDVFD-14NU6Cdsw5dqH45txOcqqc92cBE24tAVtWtbc_8YiJ54I_zQNEdhY_PJ0vDNPsA7efXAA2Xb-OlVQy2PFzx5CqHKqek1TyEdcENFky46egIJpPoSgPFXmftA4kD0VxyaB5QRUNWsQMCNrECodMv1gOeGgkfGWcFDLwQF_7UsiI7cIz2Axy1mQwHhuZA0AZoOCIImhfcREc3zB7VZqhWHyaQccwahFjm8D6AV3keKEd7HXpjqF0mW-sVJZ54U8ld-LvP0fWuabu323CdwOQqHJzXQz5NCbk-2cKlFF5bzQvBlNj2Oi2t_bmdA4mfUMhxH-fQywxE7oklSWqFC0LILcYOEDOs8CzLdidlP93uHzgWySB3cjNIz9h0RER2pGXxq0e2lWlMFTGd8mzxlE3tH08XYPgGhIDrAAJVWttMcYN5pDjDosIgcHWDQoE05OsBwzMQ8p8sv33DZHKVswt3IVGBcJk72K2Gp_bnsb8LZsT-3_Uk4ijpuH__D8VOXuWM5GlrIF8nsZ0nk8x27YHC2S2t2wcRVlxb0ysBtlxh-mkP9uksLXhAaoFII1pzhxsLFZ1KaRuEjbIhWd_B0kS9rE1RXGE60I0G3pLGXb3xlWNv5MA32_A3zOqoXLjPIcUnqjpHg3f-2ssSM8UhfwVtMT4dQOeTj6dC9qY1-U-k4EO0Z89Z5V2r__Z2UPi6Mu8M66GE_MhUse37_zFS57aJx-1R333nfnXy77oVfJ0ONvVrfaLdD_Yy2w6jB9_pb2_Vxh0Cpr-lb0OOyNMirJSjfE2ywlJMtvLu8UfCamw5qEBHNTQe1marbH910UL-5UegNFTdiUrfhxu3zTomMIu0Z_HCRabxMehXNJuupKsr769qxTfA_N2uzAqQ0SdpL3gpfD6mRhYAhtdgtk0RMBy97DvG7qiaHVzyLqAj84dgnEMwTndgJXd146lcVCSfPDjIMz06mTZ3jNjhtlLiNBAt6Y2HpDiJ8Af-CSYdT0RtsOAwvEnawqc0mt1ZITxP50HeS30Io592lIZU-v1aFEsZRq-UiQyihCeYwzjlpezlgvHAWBe54oq0U8v7LDjDsWXco8Ce0K4NZ5XakqUeSxhCSVc_moIJDLEktji_rpGHlQ7GiqAyC6skKqicrqJ6scPZkhbMnK5w9WZ1iaoswfDS2tui8F9oi0JOamrYI1J-iaIvOUV1b0FpwQwUzZdFDDY4K_6C4qYJ8gBcHWFUmQXVjwQpvZdyKihCdqe8PpWCB94dS8NP_cCgFi_x4KAWL_XgoBYv9w6EUKPqvh1Kw-D8eSsHiPx9KQeUe7NRHnI76prtXEEhu-YQig6TaT4eYx7R_s9UNR18manF4azMfncrp8xlkJOrHmpNbm-YbwfD9_73w_cf3g4hPvIgufhIvwo1ezKhleMQoBqpA5mPiLqVZmj5CHtG2YewltK3VYGUQ7WNnJav43q2oGBO9rbju8ngj5J1Grc3SpZHmSPIKl3O-xGoBaM2eBONhGxS-Hd3fQ0xWX0dNI5B2ZCd4PCWp4uuck1RxQu1JqjDKgPS8_AXKUvCNfgRy1TmUyZQD7nUerhz8syM9nh3p8exIj8dMUqCYlkwcbmzbkum8F5ZMfJFKiiQrlRQ5irWT6WCbhw2AZB42QI542GCYTUMqCHvsScJFbmzEo52TihNEtZFiUW6OH-oooove2HHHkw85ag7ehaBfkYSMZSdNd9CwkIOTd9ACLOygF5Z6XQaKoOslsRg8qilo_1ZFSpp0f6LeBldco5b-xdNI-OD6MzGPIhF1CV1eiquRsnwmZxaYgEdp25vR9GHyC_pXgE-qOQ0fg6YcoLMvlAOMVt6VQ0oE4ao00Uv8NlW6vYSsKrehELVMUj236eyFTWcvbDp7YRO9Z0yTnIneMKZIzkSzpWyOeSpgzjFDcCY7xxHHRhWcyW2bS5Rl6eReTJp7cSHoHsykuTiHuExnxyMy65EJUiZtPshd2sHcVzhsUc2Nw3o8tfa6Mp4JHUIqu7noePynczM5yeQz8fAtK7RD6xkjXPTZkYLMss40B21rit4DCo_wHlBcT1l1V2XBXUW7o1mJ-ezMymdnVj47szJJ4tnSsjK4p8a63JcuUKy0rDx8q4YoyY7MfUVMZLrLlrOicXpsbqds3_-GQ6VKhuxVkyplPq_XjH1nwmDmyDDmYeThPTjkZg0DL1N_CJExOVrPDelFumASdPGWtdvmZqeHo_vsJIHaXsRgMjswuhqQOyrYj5xkBO5h2Gcur2YX5tXNIh7RpzTuZck19F_q8vHjwf8zZWyBm7eGiC1Btg9LGyKOPpACKAWzZhYpWSBbhkjJ_oifJoHyUlikXOq2uzBGOZt2JbC69n5mQTUUdBY8TXpO354Nt5NgpRbMDFIM6o5L6cy8m0uwB2RIEZL96GvnjWuqh3L2M5Szn6Gc_QyFJr5oGqDQTBZFA5R5F9-JY2qJoiT5lcPGuxyS74qa3Aery2_25dAfuGZl9aGgIOIUFMRqOV_LhCOiCu9CPSkGQRf_5SLfVyScMs5KPvXhmHFW7B-0nLwLZ-xZE5VTvhnK7FJqVw6fPBzjLvjK-g7LyK3avcvS_rkrHLOZchx-mPpKxHiusB_ezj6O8pqU6rghpOo8GajI4Urv1dqapocP4dG4qxnC1thH-QCkAmPQWimMfaOHMnnvKhlGZPuVzd-RIigTez3UKVyTZe7X87a-nrf19bytr-SKpc3cryRPR7b2O8M6A4mdsCPVle6VFTneOeYdizhuh8YM7wLnHE58wKyi12t26eOWSefZb8P4tyc-qnzPF3565WQm9v39yUws8cPJTCjyy8lMLPLjyUws9uPJTCz288lMKPivJzOx-L-ezMTyP5_MvJjE0jVepbeu0YKaP2voNWDoYWjUa7U9x_Sz__hrXKgDag2E-x7xbX1n46lvpxU00W_3nic6ApCE93dFvxWT7u_ixSEMah7EaTQRjLuXGs0A2_YQbez8Fo3XIgWkzj0E6YfTULA3NVGqqZ6npiZKtbNvqZ19S-3sW2pk17wpm-bmvJSVTSMuDPFHAnBoN4HZlCNy7ZB71A5OoqbkJqGbpg0nyum4TKHnZZqSmzLww0G7VsgSl_Ao40NG9Ar2O2CGKIBptl-8iEOiG-04ZofeWQTN8zT7zz1LqieoDd-x6AoSYMEXtLN2N1AbNyUewojwac_-okbv1rMuf8Qqj2e_2VeXakxubdmRnwYgxd086RGusXFCKQ3Px90z-BwUASA-k21KI1X5QO9i6zI1XMPL89SmSFakJXqv5G2OrbsV7DWrj8INvxKDR28yg2kH8hTti9aCzklZOwhlUdPguOyJQNDcuIxSetHGGFtXfdNVJEh-aF_QLNBvERGOnNIe5a1-6zJKHCD7emD8qiHNnJRVDSHnYhxBGwGxGLxM3qeYbZLziWaT5JziuUm6edl3QUhSrstknCJz6KDaKTVAMlNqkGN36LJHCoPDZAGP6LAfG7TJauRjMxZJbF5mhto2GAUoZFZjA6UCeQ6S0Ku3Ml90-e_qmOOqvoeWT7hqTwyxqelbqKBoV7qxWi7Z7RhgML42xRPGuO5fEXO4eRhwDJ_WLY-vvll9ZthEFxSiw6zzXEKltASYAaFXZQY37joxdUunNVW1jOpg_7W1YBw2Qcb5sAm8APNB8TqaeOMmolWCcEt5aNTtGZQ4eGnrx4iYe9KG4g4D1NQAjrjDHP0RZkRMnxCSLJ8Qzg1RiPGxuhtLf9a8JwTp8KNwcGqiCszYdsjgGzWYHupu9-JXUe4-l-Dd7l1ZogUa6fQwfzc20ql2PtmGzMti0qll2t6UeLJ_6dtQ-5f2iB5mCx52c8M5Hzzsg4nc8ZCq4jaJ1JHWZukyStMHNPkd16XHPTVOxO5LH12SfekKDL50BcWl2puk5pvXXBLQcRE5uiRwxEzK0SUBDcnpLgBa6S5IkdwWjnW0f21XyKMofLAl8wgCzaOA8GjVR_zb410KuIFhHZhiSurFWpSPu3-Kbv2BKsXnWJn0GeSsIwqqFib9kAoJpayX_UPYR8WhDvD7g2RY4P1BMvyaPxwkwyI_HiTDYj8eJMNi_3CQDIr-60EyLP6PB8mw-L8eJBviTjOOce4I1vFFV2JR9Q9ar1iTGfFlM3a9aC-EJc8nhLY-GFkVLPEHiEyJgAIK11BA41FWHkkKrJenCMrnoDoOguo4CNpdVfAGInJMfcFhMSnH1BccRPk3-WhfupdshfIOgbwOM41oqRYJWSOATA9hEER1EZqMoxgfPRCgfIZOta69Yu_N9JT68iqLjYCiiPgwqCQ2n7oKVD_kjvNRURrZSUol15tKLrkS1JqB7Ow3vRvaETwo5bClXglKOci5VzAA2nM1KIPvrN0-gu2pAj0cvCFY_6EC47encNiVW0AkWNh_Laz5svtmKQz3-GlbFdYrqQ5M42qR5NYaHffWwIcwvDW-9nkUSX4TytqonYn52mkqot1mBLWZwj0eE26gfpGCPoDeUDw4kzorHJ1J0VMh8KJKEvJOQr24hqKX116UbwSCF9ee62s12jcJYac3PIRA263o94cte70nUqVLCmfQcgM-7IdlFDe8MjocL9Gz5T7NH4Y7EMGSX2Z5m691vL9JcGykv_XMYA9D6jXHgtUdVpTXz3lYP-N4nHVwlggUg87A36SZH29-GZQCMKUMKbBQTymRDr54fl6kXGOy387J-Hy_sPMCDgyh4LJOWrpFSKpvoyN1_3U0dFim7vygT9yrIlUOcvHOmmIqWa4T2pQWUFsqclIcD_rhdp8ua4GVxWXfy-7iJCn7d-iHV4CiiZdk53fgO6he2ZT3u2cWqK5BnUGAA0R1-_W2leFd9_qCgpVa6RQN3AJLaDgnPAJHn810hs3Q7t_MxC59j5LikeE0RsyNMvopDHw2ssRW1ccJ4VhtEH6AMVfa-efEyf64_47ktpqaY9U_JcczKAlj6S6KFQOr821HsEhiBIMBL8O7LHXBpLgz5SJdGQMrUbJAQW0Cy9DQN7CQLYY7aaSlLoPZXKTDGNsza8BEj7ej7__7P_7b__Hf_z__5___2RQW2oO-TfZ5R8guzWeIP3cXoePlRyJxmInEDRxhX3l6-_O-2qxsRWdqyawijipGDX6WZuv2azmmRQ4ujHCRY9Lxok3DVKZAMQE-q7YKd7ixrqQHiPc3cd9v8h90c-WR3nGbvmF9DmlKJMMIXMB5JCe8zKnGEj_kVEORX3KqsciPOdVY7Mecaiz2c041FPzXnGos_q851Vj-15xqtKxhcnzFQaKWNUcrMzRZ3DeDI4XsxvojPOLCeFK6Fi9mJ2V18ypnNC_RHMhJNVZynNFlsNY8rQu8sREuxuf1dfOCd8ZvPZmm5Ih887bRShGrWLyz_5m-toz-VFzPHHiccGRFCy6_PH1kN3FHKlahlXXQukqIEgFykHYGKocsdY-YiqgkPNnNZF9gNyPvdgAvOJUpJcB8Ts-13YF5lLpu67T6jtbe0O5TL32SlE-_Ji5-dVjp-VjfV308PhdUBblBNX5y8UnmfCdlfGTFx_kaJgeGvqB3ahbwz48K-vRJ92HpIz_eA_g3fFq-vFE_eL8y2OD3e6VxTOhxf1LFswVyynDsjBlPAZY-VBIRGzxxFKAmJ44Wz-TNhmxnu6E-9aAg4MLJBVztpZmSplRJ0mAhS0SUuosI9MAUeiZmB5acmVVMoMOTfhIt06WQTC6b05PPDpz8ghNecK4zp5wpUaSAg222JPusVZZQiZpwr7IeGDYUpfbZcqd5x1pILbkSQ1GnPYIJXYM6rUHwttNK6L92EN3nGkmkVBwCr7kngdGl9mkhSDzo3uOW8vGZ8H3TMH6NhZD6ViX3ZQHG9OA9_57z_xpi9S91R2gc5m6gpHKup7yoJp6riS-qcS_e6k01LZzrIRy1Iu_PFVGOXlF6UVF6U1F9UVEVK-ryt84MJvqAXbcKCqLSo3dcF1V65m7TRTWSgkznVMcQQesA43yaD5j0Bid4FPjlJNCduCnAShO0pK6UUduiATEmWmm-iJaO2kl2vnwdWUl6cn4lyStbelsHzfQ2oEjpbegDrrq_Gd2oVXGjjir0PL1RhXJSj_dC8iXXxZfMGxVLLKcFMa5bSa5goM_t43ij70mxMHj9Om-oY1yPhsau2yFTlWMaEHUcXDtyTIOGSgzNhIB6qjO1v8ASKun3zp4qoayuV0G-PUGwFBfVK8Wk6tUePe0fjkaGiXU9Or2dtlZnXnvFw81V_dr_rX-iz-XGJuevK3Nf0fDM2vqk7UK-0ZubuJAHSD4TDVUKF22EFhgiSFZgGEcgeF30ShF4tPyeOXRnu5MEXqKoQr7j2RDybbuvWBDyzbywmHLMSy-AE2Uh30g2iyzjO0NUAygrm55Ah0KzKVk3owo9wW5UodxiPKrQs-tGFcp9TfxFJKHfliwk3m-xRFBKbHmIvHe7YmmHjKSmnL_iuH6-q9m_f4j9OuBnBdbWy5Q0XviJd273snljv98ueSAWwvIDhJoapgJS03uNXDJ34Jh6eIhnQ3_iUrf1p8AiKuqOyvWpMFRJrkQ16XgzcXSftTtS-ki07365eaqKNI9bc-kDW5RPJjtCs8j1eVRjip8nDflFkSt9-qB-t9ypLToyofi4_gzPw_DkPSMTr8o3SfCAbpJ6LCleCQ33d8cJsMD74wRQ4pfjBFjkx-MEWOzH4wRY7B-OE0DRfz1OgMX_8TgBFv_5OAHYHDgr9n0u1Gzsc5Fx3ufi8Kat8LLPhe7MfS7YFNhNc5cKJHOXihxrlwoceZcKoLVLRYqxS42Xnt8J2hkpmu2Ao29WoaQz8l4IpgEgmzEBNYolllTMhuvYz8EUlDmWO-BHpTuG66R0kahq-4UYlVPkV2RiVteVOJ2svS9yLJ2LHEt3I8eyAZgiUHQ31mPvfSWWUIl_VYlfDQCcV0-AK3XH81_F8xzh1gdXZc8zmBPxcuEPDU0pZsULXnvFgx3vl5e7O_7bwRymAVCmxv9y2hNyrPVTU4syx4fH9kjtc9256EhRrIh43-XwfN72CbcaOxf4CuS7E3-vT8w-5KgXSQ5fDV6MWyEIh9qtJfxhBIDHyHzx0vHPrW6HnVPZe3fLu1yfhr9MI3DcY2SF66tTYqQsa3ge6-_OFHL1XZE7nBu-m21PvO9CASYWSTwVVaJbcqCIanTUfzzQUZAcr5b1MjDOrglgUtcEPMo8FQS6pPqZAZf8zKg53NEvixRVXTk9T3hUoWTQjiqOrl2k6FVorl1AdqXptqxs3l2hhOYMBmRXs446gwX16Oxjo1jvAVf37Dhqpz30GN0jL77kHZ3UkVao77WxRXuv3cOmX-FVn7SOO7xaIIWShU0h-PutFPJI0i3eiAqksV-dRUO_OotGfqNL5dOvu_w2EvLeNVSZ96_a3wMBmhUuUKOqtQLvFpE1r-p3dyjfbf8xdusG3kwcYoM4ia3QqMDZA5H4ra3QqMDRK7JCowJHr8gKjQoctSIzuC5w1Iqs4PpOUauxgus7RX-tF28lVtPnn8ds2_UJu3mpi2APfql-TUV_FMija2P5WrZnBYQLPiPXfEPbwsXdgJRtF-wvImS1XbDfLkDbd6jevP-MNia78XHcjE1wZ4j75LEToaOv7VY8ycc_cMzd0_j2OwcF7PhI4jU0Cgyn8o6FL4StvZDAuicWGGEw_9xT-3pOADugob3sd4qOhE2tBpETWD1eMkx9ILOWKk2c1WE4gfijLvBF9jONFSwY5YJ0_J_28C-9z4a8FbEwKv2w0yWVMyxzuIzw-YKtgYhIfYOWw2cbh8GJGTh_1tM9rV17mzgNCtgUS8PeaJgSYndb575FZgcd2AQmT9r2dfigYKYijMw6CD7nj3gdAH9NWsvWa72qtefB4VU__Av1LM0kDCFF2CsHt1-lwCZKYmMCpmgvW_COR42Qqk7IrwjZHZo4E-KpiSOhHAjlOhHCoZNHgtWHpvRhmPsgC6ANhuTTdwy08g2wuhVOgxuU74dXXUcPPwE5KhnGw8YoabGWtLrGOAlMNp5tvjJdL5GdwORQ1qGgQ5cKhaJDTOh_VT215PrBAg40DQC5VMgOUCgpEGAj5O8zb9N2kndDB1I7k5j8-7uL7r9cQ161i93w-KTuRGL2tOJT6sFBGynnuLDow1HhtBygUZTFwaPSksBkgVUAUZAv1VIkqUhdERi0MEIV6_wKSUXqhvBXHkpG7T1nrN3ZdREfNbN-gbG-lFF_PdYvMIgw6y9gCNJe3iKkVzUYGjOkE8G_IhgqNaQTob4j6GoBmjAI_hXBUKkw1EeCrtbhaxqEdFCP8BYnwrEPVg319Jp-1XT8_dup6p0QC5Wwt9qLcCUlfR4KGrjLCcriaOkLS-M-ndW6buLXqiWHwawY7whGBpNxKa2ufQ57E47NhrMJ30LKgqMGY27G-ki0WMLIHdEgyWIJI7dFgUSLJYykmx3yJ0is0Ovd8Hrnvf7Kfh8oOLaJ-vDq7mUK1BWAH4iMIbt-cKnVT2mNe7AglgGulAP1WTrgOX9Ra2j1TB2OqWRxsQM3v78cOJeQ7z3jtxM9_UavNh2MU_zcm9WKk3gHqgb4gyEMFoZBqLalHNoThUBvpWIqn1jtFQtavF61eGC1Fyy4lyN-l3IP3Pj8yY34p2_B9ud___8191_Df_2_nmoh5gKfsD6_0fTUu8dlcOg9kOb4Di5byX91w1aG6g5BxmDswcJ6XW-vnxCKHO-fEMocL6CQ2nlzA8XP5TD_bi_38g4KqcVfLqFQe_z2FgqhgnfXUJRYaPn7R4nifeLkUQMdeUKL8co0t4F90ysRyybSLAT6KO6P-iYXvJBxxHS3zUt0wq4RHZQxoQcWfKgXq47scJZq_jM9k2sdkfCl5RTjhqCRNsZktcxwYMQACw6RAToO8pfatm38Ey32xzJy3R-bxBGhGK-T3tJOPyq9pZ09r_Lz6erlQziuuJUGg19NKzLqiYF2qPArR2TY0mZq4xahj7yxn-2z5EzQ9y_QhEEorwjGHgpq0Hdh0EmD4F4RrD7EdwR9RwxNHAn6tr0TDNdB78ORYLl7YSgr_LQhc1TDKM4fAZc2nSAmMsszY0K2qVCiYT4-z2eFilxqIz9NgpNf4KVhdtEIL5mWklwZwFZwVxI5zbuQNOkMy3t9rO58AdZ2vtDyvvNl-oZuXndltHX7luOwpYqhB8fCE_SDC3QKoV2MFuOMoS15TWuNlNo_wloZbbPHH8Z4O9yjLm_N8Hs3an0PZ-7rYeWp38XZ38Up34XBu0cCvwx9we35_mLUKrl6wGU6W_rYDc2s4Hwug1pUZ9RUm9wakly5fLx3Nwvq_QYTA-_26LNiqH4BnhOaVxEChqiFCiaIxTE5A63NIOdXyHDjpu6CjqqfDLdnzMWqRXhULaNMVTZDh8QTwb2qoZ6UTDspOotQ3hFO2tgiuFc1dA_NqivB14ALcHNCgCwVAKd4J3hJi_CcAAgk60WA42XDYYHBUxH9iyjigdTOJPAnTLMPMtW5SmbhxT5uotsgxh3iGvzvsi3BJJz0uG_-kgQ-tEkQf646c83GvTvg4YCnA37onz_0Lxz6Fw79C4f-hUP_wqF_8dC_uPYPbnWN_ebcgoc87-tc07jO9fFnhE_OLjw-oetrFjMzLZHczPFs_qTEZkYknj0pMcYvPewHJbpkT3hien8mXc4M6yHhGWpiFI-a1l0hrU6ylkdlPPACQg-GVN-rzTHXCOEd4diEviXtBGMrlk5pMVCDoF8i7QIRQAkz-XZuGM4UGZLiKumyEUnQpT13EZH9B4gYInY7693OdkM8R5L1ToauE5T0CkUonyCvV8ghsGPHwqfPLjpCG8AlwV5XW2Dc8iTXvVbOB3qrPd1AIcVfi3XLXY4Jbo4dd6ETw17Elh5g9XzDtfYPNgQwhnCTxvpEnGFNn2FNkLMMkgKEAEmR1NT0z970ydL2iTnSk2BROZ55RJ5u8noWbQppTW4iRPpdnwmULm9-_PyG1AwSV2zTKyMtj4VCJxrr7ILlBRsTHQlNIfAXCKtbvdL6A3PR964GRzZ1y5j0fO2L51Gnksl-UBgB2hQbFdYU2WHuNW6jhp3gwxGETLHUSPNUdCZd0slQ1qF2gnSBK0NBhy4BYsoZ95ebWjc2nr0vZ0I6WA4W4ToR2qkP7bC_7p20COFEuN4R9P01NGEQ8onQXhGk_TVvw2S0wyYeunkyFS3CpRCGVxwNQMlRgEY5vKtHr7pIym9IvTtN5KB-L92pF2L5NJIqQFNeNA7NdbHqwSQXmbNkt6iNbZkng-kIc5pRKt_XH_npwAd_SQplteWigOcDHlQcVMjuKQINISFZRUBNq84j2JAYhHwiNIUQULu2R6M704d0YrVXLPAjYZfOeSl9fJhjqY-I6FhKWYeEbHdupMomJez67jl4_5gBMyxXTDAvCzUeeDWr2arVMy3QoNa12Lq8HcdeJcTRCE2BG6xssfrnyz1ZKA51j-4fDqADX0atYn-NcrvTCXHNKYa45rQCXHV6Ia45zRA_9E912iF-6J_qNET80D_VaYn4oX-q0xRw1ema76Qiw2mbHzFh4M52-mZXDng64If-uUP_rkP_rkP_rkP_rkP_Lrl_IN4y3lG1Cb6cVKTaiOQ86LWJiLcR0eGQkw7VEyS5KaBCEfInSHJuQIUiVE-Q5BKBCkXIHyDRkdIrlKF6gqRNYE465E-Q5E3MSYfqDqFHoNd4cKX0ug-ulDyOHmwxAvAz5JZ54Fo8pyzV85_pJ-AdFvwqvLMsNu7NbmzvEmISeh8gCZEPToAj4nxcN-cNe0fM-hP6ho3wtw7SCTvS_uI4yeNMmzJbZKjqkFe9JDnpUD1B-gKRIa87UDpmuCB6vRahviIYHobehEXw7wi6AwGaMAj1HUHf_EMTBsG_IijOAdrIQxHS6sbOHzor7_yR4y0XArgHcqrPhU2x1k_rd_SRsybUSWAzqavgXCc6DCzm4jY4NL9t7pGfHONrvoBBj7_Ri02HjTIYSvpOGuwlg1DtrXZOTyZIaOZW-8RqL1h4riTH6_kdhfgdYDct9O_k-243_rtv_8__8j_-5_-ymRm6baIbNCoi7rmzfqgEtxMCVPCKwR3Z0_wRKTYimaW9NhFxNiIapSXqUDlBklEKFYqQO0GSUQoVilA5QZJRChWKkDuUEo3SUk6QpHNLPEGS5VnKDmHuRxkZnBg6wzSOBVmzlgumq_5nZnaMZ0uyEyvhhBJbehQt8RzVTF8DcSs3EKt029uLzWqvn53lJZI3SyShRLJKfD_GVqLfqauWENrwSxv80z1nZrGqiwMbeVySLxehd-ijcQ2SqJj7kTJyukkGLbwXy7xWI79rDavlzbsh7DTUdrZuGjHb3soTLhX2VNjEkuh7aXULvWNF-buNSDHTDaLAKTo0EveVUhaUtuYXux9fBnOaLzYOzgDB0iqnhNxySuktp7TicsoaBoIeWe2jcSYYTZySo4uWdzyM9cISj6ntDSOQ2cEZj4Pvn-tbQ4yflooYkdM41My26kEDW-YsprXa2Gb2DqYjTCPChnxff-SnAx-sYhRIqtkMq3AjRFxBud9Ld2fdlMvh8D33rj7F3Qo_AjmDOClNKs2tEd2E0e0eFZFtVyHpmfVBPEZde2yKHMA9_fKKXML86ZW9yPm3V4Qyr3585ddyeBJZKPf651eEsj_9_orU519-gGUvX9_9Agseo67j7rIU73lCbpetFX4mj9gLoKPryCvmp93qyMU0TlfVkWzKTrtVzE6Vzz2xJqfmvsx219q3E12YBVnxpsegHT1Dhkva0bMacDsjHz2rI-XUGpxABkeFgwnvY0jRccf4PsT0Bbbne8eF8aOjI-SySsO82pq0r9v3QlO4jqxbfplRf47fQMuhB1ZbLzNita6W_Fq1lE8_K1YuM6qX-YEu--Nf9se_5M-LdnP1Pd1ldo9nCWs4q147Ezm7bnRb6FNPui08VgDLuchX8kGZLGXdjndZOTtWhNDB2jBt9TK2WIMU3pDyi81aHfdXjs_QLgnZXoH1U9ik8T5KhKwQRt9GCv9yKTEIilLYxRYbnfeEjTafa9LmCbovQlmH9JtCq546W_XU2aqnzlY9dbZKma6sLRHSs2qrnotbMeN-WzrO80xj_pQVL9sT5u4MDHqsCOkMqww3ZsOs6Ki6OpRJYtUiPKqWUTQ0e_qu3zZ4gxBOhHwitHcEfR9bT1nK9ZSlXE9ZypXlWOtNGIRTrnU9JULXUyJ0PSVC11MidD0lQtdTInQ9JULXUyJ0PSVC11MKcj2mINeR5mxFEKuVO4yc8ILT3ygLHD4qebpJ-GgYR_OqdpE2r9oinE4g1tMJxHq6L7xq94XPL8LuZmejB5ue81dQOIlatIYjBNSmQcgnQjsRQM_vqcig6CUkq0jTawt_0A0uZFF3mP6shwRnDYZtptYhEYHd52HoBEJAi_AJpPYfl1ODtydWe8HC4G2N7nP_ct595cdFfnsu9eDtf3vcAJhTjZPLyKmeXPjU0uWBnhoheFJ72yeotpGIqKdTq56wXfU073rpkJ7mXfc0bzyW3tgv8D3H0jM_ln7dH4IcScctdQuZ-eiF7fSgVDFss9VUpSOeazvcqdGkvCiOFboTYePS9BS8pl-E2_SLcJt-EW6TLsKFHch4if1ZWbZRaCl3hYYRVL5RljB0QDS88nh_5vRD_Y2k16G_YdS0OiJodZITYlQmOSBa1ce96uNe9XGvwrhTaPn8OHcwcXN71q8rkeeTcCkwh8TOSxcQ4yxg-aMbwPtCp0cvZ_ogZA6t5OrzKAZhjkkYrtgL8jWdvewXGkvdbnrmYtMzF5ueudikHERWoQjp-Y5NT-BuegJ3ExK4x3inFx9N5NBK6otKRM42fZ4dt9pPFa0SyifdRLdXk6G6QewrSFlJTU3Kb2oif1OT_5t6YKAJhwzA8u6QsbsGgr717c1ahFP6bDulz7ZT8ms7Jb-2U_JrJxj7qHb67YN2-uWCdvp5hXb6gQaYSUeCvtFqpx-JaKefV2inn_xop5_8gE7uhLFdgzaqtaWDZmyONzh-2A39F7Zj-JQrG9m7NpOmFZzrxOQCi7mkGBya3xIBBr8wvpYHgGp835u1x_EHxpKw3wM8HvCi4l7bFYK4lZCqIv6wkwT1ZBCqttWEDRo2cT4g2_Rs2aYm8zY9-7bpObtNTfRte3LwON0KvXgut7mPlOb_zOOqG7Yp63GCFppg1cw2qlnPeoJWqmt0aa3qbsexV8kZGoGVNuAqwn2M0qXmB6dLzQ9Ol5ofDBWCcU4rkrZyUJF4mgprsqwlhUMriS8qETm0kvKiEpGzvY5imeFgLyjsbnC8b-UU_5BzVOmqmcTOB-gt8JKr5EMqmXpbJ9evLphtUEZEtER3QASDDmoTES0JH2qTzmBBdTLkTpDePxmSLqdnFerDJ0NFh5y2scYR1Ff1AnUlDDXqRh7UaxEOl7PjfFCNPJxKBsG9I6hGHjZhEA530CNBtQKxCYPgDmYitmGZgNiMzXEGJzkqDnaDA6WBhBSKoIBZqtQNERTIBqFolgpI0_TEboPpFD-y2gsWOMW_rPacaLqvwnOXG05xB9mw3-L_6__4b__3__Y__Zf_QiyqMbrZ-L2c8bZHswuqk8wkGFMDEhEtpRQ7rpaRGxpnUTSxJEJFh-b5EaVCESo6NA6WaBWKkJqvDxVKXi-oUIbUVH6sUPB6YYUiVHYIHMjJzRyr7_SquFzBJbzBwzGMxKWeC4h7FddWxVa6aIXLuWzUykapLHjEsW95vD17s0vmgCkVCSqat069yzQ59QbU5ITbJNFOhMZMU1Tm0ErCi0pEDq0kv6hE5NBKzvl4CmerpMmGtcsCyPvXBFN3qXP9pJLR5bQrcqEyHZFsQqddxQs9EBG83FGrToaCDl0nSO-gDGUdajp0qZYutKV_DxlqOnSp9jEMvAhlHWq6VQ2yTbcmgaAbizAdDUI-Edo7gtHJQxoM9uFI0A13d0jFAYKxuwCC6giHPliE9o5g9CGfCFrO0jDZgWGa9TCaNie84FwGx4PqTeXz3Ew4T_KvB9JUDvEcm_WAz1jhcG-x3tjqJx7MSoj6-bJBTw_dv-b7Ez9RI8bY9IBGNgiHXCLUpKcm3LVv6VxWkbYg6LTCytYTpKlfCB7GxeA0RV0q5fVSuOFz5cnruj2pxobvxGovWBHFeroPG7rWPqU4nKlw9i_dv2TJQHLy7wvWvSTXPtJeB7SPCGUdajp0qdsgaEuEsg41HbrUHZJTk_KxLRFSk_KhLXHz5NSkfGhLhtSkfGxL2nI5NSkf2xIhISm_H7pMfsQXX_50LZZo6fUv10KROyz-9odrsUhKP_1uLRarL4vBqUUo1lz68VdrsWBM__SjtVi8pH_7zVoo_6ipn36ztv8wbfJ4dd89HxwdeNfv7312rpWi1UR7MNCVuaVSapA5MF29cIce7rz8Oc9J4RB14DEn5664lu1Ziok_u1Vb7Aiw4Sg-fdT6yW1Hs_rSfdD8lszex0-ZmVOzVRGeHVjh_iMgtM0Hd0tx7CUUD-njaWqgo5-rTk8G_4lJibD7Ovy8J5HUslwVorC2zjy7QqszjECSCUGf9x_bDXCec55UcwY6plbC3fk235Sd_gJSJK0Im9vSHtFr6UswDXVE2t16LX0JeiAi6mWYCLElCWl930m4_x4MncPP72T5q2jQ1_YiGYW8TulnXqAzogPBs2sbhfeWS_m91DIv42leRnFeLrWUUy1Fnt3s5cSJ4HeItx2vQ9uMoLQt-iK8FyBU4_6wve1tnwn69tYfMsWQYDRxyFbDRas7Cvwh1wyX8JGgezP8IdcMO6mf-kFGNs9hDTnEf2I5M0lkxfb8IWUICNKuD80ZAakrgnEsrE0831HpK3U7x_ntURgyE0_iSCc7FbgNU0xCsZNjEsMJk0B7kWpafmQSl2Q4rdnwYs1K2zQvpKAvbedT2_lF29K2zQuZ8Uvb7dR2e9G2tI3zQsI-b_vZkVltM4LStrit80KG-9L2bh4tbacXbYv6W0qGBxM14DWKxC6lz1Bbj2ebNRx6KMtTczh4MIdjYm11i9Q10RxW4NmDFebm8N3mg1fRHA4XmMMXM4fRZgt-_1G6BMMffNkQ7NNzKU0XdxJSpnXjYd8VaH5cJl85jLsp1-s60NoO47LJ_UYQBIN0IwhMhKBmNAU12ypA9l1QoUSh8Zb0hsu8AUV5ToZlqSlpQFWAOJ6PKZAjzkq4aoUifs4d4T6SL6V8-PdZJkl-vHffncO8lubx8TW6-fwaab2htjU0loFOGZOLU-BymZB4Q3M28NrZ81mlg5ksN-hgNivdYTOPi4fa-wrrjpcflC5BoE-4DwhwL2DfW8xZTZtuAfYXBnzvMdgnXerv-wzrozrxo475VpQpXbQpXfgE5c-lRVO0NVCWNTCma4Ppeilv1p43u-TpurLk91_b2mYrraTILa11-G2O0Eq8f9Nf79_1l93QtdeSXrWV5LaYiIQfxFqkJyaRkkd68ihWJEF6GmjQk0eDnnIa9DTQoCePBj3lNEhpoLRCGRJSTr-mPlMXzn9q3zazo4dPEO_-s6fV5OzPhUaOTruoQr2dbAZ5dqg-kiiWj_NAt3oisvcudNpXOyk0apwZe9hwSmMNWhrr2IQFhz83d9ug9ADK3RsnUPN7atCp3GwisrF5HKPR2vUt9thNPbhmkYbz2DnCZtNV2joFPakw6KmIQU8PDHpSYdBTEYOeHhj0pMKgpyIGPT0w6EmFQU9FDEJ6IK9QhIRURNTl8fn1pyYbKB28QxM6mq2i1Sp5BywMNJhlo1k287JDnES4eecRPvcxm_JWyJ1LbmKmF6nXiyLo0o8NNSge5cbdbMRcw7sB0LgRsvZY2MZdlBGOjOsdA9eLUL0MZR1qOxRJU5LyjGouXVTz76Kasxf1TNCoZ4JGIRM0N_rh7n9cw2wRWHCTiUJznlbmyFVCHNj1yWig996dGshD6i8NZNwKaw20pYG-vvy1Dt5S77iVVKsXLmXk9Q70WlD-WrhXn6tnuxh0uT9jLqc_9AJ6kqREZ0O3ADysYSxAUpqSpyvl8QMTX2eiEkRFs4m2c1mfW4eJMNvbP7Iug-X77T5AKrM7noLBArMFNguEtKutY3vbJ1I2SMMJHuEXsVYf-G3XuE_oZh7VKs-feP9PGff_3FMVcwaux4UemWxmTn3iVYqY0rs90iWauE2IeqJs1LOYop50FMPeMz2TOOqJv1HP051DqkFiz_LeMz2ROep5x1FPE456DlcUUq64wFl_8WKNaQ7WXjK-KhmFkuVVybKXjNebkvHJ7whdBCc4EN83ceARGg-XIFDKJgyZTjIcU19FCe7Q8WH4rys1NlIPnbXdvonek_r99xtChhUrKVowvYgMVc24SWqSQFITC9KejIA6sRdqsiJO814drojTyJWQ9TAUTEu1qB1gTBQ9BMOiofWM-mZrJ_xWB5Y3WKgFYCRkZdPb0cBqgZBoZWogeI0DqRokB9HO--ftn-OMLnyCH71JF84sgEOHgxdh1zo8L6BDJoZihXYCrajxduYtHmuLbW1xXA8CzNjVb_8p-FKGVoTU4HR9C94tXe6Tr7Atrytt60qS4knPvUh6SkhKOiRki3BIX_kypKfdJD1ZJ0lX-bFu8Gs2uoDOBTYcZCuaMaBGBH6GeNb2iKf5gGjOGPzzT4yCEEDy5gge4_bEKBb5mwuimz3PJG4Gh6IElR2KpJA0uFm97SDvNySg2IQB_C7UhGkgIDpxbzKGkwcZAxtXUdtykmIGsM6z1xpSEcZJkb8wVCZ6kru9hSPLvZDOGeKWspCF4dZQ90K6Ql8PpGKJYExiyUVKUwnjk1gpLgrq0z6gkRg3Ko4zw5FoSdYvmch6nCDr0YWshyuyHq7Iemgk6wGVrEdNsh6hyfpVIlmPBmUhGjRchyU9eQ4QW7hdAfUYj7DKbO5CIENUQiODZCvXNGsXi7cEDglyseQzpZ0plyp4ex9kKOtQ08R1GWfL129W1GO-RT3mW9RjvkV1M5bdzYg2dRlbO8wB3MDb0SQkCKL6AFJk6kN2IfZ-hGvXNJxxfzK1stE58ITIPR9OQqvnQVB8SmOd2uTGAlgg58aWYWL7njLuQJDaGLcbWG1cwujBJghs2JIeUze36RWs03T2i0ouqk-77P7poSRhbSnqGJaXhmYTbWf0pMJRfhxY4RXremEO4KeTFT5MHg3NJtpM9HphJcB7Hkj5DamZ9gaff-0w_4YnE35StkTlWhrBo9mbiHiELfVtGXg08-bQrOwzjD0bPh-3_HNHpw_8A6V5Bvja4aDBNFhC7o_snEqryEsVLtrwhZp5-F1xNB7nVCzPIJSvTLtPiLXnuN7dKp07OIabQJAMq6I7RIvukSzSxQUUEq2nons_i-6yLbqfeY6SVqGuV2VIv2hiqlytQhHSL9AowgUa4NKseMyKWoMVIuN-BHfb20jyi6KbbdjLQNKMXWR4XUe_Za-rBoPXVYbROVDJyT70BNT9SBWHRCdBbYqTAI3CShy4iiEKfTUp9UzxqmHaG5AM06qePavq2bO6u3vR-ujVLdadHOwcn4D-Vt_qMxjfxHIsjK9jOBbq7rweGhwGTrEoYOw09JXbt75y-9Y3bt_qLX1fk4m-8e3WN77davp2UdHDqneo2TYLE7R1Hb7Hoa3bjD9u6hp9xzXDDRwl9KvwuO-4VoBbh1Wf7iAmJK4-XfR2CC1SL3KNvMW1nbi2s3qr0SaAj9yGTTBW_EaCJIgDKb0hVYVUaZ-mnfKXstAUWTqFMF-Fkqavuq-76r7uqvu6q-7rnvNNgySdXXVfd9V93VX3dVfd110lXzfq7tZ9i2nkct3Rrne6-0XRTXdDmXi9KDP72D2PwaMz6M7sPHibrDJ7rzo5NouM6r45XAVrWlqjt0iDtoSqs6Fr-5jYlJ59K6njXlqGyg5FUp80xZoaC2i7O52VEdPEWtkhVNpzII8ZSmNk7QylNs4n4WbwO72EzeDCv37jl9f0eaQC-W7hOxZRhs-smAfwpTW0nNFTvhPORNNoAJKdVoSvbpN6pEe2L2CsFbAIIFoAbXjk0QK4hQxaALcDjJsAo1zZyjljn0-XXxlBYAzNNl8__a6l_HEpbnP5lPwyWHvJQ_LLYO0lD8kvg7WVPCW_DBYR2_kC3-vUD_6lanlTdBXiUOY-AxZ4Gb-HDPJFMn1lmZuvcKbkM6WdKWpOMfRBhtScYmhTkPvQlCDDoSUR0XKKoR0RUXOKcVhFSMgpZk2J6caAtbPXHZjUsb6DwfKIYz-c6nXHqXvJbWAYxmpi5EUrQQ0YwqZ75aGOcKm7pYXp8dcz-7IRdRQvEH8t4M4FkqfrQdZkuCQ01MocxoVhogcdOdb8gfUmv3hIIUvfIslUpdjegWTl-g5Se0Mycp6hLxpo5DxD2wuIrnto9E0SMs5_qsr9UOV7DnKlrU-PPdMDW2oybHjnmpY88kQcWLDiz4e2NX8-_Rb2_h4nkbm_HzPN2rpjj60QA3aJbO8JyUWRtMHyyAzrwo0fHkITIby1Ll4U3awLKOPT72XyNSySoFskzh8NhV6fTalniprKCw3IkJbKC_VJVoDTUnmhNhGZ7iO5OhkSrg9DbQlYeWEfzBYE3e1GEMBQ3kBy5-QFfNOi63k38o_PPR-uMqnnwwMo3st00eHFXwyxNLojqc0e5rVhYjji9H5Nd_7IByXqrDRs1lcBtdKwx2KylTiuygPLv1D1QLJ1L85Dm1TfkIysb2hGA6Wsb1S-bjhLh_IN1gmgynqsBMYHbmowINlqrpNUhUm7c1BhS00IN6zDVbhcJH4yzPFMRv59EL3SUuSWr_5De9nf59HvYzh3Gz2NOftxVtyFP0xz-u71Kr_ca2AX2jQfsJN9sQF4VIE9PKpEOIF_ARlMvkPQNPskHHqhKgI0mHdH1eiPzlqoRVSRXvXEQsWS9vSaJxaqExHNE4vvKKlIr3tiyQeg9zpxVw84hMinkF2xIO-85crEodJQy5U5PpItb_Frm_IWSLaU9IaXEl5EAyUvJQe1g5eoDGEhTVmla2X4MF6nQqY0mcNXv_teek4-75M__ez4B0m2_3uSSA54fTuVOnCHG4kWxbey50XRTQJBGRJoel3mvtSYl5GEVtgzalFahd1jhsIqlC6smpzgAV0wJVA4-yXD2S8Zzn7JoPslg-6X7BWHefII6pHEVVCdjkF3IAbdgRh0B2IQHIhoH4dxVH4xnAGAqwEEG74TnuGzTH3oV1RoKIbHlNKTWObcsgJhOJNpYCtaNncYKeU2PdEFqQnoYHroYH6Y6Em44_w-sF754MIbH1yw_F3B8neFN9678MZ7F95478Ib7x3IZ8XNhpPVv86QhZdkFn-0Im4Jp13N_T7N1D4hz9sk1myXSXRIXLNqII_n23tghs7EvUlrGhxoO5F3aO1GXLuxJt2sKzAclhRuFcLVLw4tn5yetKQuOCLeNDuVa4RUOqpbY983XjPZIZ0SJMwym2YEsmsWGZMtRxfFXEsNvS4dRZ0bx_ZO2yhE4Yr5BRM3EREyL--tmrWJiGcvXTx76XotQVKfvbQM1R2KtFeS3ouC0wzVWxxJqlztxeGdk7VehMs0hzpjXqveYlZ03WgjkfXhdmC_R47XT3Qub90vqlZASc-VztGUObcDeescqvA5Q887qTkfjYRUMqNNjR95rlxfoobGjzyXT-cnKi40tR2TidZz2ZOaxoVnqml8KVP7xTf-s17ToiJ5DRpYBRAVZdySU-8PoMalmMzZA1CRyrWDq2ohra6qGL6a5yZ8NVx6bhKIfcan7oWI4T_T3ZQed1PyiT57fjwsO3IGc0DXQ68EctD1VGCh3c_SxZ_BXC9U0wE4zwuk_HYX-aLopvcSnJl_UWRov9l_Uf1p8LW-PTsDDSogwXFxQ7ekeKaUM8Wpaqg3IENlg1Dy9gozF8tDGiVy4N7DQBvCKxEH0Wt6ONJB1MC7K-IKXl9Dyxk9pO6PL3hgvbg5AElm_j22dyBZp_95c4pgBEmigMXakkAPyw9bkrQn8-VzEAJeYOwLMGSQhi-WCk-OahkAafyswQLTJu14euJ5qjLHLRxJCywVDRjjEmncPfDD4T4o9VypAr8y0td87r6KSn6ZCYITHUm1a5Dufcz9pwejo8-y8KzNZ0MvZNjrz91MOe6ArDKbJgByaRYZ9yl5Jn1su5Cs-w2z7jfMxT4ZBmWNq71g3LarvXJebk8Q5Ho-3qIAtYhaoZeWIe2SA0BiWp-wgUOre4634TkbA29Z2_mVey3Tqz09zAYrpQ1GR1ELMEAaap1RH1_VVhY4Ow6sN2fUx3wxFU9-cVZ8DKNNenFWHEfhDUk5zwY9VtRT76mmnnrR9CYxLY_TxEMtzdsxtxxz9Dvdq79fOBY_vq03lw34jlzcsOCWCjhbgNk6c_GOCTB1UmXv-pmzeH1irFPQF0j0IYK-EiChw4ZI8eJ3yQ5s_qzOZ9mRR_kx4lCtoB4osMGfoaL6dn_wouimFaCMu16UQTlcqnHRF9QoHbyF4ZGhqsnRop3Jhep0RArLlP1YMxr4ZTiFjslKZaRNCMlKZWxUjTSrTjIuP4F3kC8_QdC8_ARJVc_PwhmzM9jgUyVW9sS3IViLdXQYv76G1jN6EuzlzcFi6OVB_JcXh4OxvQPJOhzMm9MkO35tBa0SOlQ9Vk1U_Xq_SF9uFxVATMDPy0L248cYei_ubvw-6-7H5WUmPK0wRzRBcVxRqAEVJE6VsQZUUOiXkj_9B1bDJ7pGRHN1u7iuUXhWNhFe3SbCEYkqUnax33_daUCBPovk2UVoPjRiqte5I9rs8brfccgh0VSv6WCqV_fqLkdow7S669mT04da1Bo4bLvWqNpvtQEi6Ya6ZyWhoOyQeZMDiuY6bqQT5HYduUeG3AbSi_vD4GWM-8OAsSgBpefsOOva8_HTMkLPcRsz56KxjRmT0trGjOlp5mjB1FCURjWzsOorVxZO4QPrjSurvnFl4QSxSW9cWdXK3YJZsekM_NjPPZZfOSPdgamgaILLKLhvcAy0HNne6eFqEv1ElZ-RRhiEbOsHZ54fnsdHbT5Ckd6uXcw34r8BgdsgE-QxswtpIHWJedFH7trjAw2vtyfduTQFAa1JSFZVB1ZHVQdWxJ5lqk4qHRp6kKS93W28KLrtNqAMOYOil1n6SK7dvH3wB8-YVUbpFbmqUyKjum3Dc7JF8EF9tgiR-GxH4tvZbYXfVoL0BDb80hKkHqxtao5bU3PcmnqwtgkHa1nvpLtO8UNII4IaZg6_euXCsHxxtpGwSZOdXKxA-5Gfz3xQE83MOkNZo6DmuVDaEw09KbH2ysXW3jjP2hvnWXvjF2tv_GLN-KUZaEYDrcy3JuXMLR8_-IaqSo3eseH3IGDO_Hzio_euvT9W2sYNgmNz14yg0lKMRFVuEalF_XHP154flu2TMmrP_6xZ2oEOPokqSQdhQBrYp2Ua_bUq7bRM42dQNzgzWLRLGk-YW9uhYls7uoNGw3pC9S9lYShsGaENzuIb4a64patv8kL7fEeqz79MTagfz_aAMrgSxtC66VIuvxpk5ar7ozQfdasKHsWvNi3T-9qNsIEVioGKhuqlXS9Ajz8L1HW5mpnQDv2XtCn0QtCmUEjQjNhzvSXLIIDSNkW_GxH2hEiRf54N0NPP2mElfk0xw_JtSzEbhJnRt5QcB1TlnTEStB_EGzS_vJ-YrT7nib5ZHRPmD_2peVSGODdktYvTQ0PfuDTHBz-wXrg0kWSq0vFtbNILlybUJKtW6ItyTwJ24k22BAh5nHTKLQUIK7cUILyK0gEnS_qz0VVUUaSycKiiRYMATNr5u6sZvryWzoDsXzuzwfJogCh2bhPF_VH0JDyGSNwQx6GQ_4Op3_DIe5puNh-K6WYqfF0GHHJfvq4gugqBwSBHNzkDBapTM8mgsAypmWQAyb_1BVXuv_UFQFFlIxRcfutroI63Kct0R1N53A6oP1UKBOWXROGN415td_cc3ilovUbx56x8N6xEQ6V8NxRTzkgHw_mtgFI6GCw_98zUmaP1XGw5x7CmET0Zk5QjdBpjWXUaD4a6FGJhS4G0P5Ci1c6_JD-2wGfucmwh02Uivhlbwo8Hjr8Kuw40ojABry4PMXWK403eMQyuunLuHfbdsr6L9pPn3bU5nz9Wb9meYZc9ZvPQZ3g518Ybp0E2Nr2icyBjAyEjhTVwTTKfUf4ahjAutv4oLCsYXMJfNMCYCC5hBW1j3MSy8DWXTgpTl77z8lEHhQylRiFjqlF2JxjvQxEhIVmON-rY74viqvLXaUX6oL7VoGT1rQalkTVmUdYgIm2hiNDuGsRFjxB9tFc0IKF9jokHgMjbaz3wXsSuHUOR5PPcI-KjPXNvQBcZf_4RkRH0L8SaVL6QJ12I-jxy-nu6tHWnP8K68_7oIj0CXeDp64h9BSHcKZk-6d5LidtU5NJFOWvIpmSVMmxI35V096_1mElbAJaHcH2eI46oUGBEbzcCKnpHv6uINBW5NPXE2zMZWWO4RxTdlNTz5IZJNoCenZx2oNzC-97urMD1SPX7ovqeDbYzxtknlRFD77HOyO5RT8mo4_kpjvG8UvHRf500jOFGNBeQzxkvGH-yTBzotXI9BrFDpTWA-ABhAKhYAx6wo88SmtWgVQcwNmIi4PFE21b_XH5bMx2CQqCF1kKCohr9ljDWf4nA3kMlWOYp7bysDEN9x3DX1o1BSEo_B8Hv6hplqYklA5uuCQWTTePg93IDS9qbIkH6qRAixYMfxuJ4lA5FpB8Weay8vpTCsyRyF6JdTD7HIyB1l-GOSOjeFUkD9B6JSN0Qx0ZNEKwwZhJSV2TIlFCeHy7exWR4xFHepVB4JNntPlwBcP7swCM_d7ka4iOiL6HxMH9KhAPubryERQaGWoYdCUbPI-Vg0ofWA9bx-TaRiLnwnHx0bZN_ITya0e9AdVBiTBOKJgfFKAoxibvSnvx3uU_K4ObJ5OP9eK4HSj0_bJ-ZlI6Orh8K0OQk-rxsm6s4MqJWGR3HnUfrtisWgoD0BgHIuyRISNY1CS_qDmVtVq3d2oXR95XlbyQ_LKEw4rEO6qaQNytxz9Pj9e8yivdwH0OUxwMXoD1hckB7hzgkb0LmYGgdWa6BANkepZ-Opjj11sgbjqi6i3gjorbNldbRll8DR7SIKOgSmOyqronELSTvhyJxVuFhEa4jehuS9uhlRaSoiDtuNXBALIrunhq7kZiV3cgErN0IDK2g5mCJS0hREXfaa-CHMBiqw23orVhP5n-Mp01GHHcqrQyYkY9u7MZfXPcBsfWfqroTeP6iioTRDVNVxtQgNXpTlbEuO4IB9G1JkDVidA52GJJGjFd4bjUI6XMlX4gmSXhBFn0WFK2W6MlB-rxtWi1RpxTfXyTqjRLqlxReb0FUeOMNBF3D30QiSGchKb57bDhkKsPlfdQOioEl9nqmIkvUd6RQqPNIVGkpaz0ZjHbuie5dStKB0q2H0raBvqSoUi1od7cOaM86HND-GhySle0cQq0jsodxfr9df6bdlTygoIwo4oujmkJZnQ6gd6FP5WKZMQiGFZRqyENz06fN0udpd0EOKNiqPhHvtKzqE_GOyqq-Ny-p7d66iKguyaS6JNPZJZnOLsmkuyR5F2w7Ifl-pGo1E8ZzyUqYpatiZUzAsjLggwsWQ1K9oUn1hibVG5qO3tB09IYm1RvK2zctFLh4IrCzMQPMt2nxN8mgf_ymTgav56e5mww-Rsnf3WOQwlPllURDKIErrchoc2BIiWg_vfEVNyJ6aT5YEBKPU7mVaWHtJlRyYELVaUIBUlTjKjnFD5uyZVylYBlXyZXPYxLG-OmqG4yWPNJnNhsoo1PuP_e796WU0_M7aeX2L0N6A3_-rLvtERoKWfDzccxU3XnkFB0oojI9QrJ2y3t-4xihxPtD3yadu0rdwIvgw34JFwLyxh0sDKUDHFY7URQW7wewhK7UrRLakx2VOoIuV7sjlCV8K8w4BeOff0NR8SKj2qobZq6k9WGk96Kg4LLq9M2q0zfvTl9Wm60uc5TV5XhuqbusepWz6lXOm1eZ12aqmwwJV5sIzo8HNe4-4RzCH0xkIiIZVssa_kIJmS9l15rDInEx1SOnR2aWuuTjjVUhwZgJIqN8ybDYFl8JwVlyYVAEOVpVscKXidgC2sDQQuC2NZvx_bct_y6QV6vOOdLJ8syW-8AHERnuakXh4I9YOrZc48T7x8bxLW4TJ2SMBFQQSQU9d6ZIYqxdNpa4VUI7sqM_dwROruBLwamE5-TKnbvVbyL4fnj3HF5xH_fk-H7_lu_rDsj66RWUwJxGo6czN825Qvj9W8P3QyQyhI4aMorG8I2-TYa3KV84_vm-TL3DHvAi7k46fVYV_RT3z6hAmXR9izixSLtYmTbb-dqw9duwVKg22jm8RO2OZ30l0t3S1yJ8ujpLfdlS0TTbq097bmvvKfmt-fkMS_E6i39ruk8nfa3RJhcPZOXDsEdj5cNXDebGGT6wQgJZUMa1frv3CQKp5b7Q7ZGO9XPHtcapA9B2G-7oHUgS_IRbQYpopSPpXmKbfD7Mj2bCD-zvDzyn33OkKPURTnvJOEs-89bNDzxK4gfquptKplxm8dRnVvoWv__sn9g_ntbEVApZoXDMG2rz3dWL1j2VPBNioosueZHSIJGyPCFQX-d18Br6l8IwPxoEY0v6pKUCZOD54vpJSyWwsyn-etzG99WAKd6ft8RIxdllDSV-i0c9MWE7VQ9RXSCuGTZ01gbyhTHndstyQ4uOQ92w6zimO7wfXUFZXfXczqrndlYjt7MauZ2VJN7hoz1ZckB6imU1MjN7I6LgQkU8KUL_L93iYX0WpdVoYVC0gZUlK3v1_vPcWguDon0fsYN8BGOwWhgU7SvbHZwfm4-K6IkWPjbvh5zyU0neJj4SrttESM9YhRfKhi0MFUtDxrsjDsugXMa38bQv0g4SVp4IXXrbSAn6G7AG5HHg3ZNCRaAvO4PrS4SyDgUdugQIjk_dFzP5vo_5tOcSqIFc6fN4lvxXJ7hAFKAk9jJd3EWEJLGXmUTiuW6VNPYYy3-Jtcy6MuXN1hVRUPAuyYYT65m8VCutJAYa5iXKubruHJVVN0UF1V095lFpqrt6_KW_XXVjG7FnSCk9IKjUAwhPe6f2AH9r8NJ7UHoGl9IDgko9qOjdVXtQ4XaxsveArg6nLikdkQ5cCyKGIWJtevS4GXmybZ4T3bF5XHrTUE3ILB1Y2tsbWN3b43WK0g21zaRor6nIT9ozUTiOFgZFGyxF_NL3S5L3e7QwKNqQ2x00cnqlkefNLiMPaq8ZebtNyBMeWBW-NCsnjgRvVnxbVGetSvPM055dIrYHCTgkdgy3sy2WrrWK-9T7Qy3b2Q3n21kBpttZrXRcxm1b47Pnu6JtrV9wGO4LJmomKkZc_pkt1SZiwvIHndSIh1bQmsZK5A2LS4m3L68F2o0YNNXYrh5Lk9UCRYUNabsCqJ3yyS6TryBMeg4F9dtJkluYqmwq6Aif-rwhHZL0StNuHxaXP39bsaX9PhOGGC3pDVHkPp_p7215RPsJAplwIJYD3adZCeArOZ1bLyF1FTyc9SqTD1Ie-ZJmxPpZXaOdQqa0lIIFpZOeGgVtJykiCaKeUIQGHGlA1GHQS6kPowHB5cYbFzOTxLEBZQDlRP2E5aQ8HsBWi8LTbyeXc_sy9o0Uo17D-2514jL8TtXFZ41DMh262YM_F52F3446v7itsanpZsx5Kbb4rLHE9Cy6dnvHi-yX3AoSP1r3WN9tubPHGotPp_w7j_UY0sCH1KXHETs6Lb9mmXGDt_7XMdWeMy3fQsLhbwVt2LZcNgdVWHBMsS1g3gVQ1Ffq6WoDgH1OB6SSuRwI9VRDU2pIhawl_Er_GXECXEiZZ9h9e3Q9v80p3kUSacmU1kd8xXK2YPyIMoBBcoWCVOGQoMcREpQetiVCRa-wnCo0uiEoWIAEBYuF9Jb07qmIoK5Rwao90MuIvd5PAvG-iYMQdwjVtsPT4ET3ue6ycezZuDlu15VuXD2oYbIBy6qWzWDWI6CIBkF_D2hHNAhcmG86nmV4U4nf9jfmmLIjo1Vbm2B8K6vPl2YIoalA32o8E95qYMJbcYzv6XmdUdoyk_cQsGuvE80VN7J_BawJX4aVk0cfKUEbuMEY3lgl3xqHNsvHoKCHyVkxYSAtr4iYcMPFwILaO1ZaVKGw7KX6EWoHSNpXkyZ3SHV7k4GeUL-PDHtY6KMGjwR2b-PK-xuz-kTGtxcXm3SBPYTPFKBEYSDMpcJrLXutMid4ELWx53fdtoUXb797arxAoNUwRB5md6GDJTmNElkVXY4_GbR4A8JaxU7p2gG-3eMSDl1CxeGhgb1_vX9c6PbQhNY--QlGowXVw7yCBTVWzyUuLjad5PkpnMzgkFGhPqtlSDhZwiDRYHJZh4IOXSdIMn2gQhHKOtROkGSBQYUiFHToOkF6fXon9J6riGRoOS22gIte7bWOSMaZ2-NxrAciskcq-JtKpp4Tont8TEVIP69X-wUvQT2PVz3J3zQIELuTGelk3nka8xBNJeinepoNGhH6OQjib55vBKmbvI1zNy2Lbr6IgI13EDDhwhGO8SwIXqdsvc3e7gaU99p4IiFp44kE8efjN4I4nKwN2c6rpJsHM80nrSdI8OrcoDVYBpuvWlc9fVvTnVZzWPxpvRDxp51zQLGh6Ux7kQOKhUim3tscUCi6edRe5oBi8X_0qPm6etTyPUL5Kf6HutRAGHrhcAxCSYfqARJt4zlrdkgIYoGZCz2UbGTooQjVk_kMvTHNZ1wIoqWMK14G69mMxuUscCDmWP0Fv_lW6ic4v8Ycd5zFHCWYxBy10nh_9XgJ7f5qan73S8t62G3cO8DNb4HyiAxozGE-KfSS60tB16Hd7lO3293IbX9SNyOMQXc8Ecm9zTvR5oXPK0JCEJRBomHrhbAq64YFSdarFwL7vBsGJJmoXg3rYTcMSK9PRSSz0mvhSEAkE9FrIVGUpvowSAaiV-Oo-K4ipMUwsSlpY-i_e8kAc2kca-NQv71B1vZI2W-02yjhTJFuYeMU4XaEhSHdBrA0o5rOwSlmxyAIFzAPDM-cbM_EPDpepZyNx7oFFNHMDSP4o9jSQQpxIlb2l-KYlb0EVSs5pLR70pYELd_5AgIm9H1gQt85Jlvhs9NaXziGBnAQbufmmJUOjM0a6cDYurjr8ZQh7no8bcZIyMXO6gm54qsySLSjQtQht0NgCkGFhrEE9VoMdzan8K0towgGz-a4o-EUQj8d9F0Dn-fIy2I4bTg3nASYGk5KabBzwhXGXjHujsodpj-dBZznIoYucxR_qEDhzXRv5pNiq_hDBQoxyPrNkLFtv9YScYweuei5YbWIFJ-2ZS9ngQQhXMvrMjFxQx-EiO2CGSJIOtmAU88QajokVyiFKzlmiNA1yxK_HETgjXgPcqTLoTaO6JNYOJLIXDmCClo5kppdqzlT4knXuX1eSnZqKHMWs0eirR70WH_QY_1Bj2UHNQAe1BB82MP2aMlE_RBVjfrxpxrJkQXRMIsjzmoSLJ9npOFh2Y6J49yFgIX91QaWld4Nwgi_mgTL0xlplFZm0CCt9YJWrrs4BgMTxmBgQkbDwIToM8dkf-l8Z60vCnbtdaL9Fcd9_QIWhO-PWNY-IRLI9VwGwQqix0v_giB_ehc1_yuSskUCCzGSy9NEKzNm7YWQEIShREy68Y1hSjIo7Zzk_wUhJNWPkB7tjHoMfw6HAok2b9Rj-FGP4c8B2KxceC_J4wjvJUJZh_QMAHgvw-aGjpo2Ny4o0UuJU0QGswVaOQS4lCwbHheTnENQWfe2X4Sldm6_crUrp8WcBjN3ZxArNybF7QgdtBK1Is16El2T8bkSPdJba6gJHfsvaC0mNF9D-uSVIT3AH_UAf9QD_FEP8Ec9wB_1AH_UA_xRD_BHPcAf9QB_1AP8UQ_wRz3AH_UAfxTiyrxCEdJj2FGPfEc1yB_VIH9Ug_xRDdhHNcgf1cSAqAb5oxrkj2pyRFSTI6KaHBH35AhcskmL4w6CEHQeWNXt50TD5bK46FWb7sjePclVhzZi0mOzgbQi-xOTcLUUx0wDPCX1HXn3JEMfrb_5AgIm9H1gQt85Jlups9NaXziG1l7Sf9IE27NyMpNxLBQpepoHWIowVlpSJgzXBoOVloQo7cDUzIzB0I_F8gZES9HTV_zt0Ewv8_OhGWjqh0MzUOL3QzO94D8fmoHi_xjiT_7HQzPQ3K-HZkCWJj1BIOkJAtLUY5BoeCc9QSDpCQLQlmRCQynDhE56FgCr3jayUVaI1jAuNcsaxsVmcoyUArSYsZ43Efv0XOAUkuEgFijEdE7RdhCn2A_Lfst_8rcMs3EhqTbV2H8YoJXPFcs27_RpIkN6ZD7pkfmkR-aTHplPemQ-6ZH5pEfmkx6ZT3pkPumR-aRHq5Me4056ZDyp4fSkhuCTGupPaqg_qaH-pEbZ0x5l5--qD5Bkz-UR9RUwEjQWjchMbuIzCJY_NNMgosyggUjZGoOXEL2i8BK6VzSTWxwNguUVhZcwvKLwEsbdFuJLDCwa2Oi_hskG4-y01hc5RRT6Kbonc9TGEgkjkm4SrNzP3v7B99g7cvA95hF7VlyLOWq9RcJI5dBqN5yHWchGQEhIRkBID5jPFpUKZUgPmEMPJZMDeihC5QDZRgaOqWhk4BeRwXIEzWB6Dle_arOkj69xC6ZvOA-mCzANpiulh52SWxiCSnbw7YzIKriOh4sECq8iPz9v8MxZJbtRoBBbKYfFzcjnuj41ZUg_EZ318GTWT0RnIcLOIclgyfop5ayHQrMeCs36OWq8SFyHJIMlq2eLsxpazXo8NgvBetYJsb49ZYBVJyJqeDerp7KzGsWGAZJMmULushOtjCIE3QZmHJMuxjHpcj4mXc7HpHvPzMy3Ypx4LsaJ53I-8VzOJ55LUN9hUGiQQXSXFSEgOjAhwjcw4-jzfD0BM44-zxfS-rm40thA8MPo870Ug6cY56WLcV66nM9LFyPXACn6CXuwpIp8pnPA9nnq8uY8dTHOUxfjPHVRD9IPhpWMQBuwnHu9kh-P8PRCvx3hgYZ-O8IDhf7lCE8v-s9HeKD4P_r3SvjxCE_RY-1Fj7VL8wchPdZe9Fj7nC9KN2RIj7UXPWpe9Kh50U_FIyMcGS_i6iieRJsaF64MGqfnBye84OgRdDTci3c9Ha61T4llM9w3nBvuAkwNd6X0MJrL-VC_QGF2d8lHH6VAIXb3t49_JB8lt3_E3HiH81oO4TN9GSSzq-ih-KIH1YseVC962L_oyQJFD8UXPRRf9FB80UPxRQ-qFz2oXvSgetGD6kUPqhc9qF7U6HRRo-BFjYIXNQpe1Gh7USP0RY1oFzUKXvbIOc7tejrMXrWTdYNgBLyrcVknKy5aGbyPlg-xno6619NR92pEvasewuT1m5uBejznPt9CwIxz7lLfOSZfuDg7rfVFdmLW0zn3at3gWwnlPu8lmr692zr85gh71bIgYFVUPWpZ9ahl1aOW87WVtkQTpupnl6t-drkKAc2h9fHNRdOiWkeSqxSkxMAhVmum2tV4SrXbGUyd1-fCx77aFYtAoBB1XtdDwhhrrLn_pEB05VNy_CMdSYH1YnzeJE7I3A8uwu2q_Jd7Xf99rtrwaNn9GQt5FFogj8r-qBcsaMWtZIag1GxGcKrRiIooD5sRFWqOLG9RkkmlOZa8JFmaFB6B5dwwXDC3QY3dOvkuc6SX-TlzBJr6IXMESvyeOdIL_nPmCBT_x51liz9mjkBz_5g50oQwDki4JsRPhpzCycDklGNrqiT6rOyLgfP7Ylg2cqy0xMD1D_Wn5ZrT_lgNt9ERHMtpQ9ZF6CnmyKmjJgS-MhU92mAKcSGsUgiYYZVCpGpWKUSTWC9FLdmkGBxssVpOn_a8cf70H125f2D0S_s-dH_-u6__x39mxki70pNVEuN35H3r29mLTPAY5Ot9ZbS1ZqCLYJNUB7yzCBVdq7TQT6qlNOdGynTZtYQX7iY6Vcnzrijada1apV1hf5T3R21_dCnqCKoUkawibUNg3WGnpVWLvZexbGBCzIG3Z0gCfEGLkY-MpjFg3WMfYTuzAV1UiIIIhyzJhjOOmgpnG75McUabOJGs4019ZUBrkuxBSJQ9CAqyB_omVykczZlVCmdgsErBR4pVCs7JWaXgG8QqBd8qVik4NWeVgkeRjaUkdPF7iZDqriUzkYpqOgN8S-RazxDmduH-zemvEZK37gnyEbsnQlmH2g4luoQ3yQlF6PNM-iY8z8vz_rP3zWG--LPkHX82flMCuCEMbjfAmktHm3YLmEChnwIm2NBPARMs9A8BEyj6rwETLP5vZu239NuACfzaB3wydIxtgCXgHfNG7cDuOOEEwWWxEPasK05IXvDNQPevvlMV3TecIgZeF4rQE7C0vl0xLC0Fbfi1RRTaFhyOy-tLFyjyjosb9VgSmWvBP2oQFho20KHvGor3T2w_P8ctfwhydya3ahy66-4WvSPV-pLps-6KeBwVqxeNFZMpjq6blMpn8cYVSqoWyVOB4Z-fiqBV0HHJ92_VfvmOOZeuSFp5bu25a_yK0TH66VYM3__nnf-Yar_w45nvaX8C8G_mTXt0hZN6TNSqq2zg0jMdUNm55JhzrF2JfYunOuGbLTx_5NWfeGq77oqZNnyVz5laT1TQk31JEj2JAgB9rPfCZpUSywe_Rx9tr-_DYeV2RqjCygUoqyt3EB1d4miBYwOOiAasSKijb_gi1vGXV0ITbGdv1qFmHU8p7c-Gh53IBA9-kyzICx8NrJzKmaKoN2uKIqDYogi6uEmZRju5oiChoJtPA33cZUkGQ_TUEpmsivR94_Oz5I80E7pLp4Mo9sDGg0l7ZVUuzmkNcgumJU7-SRx3kwER-7Qz3ZkZaONv6pz58GI3k5MqJDIzwCq-XL8YK5dPzY9zxkU-QZZAPMoxP7P174W1CzqcHjohngjTiaPJv8Tm0FbTZESlt0sduvxeqnohwGGEgv_sb0jnkBS_wtETIcFPC6Z1yMcNy-qEhzK_OuGxqfdOeCzxsxMeCv6rEx6L_-NupV_b9t4Jj8396oTv6zFgest8MnbPmp7tDHAUcT0LUDhayGHcNLVayAH9frTaQNX3opUBrIpWHjTqv5o6l7ZyUb2dUHeTutBUCeslS8NUiYEQ1j0UslBthqBr3ZANrBlYOGpreE-Z4uka_VJWVUp1OrRl63R4SUWnw2tqaLM0PrwHNC9re3gR3Sqgoxa9oMojHfPvBlZX9rCWbqkpmg1DmY5Fd9L5YeZar5uUyZm52ofKptfuFTEZxECbnkbB2FLv1Jn9_f-W9m69tsS8YdgfOmdB9wsO_GIgTl2gKWwndfK9FF_dhwBNEaAoULe_vjMjUiIlUpq1_bIX9pCi7rxLOlTuJ5qCSuIoSWVwHG3hqPa3rLdMPaFz69k6CpN2wuF4eOl2cppPis0jAlXE0GJYPnxKm63Ht-EAmXpANFxLcCHJ8hVdSPdUcJ83i0YbDGOrur6FG1tHSCcETxbaxoT16SVe_QrvaDr7tMfrKhryqSZYAFnD7fmgL3C7e77jSljdgS5jJTqfknY4cNJrFwCM4Qa10g0tKpZeTTHHEWWgSbhGWbi2lR66B3e1z0OkSsrkpg3ovyWqAxSIioYxFbVDp0F9IYwrbbgGEND1J4jpUE6wrZQPG_cgSPnWrIYCfgQQvGHxCVLgG69iUByGlL4m2KFdsmCnvZPFMYqS0O9KmlyCi9Ahq-WEOS4LOljMYVw5dKLZs2AP8jaUqfLfQu2e9l1yb3bhGKgHUXSBChjcBdrup7ttvHGdCPV_NrlSHPEAPGkZd9uK_wRfKaS6dtWHs59UMvEN4HIZoQLg0AhQRUzoMYE9qw8vvbPBkRHZiQRc_7oDIIzMXQ3j4AqeSL1wEUDzz1w7qEmwOOYiaw5iiApXFVi4MR-dBmuUsxX6LsoJFX0X5YRCP4lytqI_jnJC8R_6DeLyVroW5Wy7LZLMM74EKHw6y2kpqNCQXmzpURchlh6F8UAZjPquDEXxC1MfdMsdMLJuuaOMjpgBun6LUSBsKeHZlQ_QsHflc-xLAq5ukwby9eg2ieRiGe42gTkxcRkWJDu5TRoQmrMqNai6xDkNcXZ1xOnmEgl6coTETdgibkITMO87NSieQxORRARWVSWKUQe0-aEFTrX5Ec9-gxf8GjBgFUoIqE5E7v4XVY5Inc9bKzvOYYdVOaGIG5IoQuPJkY8jfkCAWK-IYE9VbBz4idaRduYSVLNxuEd0nXe2msipaTnkl9A_vn6TWFTyOxYF0AOLsrDWE_H9Ah3YRsnouzBt3Jhp48ZMGzdmOrsx086NSWvYbn7omLL509Z5mbbOy7R1XqYXzstE_Ksb5yWgGT1g2fd9StMmVY2SxN2KMs7sq9OcemncC3FATMxkE02blFjLZCvI0XqVTjCcYSUqLsKps2NwI11rm3SRFG9FzNVhK7k6LCViB6VUW4w0XEsHtCiow_lPO8x82b33_qe-PvsxnuhUGq4luOhDfEMXfYgyLvgQAXcMbp0GV5MeaWRmy7OAnDuN0IvM2tPJb4k7_kBhh3BybKaqIHSLLX3hBUw9VrbDTbT3Wy8fjLWRcNoWy93LASmrYGPkcrTclnBvK_N1uBeq-iLcCyW-D_e2gj8O90LxH5pteXmQ9xDuhep-Fu5tE9uCq_SLlFaFlt1YDKJlJ4ORC8lQ1E5gbWxisoCxicmiipTRg7N-Y5lSWHVz-UlBbACdg9iZHCzn1ljGg_PEkso9T5uNNDK4Xrs10OYu8ldooblLVIPLJAd2Polq6QKS2kYuEpottOx0XS0TN7KiiOWNLztvfNmsyYcUVWjirW8tX5WiaFTBjGfV6srgoj2GdjP3JIuybmy5owLT96siP3GLoktubYqo_PTBIW54ngvXhQ-i6KIMx_2NKMOJFENVjSEV4siAL2rmJ-6KwtI7J0dHoWmhevSmkOtu-WYu5ExpJ_oqi9MzCnNGqGWt55yrrDmeuFfLSC2TOUgxprd3-mYls7PtsWJZ_wXnbnnlyykbX07Z-HLKxlGDKJtMVNaLA6coWVv2HWNJJpVTQGFpbvJGyjGbtG_YMmdjalZSOSRjzojnyBIgmhcsqe9PNWo0NY9y08AnaU6mQDunmPTkKURXPinaba5EsS2nIXrDcKVcCQ1XypWQcadcCdofMVhShLTKvshEvyPaEOXkBSuaD4sjvEhnLeEtYnfdnS0R4CeilcFxotfDUaAl1zrp2KvhsYaMoNDQ61-EjKDM0K7fhIwqOz34VcioFX0uV_lJyAiKx5_ZHvW5FplaLurBOBgdPFvdZXMVbjejELi5TTQsOjXZsJDBuO1lKIrnNi5whZAu82tPQJllfkUTnxL1hCg3GeoxdbQTUdNQedUX016_JcH_21p1x84k_y9A415PQdWiys7pOahTiTd6VQRQoah19AMleN24kevGjVw3ruJ69gPXsze5nl3FgLLPe6nbnFhc2rLTmUEPJgtMrD8FlOopKZXR81Y1gWrVEFDbwCadc2OQ1s5KqjxjdIfzRm0hjEpRW4RG8bMrl4xo2ZPOXVy1rtR3mTnI84R7DfhOdln1O3TEShHB3EGpj6v0lMmCC_Uo9HE57pMQ66tUxdp9r2cFAjqyUQ78fbHABTPw5osZXLfc415AGCEGPzYMEHJLoRSPw7LrYd5O1Q4hgfSE48MdpB8f7jQKsHTFLYYYlxQ5uMWw0uji8m1cWNglQEefT39wE_NC606q2ZC8YOoZ-V6JdtKzMWMkIbF7bKECO3mi-silNPr-bM1OeNXie3s2oPWajE6QgZoI7UPgZBGKBNB1Pn9K60WfHcQvjMI52TrQ2MRtRVfvr2o7925LR8X7HtDPTPbVnw6-NmwKOfItm8EdUfWiDcQ5u_OAeLLAGeN5hbhro6PbSwmROjqEp7BbHxnbEZWE0t7jjaiNVkKcxejNO-B-Lh8_wbuFq3RevGwXW5WUb46VXmH5V1jmDVZ-gxRe1af41jLdGs3PGzJlaM6t3_iNzhMsbWB-AzM6LOugsKGock5L-Z5Tcd1SAYNkFSJc_9xBXget11p30HqFdu_LBiQSjHozot74qPY4rqNkqLZxVAQH69dCAUOCnEn1xHJdWxySZKMthrYxbHM2lSf5gH-rN4dxwO9sz42zHu7v49__gHMEP21dTlOsu5cZHqc3se5ebDidDrHuXmJ4nN7FurHg4m96F-vuxX_kb7pLT_4mF28_WVwzlHl1I3fgXazbw6Ifsy04mDRwZXXPUBRVVr8npAO1e0JQFbWWq6ISSjij5DOKcJsqr0GBCbd8sqFtkgg_aZcLdEIbkKQ_A0ERZA-gWFSCMigfQKLKbYMOsieQJLJs0EF5BaHmbs-Xq_TeH5Dya0py3mOfUdG51WdOCjlbxrfD4mhqiXR3Be2O78ruXfo9Z9N1bJRJHNtIyXcXeikt-a6Ej28JdQhJqb1WnK7BeQSN9Wxg57PB6J64veqgERPHBAS7LrBrryBH6z9-DUplxpQUXEtwW7DrHd0W7NJwe7Ar23n1ivpMGwgZlE8gSQsCgiLInkCS7gQERVA-gSSNCwjqIEkZA-mtN10kl9VCWdONoXV6RSpEZ046bxK1S-QVTSWkgH18ExHURM7B9faezrE_j3j5K7zD8fFe7xnv3U1AfUTWcw8D43Dt0OC_EkaizZnPCDKUIKKI1PIJ1XkqFSF982Gvz3eHoa-hRzp_VM3naHAv9EU0uJf5JhrcC30fDcaiP4wG9-I_1M6dn7Vzfw9qKYt6jvp1nxpZv5bBKMpkKHo6nHrZdbGBdBbyxPCbXOq5__FBc6QoZEDitzopgEuhOulfA8FSqjau37JeaL6rWEYgeiQioBelV7F-E70uvZkSTLg5eYIVHZY2NCe9FVQfZ462i_MqSi2VUGmx-v6NHpjiDwBVIGxBnTP-SRjt33P7Hn36rKVTCqTOYN2HhIUC5RrBtbfuQBsjBR7eFoGOa7f8-5qxNg7OzVdqp1vZB8Z9SOSAAZf-QhVTmGjCwi7NWGVqjyPNrXRMRCsPQJK9BmtAsrxg7kVQOoBEy6sRlEHmBJIUHyAogtJm3bdgdGLfKnzTSDV-VSmn86I1COgZEqHxa-1fBaPN7XIOOtI2t8HynbdHEhMgeHft2l1xdoRngFmp6fljJip8HnKJcQ5HQqtalZDto4HzDB4j1LbsXcccul2HwSy1sIbWbSPSqQ1VbsKMdj9z8govvsQrCh5uhosrTbWuUKGusGIJNYVFFyCcSZavUxSXF16l91Q4bgqvGsdUmK_sQnezGwlny6pfll8XcKXZ-s7ZT5RH0PJK0FezbGe-O9eVCi6Y21UzVqrmr2E9awt3aUtPyCZLho0W9zDwoSYweai5N2EqnE6F_aawPxU2m8LmUDjrZVfFkxcNetFV0Z2avGnxUpQxpZWxi3wdWP4qF2bJLHqVACT5hwAkeXoAJPlfGkjyv4Bqp5Pj8sdDZ9KvKVZNVUi4Xa2QLBXwFnRt95fiFnH7E7IdQbs-gSrPe8cF6gNHPPMVvaPDBOid8cZTUHu8-g0epDbsHDWOHmMXIC98PTBJeZmk7lmBUXjjhIGBfYVawXhZ0_Yd03mJo6atWY85Mden57Vv-u3XJYx_N5UOs9A8SYGaM-8KKd0wZDOs0bCPNXIfJxEenZ1QSwZUyaBsosYDT-oHn7El3wWDocy3wWAo9kUwGEp8HwxuBX8cDIbiP3Q3-fhlMBiq-2kwmCxFyVklgyure3FWgabp3Wq6dVjZwCBdMSkBYl9kOPZo5L8tICHflYFEyxnaw108oH9BZYr5CPVtoQeT0O-yD3mPJXXBlwNINO0bwclxlQrlTx6e9bTOUaYUhW_c0GSw-YFuBgscZimsvWTKvo1rgFSScHsQ-za751jTXX-vig2ao7mnXrj4j40jHwC-yt3FH1WDGlBUixngR0ME1sdiiPCmhLqtRrAxuMjqSdk7qZV7S4WnqPqaxAzvZbKC_BlP3inT2CtdZ9JGbiX6MqrRJkTfMc12O0IGd30u-Hss9uU5MA3aea8MNZRHdil7rbJb5b-l0NWSqwOPWDDlYk339Tvml1nLelI29bK_lcKo7eTWqrstGZeXbZb175iaSxn0RmRtJ_3SHy4X7XiHe0E7XvkK79w-uAXhXT90PZnjnfVuH1_SK9_g6fV2Jdz3gxL6zdq92vxCu_bH-1cnVFERHzjxBU7RFXpPxcNQ6B3kfAdIHzCYLZlSW_whEgA9ERN6ypZ2miZYDQMV9YZBdPoVRpR40pvSOhzcUN_bm_fXt55lFtpEDiYJbqdGuYnb_m0cdR9riOHDqailTM9sXURQiLfd347Fcq8mQNJq7UwYVQhq0eER1b4w3-O5gCTXRsh6qXwoNY0bA0m-lzCuDVBAkp8njGQMBSTpKyGcQJJ-HPQkwSBlHYLDNTiINFYeacTvcqQRlRJcmLJqHQIE5AynIOL4FzjpBU494_QX7SWcXbYiX8mSBhLm-0-WNamDRCMg6FmcQcjinHhKO4wBah1wNy--od3Z4wLOnnOp9Zuk8rM2imp5GMk-cqd3IJFgXkulksWW_hkBgJuBAUOQrz7k8kNVsKkIUZD47HQ1mk9Q17vVORIx2JR0xZ1_DvTEpjYjmtodMCtPhoYzVKWcxbJ87ljbqfo-FAFJfZehqL7LUHSDhxAWrrPMYlPUppPOnY8c1dshqg94L_P1YKSPajDM9Uu8sxoM9Hbu50QR3yik0JdXqFZVJKeRYX5vVk8vTOY49rQRpptRza6tpPiTtL_4g7S_-JO0v_jztL_4b0v7i_-mtL84p_2pl8DAdo8jvjRzmTgiSwpIUhfjuMVBAUnqIoAkdRFAkroYR-hIAUliL-o5SlHPG4rC3TgcJA5U0gcq6QOV9IFK-kAlfaCSPlBJHyg9LSsKaVkTC5hyC_3EHgSYkCExlStSSAik3gYF3W6RJHVO3yavLYNlHcanb4L5DSxtYFWHTZc0cFjcwERvdtQz6KKeoxWFbDUOEpd71Zd71Zd71Zd71Zd71Ze7kBTLQeJyF1Ji-XIXfLl05S6aHAdbRc_ka1vD4htIUAupZGnKYP_W8-MEfExJGxfLiwTdihEhL-9-yMDfv8u3x-q8ClFAMA-5AbGgsbbh50msDMQ70GEelVXSMpNpOWeWb9k-dn3_CJnFNtTPCKuAzzlicPk-WeXuQw52eJ9D9z5n9wsDmuB2dnC4dBC4vUKWjsZzzszaXiefljbqVRz0VpJccTMVLWYxtPgQ2DqNQftq2GCFZVTgVpBr2J9Dd6V8Ug3tWsi2t_FmxkCQrX-uYUr5E1yoF3I4oTrvPvf1--nXhiy0wVn7CXdyAHAyijqtuxabq2wY7iJ-_uwK3YPthvxIB-r3c4DPrQB36XgAwXOIMbpPeNjOx5vQjhKawGp7dOz5Fn24mJ-uIhcHJr3Ow__p13noiGalh9ecqHQogllbPu4hmdrNOWfo66d_Rz623JhF4aKkR5TUo5sRzZCFqSc9_pz0YHwaYQ4ZJIo4AEkiDkCSiEvj0mcZJIq4NAJUCkgScUkIniNICHpzkDhQUR-oqA9U1Acq6gMV9YGK-kBFfaCiPlBC_B1BetpC0nM4UtEHqugDVfSBKvpAFX2gij5QRR8oPQElCZkaKInaarvF9rTHO0IcomqH4FZZxuuogrCzOJND4WiCskNc11HAeQYqReqxslV2tldAmETk7MieGVZZmB4qd0hi-STc8d0RYq9WQehsclsrsFoZIyoNR911IrFA2ProE-BB-1nm0zqz4pWhM07YRVBOcb68oNmB_pDgbe1bh4k1bHWNlNOja9wqTDKPVFV1GKT76CXPwtNRgeyj7jR12ak6DCzl3PvBV7IzQ3UFxSM5IB8uraoUCin5edPVmvTJgS3SuEwYX79BBEVtDSFG7kfouaDu8EDhilaQ-7H1HVIPWAt6Aa9uiS-rde68nR2z3oNtk97HhuB7-_miBf9e7tGt96mrucdAv0pdzf32krepq1Aix29TV2F-h1v3u9RVKE6cnt-4TPOTkfT6zZ5e3bdv9kyrK8Z1Wa58mCOEjQDJYWBs1qo9IeAt2Z7XuzJ-UqEoGEh9G3iIy1LnFeQNgu0Isngb21Oj3m8L10uXDYZwpxFv3Hwn6IogHWoudKHZITg7wA6AfIo6SwFhVnojsfWWdQwyeIrLio7eFmVdIbyWdYXxdk6e25DVIWLwsMKf803GupUODqSIERQMkONXLx9Ba4vuikDUCB4Oc_ZwdLK2gHqw8bIg4brzskA6WG7Wdyifmh8HyDOuGDzOrjaViRR_0kFA6b-zJ6F0G5H2iiE0o0DuTBBSZDDvMpd2o6jn-TVY1qX4kbNxZwLuueIpshB06afhZClc0l4TKfWFJlL6OdGNktFqOmgipb7VRIpj-sUeaygZsiwp5CUFmU5dGSvv_c7WaeR3wqzUgSFehF1Mt4b4J4ldT_Oi21hIVZU4BEGWCH19qQi4wPYIosTtIyuAaicrm2l0RJW-9bWmwjbCcixnbWy9sqb42O5mh0yf0gWybncTOJ3tMKHSyaPZ55XIzZJOUrfUgSFK9JKUPnIKckoW7b8slmn_FQwykYpUJitlU0nQW8gWaBeT0uhJ4pSO4RlvJ5jHYMuCufhmiz-CeW-LlwjGstnY4jNZW45CHOnWresABHMpO8Fc8l4wlywI5pQo77wfZyASt8A1giitmycsQyyEFUHRW56305rsJmtwqandHNFEMydIIXNDmuhnSW1zxU3mc-3gT1uMpBIMXCyRCYsRkNLPhysRkBXBrPXQQMu-JueOsRsd8TnaGujmBRZGZF91o-OqelC7f2eH1J9POOkktTt-3mGi9kIB4-5zWUOq42ptyW0y0TF770rtrv0vctJqDxW8z0mDioZv5U1OWmUnO77KSavz6eXvctLqfGD6KwdLjbODRctJQykNs5Xi-s2vsp0vzI2Do0rnGpE87g9NgaLUZd2i7549CdkdMfaKWk7QbFHwUgxFrJP2y4NTpo0kd6HvIw0hjmnaIQhP9Ex1CE_OTG0dxs7yxhd2Ga5mqh_XVG4mibMnuKF-Uqv2Y5o5HVRc_zj4ft0WuE-z3sAwY3io3q_MPZhcbXhQQX7X-iRUF_qMJr2wuJoKXnn3yewJjnq14751zl3NareQobBt45Q_C_fkCK468T3TQGRwxfuSCWalErimyBBY2fiMgqvpk1wH05INnDsYoxWoHAg9WaByN8ayeQ4CDvG2LCtrWARBgHdPj26yA5G98X_poUysyVsS27u36RHrYNNjlUZBg82HaEnbv4jg1d0JI7BxAOAYqabeIKGBJOdBxxBcexINsIJEZs6aKHJzshpkQ2lDgmNI8TQyzNMSmubBxPVbXWXkNDbpiOE3jUqUBSxs15rUWGm-Oenj8VRZ6V1bu7nu5tAPrsqir9FqzPRh5xo3RdzYWPTNzjUeDZq7Mc0nhgwXc7YAsCZzAWBN5rIXK3s48SNEAmG0MHI6JwaEF5zYGh8YN0XdenBUgtI4audEGAPupPwjU_xnPUpLaSEOocOMQal7C1Rlz2g0Xb-m8-eNcQZ4enqagGDWel4k1iHmMbFug0jSQKwlRqMkFqyncIXf272L2toXLmpoyd77DM154X2GNp3IHZ3ZzWAAal8FuaHMt0FurOp9kBtLfB3kxnn7YZAbi__MBrtKfxfkxup-GOSG9bVx8cPi0uPV0IItCS1KMK04KUyBS1aNceDqlwsbpTBK0d5_DZQ36klnA5rqYdUYAR3WTVE9_jDYh6YgWMU_zOGii3lCkUINhJVseyA5ufkY7FtgRi2KBji4n1pcj90DxiZYAPT1YAF0dEviEG_AgZbSALB_MkyLxfC2bZRffYQ6hj5LHeUUz9gtNo6ixjOkzcSbuCm67nKMPogLQIhSsGWwxXsXP2Fr4oy3iZ-QOd63fYthNAyIW9g7YrGJW9hb4ixxCwiJWNucSTwkgkzavXB5W1cokgB_6-0GSqACrsZRRwv7REDr8otIPmKdrH6o7eAeh17qrlPo3M6md4JzdRqalVtA7N46cJ0unzbZV2N-NUnlhkNWQVB8ogIBpQlrBjMH6ZkAZL1otZNR27V_O0ZadvaKoJKPEmt0R48za7_MvYXh4_SFqbFQ8nWQdeAe47x3ra-TqqwzL5KqJrrHHLBOdZsDhg4HF5vDIddPqKkQfwLMa9L9CRzTC74JdBM4SJT3mvP3wogcg5UG926SvL8EvHp_Own0_CTV5TFQ9i4Pl8HlsfN53MQ6kur0mAZugciDTx0RgHeOEiPmMUq8QTRrzcc4OODpcXABob2d-vzjIQnzNhXhbjlXFhARFO2r7ttocOeiLGPB2PX9dqj3YVwo9FUYFyv6KoyLhX4QxoWiPw3jYvEfuhDaHZlvwrgWRifoU7wz62GSd975TkQDbQzcsYYUkUjr59XDvQnQsxCXT1PqEwVxMTrBeD5au2cLYONID_8uXboI98fSknArIQNMLzcVujrGaVzkmD617Jvk16ekZhwHl9_J5qoX7hSrRqncLEUIB63VbesVUHjzn1UaWd1-_eZi5sv0NkyigWVSzK_JUfJAA4FaowCi4cWcz6ReP7uuHch7P65p3ICtwvzDlkifJXtyO8AGKcIGTYy97yx3YQvDFWQXqDbGIV7uroArMjkRCsqVN2_y7TjuNufPRbKvPHlFwU0i8GSOjzGdMHBMhLv6GIixntIu6rLBUEaAortDK2EwWHogtWUeekxi-t7DELwcvCszyvXn1C0vt3BfszaKfp-6MhZUpQjedKYHZ3ahJcCtDWnE5LihICuC1vTWXjk8miS-wIS3fdjQTmm249zumUJjCcThpWF36nEel1d70Fu8q9nkuxqq6geaidk_-qGcLip_SDtg3QFJkqZqUbS6FSRkIoH79ihE9-mhvAnwrKMbl60SoUEJMIlglpoJC0aBEcpRpszVEypoArBJ6R_5pFBIUiFVhchzsdQnYfSBnDq7XBcY6eYKnsjBAfMbWNrA6gZmVPk71XvASS9wqo7jXaHtuRknvngGmOPWU0wPCKVZtLaEz6UrXCsocZI7rNgknX1Dydh5KzlMCqe71ZpyAzzRXRBAtBPK7SVA0gB1C4iGbyFOT4X6LdRIULznHddVt8pRtwrj3rKmHbH8aVChcLstGhZlbSHtwX4PNhrYsLqHgKTt8xMOzwLnFdWpItQAlV50MB-oriAGuhUkTSRIIaVA16gIkoJUtC6rViVCVgHOKmIQsG1jv6rkfcZB7DeffJVxAFV9kXEAJb7POGgFf5xxAMV_6C6I5cuMA6ju64wDkMnRwBuosb3wg5pVu24OVfNJswKy5VpnpTyXs10TSs-r2OgEbQg1lRi7Ztk_Dbc_11IZPSqSKTDOQIlC2WlXFXt9NtojpPQ-OBRsWOdm9WntIdGMFpoUJraJqCvTWFOI3GeKIQ_dQl3C6JpAdLpGAsMhw8oG5s4aCDR-j1POWkqr634D_FFSuHZipwUKugIK1ei6O2PMDDotGvAW8jp0lsdwjTQO6ogL4HfigmZRBwZffcEdXt7Ae8KzhrA2oCsSreVrDQMhKlXgTdnYCNcjAUT0eM5hukTvHhWKFN8glQ0SCveJUgdjZCDqkYGw4i26CsFz6xg5BacwnLGMCCabVC9Qg4CEUN_dP3RbwDZy7cWZ-53CXwUnZXZhiLgTM-10o0R3oifhgJsDfTLALmYc4r1Bh8gOcyxElxMRgys2jLSjfg5kNYjb5VRHiBICq5YhljNiOSCiowV6nZunpSNPXkgmqrw3DJEixI6gU0PGmZ7jN3VtoqNstHXEmLUjFCu-wnJfYEXEkqtSwUUBwy5vec20CQL_h-RnrQrDlx6jUQPthGQDjDW0gooOwvwBhZ4IWdMRSoMkCKgQvbJ9srUrE_zy33LrmL8T1TJtey-43QCM2tyjIM_OuxQEpbMD8w5oX2iQjfwJKW-QUKcarcf57JCgQvIestPhlJYvLZK1PGfZZDbpEZ4__pf5q_2r_cS_mr-CODQrctPHkgIAEcXJz2IzLXcaDRTQ9FoXZG0SisuwvIHZs4YJg7vH0eNiXQsFnJ2vDFWwxA-lrToa7LIjwnpOqiPkdwinKnRFMylJC7yCE1xXhPuS2da_0WOTdpasq6kwBlFQUwdOUHFQAYWWDj2Oa6mF8QzNg0WxFlcZarETkVkTRh6zB2tR1rSEUZkjjjKrtYGGzSpNxJn2O-irE97QVxPduLoOOquFaoGncZ7iGFB5PqkQHa9WGacSf0VldeVFv4pTRXsNBpmToj-kMOsPqwoCONGccUhjqLUaaFMkjSJJV-oFOociCOO_Cj0RsgZ0wamW-zsUX2Q25f6uxfvMJqjou8wmKPSTzKZW9MeZTVD8h67KXN9mNqHylz3cc4MDdAzYUp0v99igoHflHiOkH3cB2ywFbNEfmfk5DArZnL9g1SrOylbtwQ-Z3Ta-2NHsEs9lWGZpsRDXpV0V4rpqUyiO0g6iYE6TRyFqdDer0d2sRnfXsZfLbh2aeRO2zZuwbd6EbfMmbItTsFMhcRa3OC_CtlkP28JLlVdVdwJPqC2HF5RRGx4pu8Ztc8yf5xlwGz4ps_5BzFbAYPFanQLqam2ANtoo3L55RNDV1ZxeIej6aO7JMQq8HuDm1ADlGDTv4gl-asBGIYYh8iQDXMLCTCQZqelnubSwE7lYA1QtRPBn1RpGTMbh_GAE-VGZzeOJLO48NStS3SOZrfpOZUIPcc9IlDURJEnFRwakaOHZT60RkxknLLlL_aj_UoeZNP0q1MBxiKIfJrGjesI9RVRy-JlDPPsJdaXJuEHVaUYrIWIvapiHaVZGsx7Tz3pMH1e0Rk-EqIH7vAbuGWQ-vtR6WGigkphU7DuWm9MbSw-DcB9-_z4Tne4P40bZDB3bG2bjqvY5yPPbf2zOcYQk2nnIRIyugXoJm1zQ5z8QO837tMxLmh0VaC5pjFKhUp-nq8rHuftNrF2ZuU22fpKzbocJ3URMoX5nSe3zKKCq3q-klFV1WVPvhcllZf8GJ2-RQ89cUy89qDJr6qXHoRbNOFAEdsIAVetSQaU1O9WaVs50Z7O0gIFtn9Wd-u62WBRFaSzRepWxpJpheRFyxw5vtMfSrvkJmamPv_2zkYj6SOc491S7_r3w7-iNKE9UhB4c8JTO-j0q34vyHe_AYaOIcrFRW4TaBF4dZLRPWqIaxSEqgIQTX-C4HU6hzV1i2iDBJxILWB4L3FtqIKnoMauix6yKHrMqa_yJ0eMQYFE1LNytEO6GuVbw7iRcdY2laFbUZSUlTKRkSJaSJlyt8uAP52YVnNoUv4eEKB45jaUwxtrPT6mOirpEjsbY1X4UZWWO1XjOb2b3hGGdmdkibZngkKB9E3Kva1HyrwDsD64Tp6JR8FwJhWm9J-x2mmQKUSN862wsLBpHbsei6ybW1HE2sbK6iZVVIVaG8atWbp9FRVdEIFy9WuDq3AHtCXDOnANNHnvbzUj0x6M5WnugQLhja2D1JSljJVgV-UPsgXGgG7lu5eEX2V6EAd5bnoCkWZ6VTaNipuEi1EJGlAkIpmBly2Frk-K62EmbmufWCFbjVNsCluNX3agcrOOQhtUQiQGo5WEBYlpavdipNTBM0e5sY0CoaQZqpfFZKbUL7NOpHzgg0UpUJhFd9aBN1YM2VQ_a1DUAw-iJkDXQ08ILzjy28FfJ5VDm2-RyrOp9cjmW-Dq5HAr-NLkci_8sYuPupfpNcjlW921yubWtdPM_hPqs7PYpjU-gqGAlgjrkDD0NKcHHZXdS2ji0QA4AQb37fB5owQlJDjpIbdGs2UCa1DLjA6mfHbkETccZiLpUURFyBgI3VcxlN8r4USCdF_p9mRRaa5yy1YfitDaYwsTWUoSZ8KQ-zXNNIacTf9osU4xTXEmbX6I-YUN2qhk2ZY9zjvNgc17QEVU9KM9gYOFfPAAt_NvFT6382cgH3dC125GLG4hZVBB7z7qGCHpeHzqm59EVjXj-JV56iVe_wnMv6z3j-W_wqEKq4JmXeJ5oKMd-vMCrX-Gd25e-6a8-fqj548CkreaPwxL6dU_U0EhMtE0oIrV6QgXlHOrVHE4IVhxO0DdF5e5IPMalgNejm4W2YHf4srOSneavdaWDOY0O9ihaIJ54yY_2zIYn54Jj3iJ11ufsGzwmqyVFGDevCFKDP8hCdJCgP-OqUxshQtYoU2o3Qzq4GfKSAXDxFCiC_fuffuHQ7V77Bdnd_BMY20Prs44qgpF-3AsoK5wsAyFkxyFC_km8Xha0E-sV35GTwUTVtP081qJFMZBQ29a_r2ItdcsdMwvGGmm4z_1uzjW6DY5ARLmtaCIj3QMBvNn2Izh7Hg7L5ig7YHEeZQLQU_G6TBijOeUm0PpI-6tMJU7NV6QB7o4XgsNm5EwDdXqSC9i23fr_M91CmiOK7TPCvIV8gpnU7JjhY0Gbc0_NfWmPuYOPcJVY39vLmHE-o98S2nnTOpYcg1-kxLlYxEfzdgTQsXiop3sWx5w6xzirChKvmgaydr527Fohd2wZHwFmyPiinFiCvvp7sV9HK_HrN_7gksUrvmD8aotwu2Q-wT2Rdri4k0Ty7hygj7uxcvg0IcCQBqk7m9TXT0zFP3V6kdRza1W86st5wupy_c7RvpDq7br_9cgUjI-XZzyqgmUJVqj5BS304wloT8oExfW1iKj32FAs3Ej5l37JlwLuXZMLB4DaFvK_b0-p1fdN-ox0IiuSXNgFmpUt03dsrRrrQ64nOP7c4xL8LlsbCn2VrY0VfZWtjYV-kK0NRX-arY3Ff-j7u_SMX--ytZENunElpHS5tXP0xNYEyoei3U8fVgHK6ffIjVLJici4N1O8E5P2Ei999HNJmXI-Uc4yAQ4nNfOOb6RbH3lV6nQS6rAepBsf_EM9q3Sb5ijiHO1GamBthmtM9zRm9lVF9k1FVqoogBCBZUG_hOVLlnFcTSBiOabw3fLvXfw6OJTGrSVHKlFgeQOzqkifaB9w8kY1aFzf1a3QByT_QjNw8Y2S4Ypp91tv63OvkGy711LUMUZ9e6zR9rNWcw-DrtXQEX2h-1xV_rol1E7zmXEkvWdHp2s9M5Kq83BErvGgleIq3LDSfHPx1--wWDINZ-TOT0iVInmGNPvECkVNE6pgQ13L7y2mN9RyQ0whJA-o3cCUUQulOtcvedi2g9SRpFYSbxs08FHenhx-9Og3WUNd_xT10hAJ7m-4EnZgB5yiZt2WD3l6geifrsf_Uf9MhPOseqmbL5hFceHXmGMHKTHHDk96Uelm2KVWXaPxnkueTf0bInVDpNrGNTy-ANO257Xr7M1N7a9_-Od__b__N_8fzN__3T_-97_85__xv_39P_zN3ywt3Cg9BEEUrmR0uQD3pheUVY0-Zaoy0klog7bxKPh66tj-_S4691yP7OtFb_d6re402QdljS-JQz26suZ7rGunQ81Ym9HQlDVvdr2adsOpOf5Nc7zUHL5uzsNn5OFDnQ2WH_3ily9p-VLlUqtOCNO3fq_Kd6PokN7ouqD3G1jawOoGZs66I3R6j5Ne4NSje8rnN5qjjy-UUG9fkQov9Esf3miOd4U7zXH08Kxf3j18oTlexH6dvWYrlqQ97ml1_XFFUzXIGVXWIb0TVT9BS_PxNWYRNS8hTxNRZ9VPisY21En1m5U-oLdospJnXqx7QZJ6vWh98AzGO1XOx_xsDWvsJ9YxhkSb8z12zLU5X-fvmex_4buZvoPLK7ijI3HJIGxlvs4ghKq-yCCEEt9nEAYnehHfZhBC8R96EYP7MoMQqvvhg7hYXFKNAt6kqOjJoehFxx2AoiIUeoBvo4eH8obIRqMkCKLSQjrI1acwjkXKmkofNVWX6SS0fu2oH2NrQY-thVextfAuthaU2No0z6fK3LvK3KGynn2wU1dnrM3Max5ImDv6JS5fyvLFyVoi1LV-L4r22AjJml4jpsDKBubOmh00dI-zC042rSeYF8pYSFskmHD_RvkL9oUeGV5FTMM-Yor9e-OnDOmNtnn38KxH3j3c-SlHF1_QehfJvRr26-TNnHEkfXRHp2ujM5Kqi3LEWRMFnCCqgoIPEjB3nj1UGkN-64QM9lR91_aCvSeC-fh-a06-kFs8OVFkmLhFMwztDd7qPsaPS9mIXhiUKHOYo8z43U3fbWOjEU6d9GQ2ZNXxENmMb8KT8RCejGt4NAF_j_0YEPL2DsGLG9dvdc3-4u3dqBoEQRQ4ZEC4qhFPocjeS1VudhLqEB7kM8dSZ-sgeGeszThofiI-42uWFqTtxHGGl38Sk3yicJ5qgrEzuPDQM8JczwDiAP3MDO4KwYMVe2wTk4NiHG_YLjSee6dFV1icwpczuZZthOPbxXfcBDfjJrgZXwQwsckb1SC-iQPGV3HAGFKTdu5TXc2_-it4DAmEcMgfk9tDRTql-4Ve2-MsEpmraZ_KiSDbb3dJLw4DKfYEqOY1at5iFkp0I6DQEyFWj0iYBBXDNglKBqNeIEMDY81PUjO8_kk3FHsWlG3B3xM_mTHoBi6UJ5i1qpZzDscLOHbFHL2pRCUsdAjSuITLMtkL9DutZHWkJH90pKwZWa3QdxlZUNF3GVlQ6CcZWa3ojzOyoPgPfSnJv87IotXVNH-5XSqLNIIblrCN8Rd_xRKLukm0DzRP0Pa3PgGSfYPUUtSPWEXGgouheqeWT0t__vQ7qEYD1m_TRViWd10DWUmnSWq4j2Og5GbUKUKpH7EzjIi9zAu4aW-thwRMmVKX-k2DCW8eRvbHQHWFZCyj1nezotAz0xewplsthQUoKTvpeKB2NLREvzzDlCTcqkIgziZsKl54j2J0FDjs2Kg8h42GMtKBaQc0x5Luuati0XV43Scks0Hqxngq6bly7FJEPjm52ZhO1CjXcC3BReP8DV00vWXcboK3Nhi6ddqROnosJVXynSrODK7en4mTcigPlyqKCOkdQjw0QaDQD6Wk-Rwtvy4FG8ngIh3lttOporC9BhQ34eaST-yz10_H4LFkmL88Dhkv3gpYK4__ITZSlizpsUoXDEcVTInCo6sZusBM6n4rDmA84YZXUJOTSU1N_5QUh5TPzavq0vzJ-Y3gz-Q0nCjgKRo8rixJ5CzFWWSkg5qQ8ajLGemkACytXmBLx4n4HnVosKyDRIWgd20Vi7m_0qVqBNkxqaticE8Ar2BWrIiuAIXDoysI4NZAu9U3oIkNKYjeGtaaICgmKBobLUn0tsI6ZCtxoZs7iQtdleUmdFEBYmRhJy-z2yAhj8499Cbz8NwPxihw5f09Dj-V30gZ7IXO23OPymoIGCLcMX-oJ7049wht3qAGyzofPt0HEtjqMJHNDeXOUImbuXOHQ-Crs3Vk2xkSae7z4r6kYeGVcLSblwSEVubrBASo6osEBCjxfQJCK_jjBAQo_kOj-RYTXyUgQHXfv4_bSufZ5G5ftiZ3IX6hjeQt6FdSJGHh54ck4EGWlu5bfoGlGOYoVefWLqCd6T0aosKiDhMlbh-AVSAWEpMQpWnpjvxVfgHMKsKygYsuBkEYFatJMth3apmtjIPW7WRco6KIsVZeA6IbfSfjStggdZuwWPvYYyF_gr9vVkXbjWjZGo4lNuCODtp-Mk5bvSjTYFBUmVcwQKbBw5vyG6GKw3ZCWG27LlQLBtI2ry9gQyLBUi8egDW6s76gVS-srzECAfz2LT3QEiMM4uHUGCu-POGN-xWHFijJgkGmYDGjTKNEJDjM4jDMIuXp1O9dWvcruATghvT03KOeAoVZh9dcrDDYJLLSX9XzDBwD4gtzVJOi7F2FQEZyFSJbaSgST6pehaiutaq61urZb1bP3rfqzyhn71vt3jf6ced1a_VqwJ1Lrr7xttU3frvq3yDt_XbFLAuYCH0r4JQs4eBYX9r2E6H06QP3hFA_XY1kI2u4luAi739DF2WAjNv9f60Nhu4HnZdX_wauy4qaDuX3DwnBmJ_gm_oPfkxcHScE3c1Y_Qnh5Kis9R2COyFsenHyplavIHRhWr08EwPByFMxEHqs5yyOG25FccxAlYHE0uYkqQeqf4_aRtmc5T_smszF_-SCDZQ7W0esdBTRAFJdpNdvewklXjLPVgpJ6bHC86UNxhQo0ymoGZCYPzAGljhwQsT8gxMeX4Lr8oBwe53POTQb15t4dBss4XYo9FW4HSv6KtyOhX4QboeiPw23Y_GfeQ6u0m_D7bYJNm_QLbt-q6u6BUlbgJHj8omZsRwkZYYBaEoe4TBmNIPyCcNkQbOZvhZ-F_JEj2ehgRLAio6AEQcONXjWEyBnDGdPy0sDuORFgBuUvWneYfbGJ3x0fnOtMpAm1ypDKX6bHxsnCaK9M4JNj2qhHUi4HAkJclCFFRh29zpzHHyiYr6qz6wzsnyfHhf50286noa8rQcOS-QWZrgYUGm3gCC9qwFM_iIf2vNWl76XnjvjPKxv-9hwBtKguNuHodyPu-C9xfO14TAAA3H2IHEMsTZczxZdBji6-F3JxeVwKXzFKTe3nrjerdVWqrX6wrLwJIrfLCybAGeDYnZ3TXIc7SpJ3iDMFlWwoEmu7tb6OuoUSKdKBrbRZkuSUZ7e2SHLehlWCpOGk8LFsRSI94RaqQHTAIk404Tw68vttHK768XOSxYgAabmiZNXYM1pePcouG33tpEGCviHLpzY3oaNpr8d266riPQVWgnvtncZqrNvUeFcw7XNb-0usDMQrY1dpQXtwqWjzjaHeqDMt6EerOp9qAdLfB3qgYI_DfVg8R8qbC59F-rB6r4O9YB4cM3VZXCT9u-p8XpFqjgDxTaSp5F2wdALODl9FEvbWpDCRnbNvejf5V50cKevibzRzOXFyqX6m84Wqx9g5-cHeGvkMZ1opEhGZNbIKTUb6ac1gZODfsswc4JNckIi_QLF2x2KUVFQQ4B7lOLyCY5sEPHpcuLSgV4O26HzfdmGVTOLxFHXWABE2MxEBRBpzUCBjlx2SHrYysW9bzfF5EJ0lNdryI4gg7_xFS74GxXc4W90rMHt8Vbnrj4d2yviys09oo7WCqhTY0HBxmv17kyfXC0Vhu4JvTvHpKELXBwiv_J4znH9ZuZdD28yes_O9z0PJ_r-JuO96ZCDt1cZke37yyZ4xHR2n3Lfcgr-j8crUW_efz-Eu5FySCAaRsDessra8BCIovhwEhXPqdwi6E4qqY_0k6h4iUriVB7V4CpwUalZFWWukoEkkvOW9vEeCXu_xMt0BNBFoAhxBaFjprqpBOohUGRoFY_HKdenmjIVQq7tx_nXhaNr6wMZn2--khQXuTJh-A1GfIkh1dJcndBxZ5_zNOmJdGVOyVG8MPBkjxHgZQ0PzvoI9f7eVeyNRnAstkkBfSbw9uMJG-ZeY_CcBilMtK1bWS6P67Y-tgBfAHzwKjRtaXR6N1j-3VhVMlQ7cho1FJ5-zfxE_5OHw8ftfCSITW_4sUn8nOTnJwJdvMRL5QVfFGvReiiWN6u9RsUFPraN3vjOWkB0BNqM9bTn3K1mPM44Zh3B5Tt_2oK4k2g3xuQsCF7wN9E5WGDzRAgw_b3taQgZCsYScGHJhxAVqMcBF6GOick7-7wt2ktKPCMSogT-80RjBiCAMUf2QLB7fhZypXhBxEN3bwC_MCB3pjah9brDy7rDhpcudYd93e2s6UITpxA4Vchz02QWAngq-57wVHGArGuul2CwsR38fTsMKhpvly4t-PSnsa6m5hCWjgPMSvTxZcuOsOh25IsRJZWxhZBoqUP_X63AKhFcaL0ayaMIgwF_tw6oCKtmWZ5au3ldqnwDDh7qPl7gRCzuSOSUNi8NzTi_6YNFPjWU4p8z7TZcZoihdmwkSVGysI7koKnoHo-Y30DkZVzed6TA7dOd0PPo9j5tszS_vzlulnZLPtq5Aik0w7vBQzO8F8zBGx1rweYeBaxhj6O_PcmnMcg3f_CZtAoSnUyLKgufz_yhd3UsM8rBmzl9dAixEWxaw1Qbn1m7VCZMLqlJn9_84cf9-Qznpa3rHF86w9JxYZrPaHWLZilSbdYDbWls_oXG0OJzGby7vSn9CqEcmHehgioTnyuEXFfsNRCyH0t1r0h9f8T10b5X9wmxXSTFuIUUpo3C6XEEqe_84e4QQYJ_kdUlg5IOqisI0vZ9KswU_tNPJsyQe_Qd2ZZJ0Nn5xk39_r4DxnpoDXdoYpq_sj9TTVwiidHGpGj0KClD5k0iO1fqKgUL_SSbWmseRdGtDdzVSbOB-i4EjC1Pxp5seHJCc7MfwVcnLrxBsuJdS3yCjywYx_BRFsQ6p1n2GuI00XYYxmbX_WW6l74LMy60Vph0oanLvO-5ecLLGPdsOsUtGh-WkVrPvluiPrrB3BNJd0LbN5n6fBNNXxGIlu8L4HPXjXDtjwLm1a6F6TBWJo9SeOSRI85uns2Y2LK4hyaOJE-4_YLj1BG-42jYQyENC0FSSlCgXEgEOVU0NIIyqOhSI-chG-COjrYQstUAYQLAwTNSAgRMB4QFkITKZ7mU6X1MskaZhcNmHSacCuswO0iL3CcnxTaa0DDPImyyYzLekha4n7AzsGynXjyGqLGOIYU3SPN4bChJ5xkXVHtCRV4qThbhkeqA0oGqikXE7U0VbVkYQRNGDMsuTk62goIKxvwZ6WGWaR0lwQ5QVsnqdJ2mg7dXn-EXeEL_dXqSdacvmgM2XzfrRAlLRxpDYfVIJpe8gBTjbNnc6bPVfFp_pzzWHPCjWsIZURnmFBQkVCpY3f0jr3tBX2guZSUMy9fMrFbA91mtQIEJS3ISqmLEDlZ5YocswCyccOqCQweSVS3I6l3tJi7oUguyiEbXHFijudx5rsV-UogkJJSlBM5AZ0IECeevGUFRDWgEZVAWQMDmSj8-pC7V4jfGXUdaXHIS0t41Z_QWkTXMmyMhzG61BeHoW8M27Gy08sL_Vl7438rZ_1aI2-w0T6qNxmfqjHZyuplt05YJE00jPmV7lBfusoJvJe0NrPLOq1beedXK3quGyUJIbToaaT-Bph-tWJZgYTbRnpaFu9AFtHEbOuBi0tGMCvlGWG-CyX6MrKZ_Bvo9Zfl7S1lYvzdjzSqEJABkU3RAH7Bx_JGbkH1h4vdA8SXmWnR_XtH9eUXw56H5VXn-CrGNJggaR2iFVeLTY1bYAEzmVBWOruBWrjvvXEeKb5DKjoHzpkiWXafzUFCcjqCg1bT44yg0vzLhOpHJOkP2xYdtBagWj1o_xVl7oKL09hGWV0_5FHzOJIRyQhAngrK3-sJdWXV35cApRzFXiatPXcZHwdUac0Yre_nG2qSZmvN61jyt02rkDkdhVZ8Nyr5w9CyeZTgX2NYw27VFWMF7J6rY3GWpbxWAeV7V5b5H0WZqXfNHyV_3jlq-8g_-XKC1-G0ZlJhl6PeskBZTznG4mu5gWyaGLou01R49geFIbK3c_xiyZDlU9qdONf8ZKQPV-XY0sNaPMZX1yD_Xs9EOedYfax5ZGAz4Qer6zQo7B4QyolDZmgTAJJKDmT2mIJIJgItkAOxMK6hyL3SDyVurCZqnnQljPEFu09inOpmxhbRGUwy5xWRT9IZsRAZWtMfJJ7GCE7Rn4VDZSWxAfSezSK1zGUaB0jKQMs40lCc21Jf3CS3v0ObldnTHbTHJtg-Gb3uBgQUTtUQCDmLsLVW67QcXk9xDOJRmXOrBAOvlIp06pOXtqffHdSbiyn1liS2mQRxSy3ibdw41xHnjUMPW7Hx5vNd4fmzneMO97Lat7EGQF620UtVwiX3j5xYu9Ji_XI1oC56jjX3VLHYOuI9XXH_8L_NX-1f7iX81f203K7TzFQKVONWp0EfIuwoIHWRvVspEY0pssNSlJnm5kfpeENkXmXXBaolvE5rdpXkgG5UaTsH7JDy9LRRHaQjhwsrgUAx5ZChftbqbb-Cc3YW9z1sc3X3JV40mFPnCUZJM-No5Sk77Kouvz9lOsvAlpOaQ8FW0yRVRR0NYS2pYyZ0atayovexXx2tdVUehbl-5MnEYjmgHN2tzPzKW6H7BHZ8TlAWfKVKg7R7Bdw2F5qZ1HHAe9m7NXsVgn1uGJMDznCIB1OYn5aTojZYc4oddNVYfzN39qtn93aEL8KGyfGqnA-GT56cDL3OqtlP9y9lALBBYAX8XmA95wdFALEEuHLirqMvBsrYESBE8nfIcJ6v3ycnrc32OcfJiOQRazL8sBiffsBg5vPbcllXL00R2eHE6LYpF6yhq7_sXbkdyvbfn_pgoFI-ONPg-Z3of7tEuKfBr8diLl9iPuV7qqVg-rOVLO8J768Y3R_nm-F87WBAcXvHOT2_kQqCPogQnKOCTXT_9bifzbSENRLq0QjxBtj1cgQteQ4PDFVM1q0Tg5H6r9PhwQHoZOa3BB4Tl_fMqnmcl5_M-rLGMeFNL-aBNtau9eU4NTlVNGEVoDJu0oSGtoEJAKPadI6UkT2Zo2tJJfZPoEFkpEKFixk2e56WBWvLr1MajfqBQE1rqZ8ft1FgmE6l1C33hbjrLrVwPqCY_p2mq_-QYSXBwiAcxpVIBQz6mAuWc5j6E_ix6wmIu2MVjEj8Mf7GX53rlyEqDzCEL1NtXbMDbV2zA25kN0DNWK1jcVd6-4xHennaft9ru4y1iO51zE98z8DRuMpMgGGBG-nZwLtFPzRtBL3WGVeC7q2GBCK_eGpMIzOHR8whmacjPirid0vdh8nqrDLApDKmtXTrhGSBzAO7rq0DzzluUUbiZvb8vuoj1IyRiGqnsvIUlArTm-xnZVnrlJL4kKEsJ8o9mInOrcO3pVRzaRJtOwUEBs0X0gPtxWzLZQbj0u7UwtJuk_gzfd2i66OMYQ8d2-1Zov9Ej3tERhpo1-r57WfrqRQCuEGwLtVhvP_xezHYNGLTcQuSTroML0oou6yC4exrdZh1MZed1IBFYRiPy0fAcRDvbvTmXPH0uKrtFRbr1CGo3VQnlrtVMkNQh1otk8RTZUrJCtXCqdgJ7twdHDmaD4fTBcNPMs3K_q1rud-WDCCbR1RxUB55lUskypstkDHoz4NhgA6nqgG92gf4sMPAX35ev37I6gAgN8Jnexn3vwUA7tGxRODpNwB0CWzB2XW9WPuCINGAsiRDT8WhEi4JKGgurg9Je9dhOb1UMUWpFp7W51yhcdosCi5UGxZcXYzkqU_-K3tQidV06gmGbQIsgDy4L8fPcxnCXvyk8rDKldp9Sec52__M_V_v3__5_-f_-i_u7_-cv__wPf0PJRNfuWDLhc9-qtiPzv__7__rfaNFkn6LZfWq-ZfdS1P76x3_3d__0T__v3_7TP_6nv_uf_uM__M3fPKvR4tKAc9XGfspN81IdCQH_mIOPIR_p0C9xhA6PB3iR4X17xH4Rr3yvObTAb64-h0ZIKCi2Ust6nl6_wNWZ2pNbVMt8bjVCxky_h86jZIUSaa0WJe6gBNc7UG0o9dTdd9pQLKRbjI2xdj48cWoF7IDWit-VDoMl24xvimT13TmKrbs6zQdVVth05XQGmPsFt7UCu3BkUSW7X3TJHtYULAdxwTTunOui7nZQ6soxjtxzw-UQ40tXsznA_WFVkQo4O85cZnJGD2p47u9RzAp6rguk69OtlKBot0Kron0VUBTtHE-K9lR2VrAkAsvwbRZ7NmOxG9g72cKjfSV_6k2IyHuwfgWUplokCUw0rLngrGEh_NrWWHKGBKiSK1cdnLdgbxi4uwzaK6VPgi64DGwdDoPO-8GIaMN6Wxr3vm90APKcpfSlfCI5HFkEUuDjzjX_ojcHR4MLsEVAbMofm3G3PVfximiuodFLgjoy8o422SVgXbhvi1O3dEsFZhCwbgq6CSnIEpD0Nh2A2L5C-2cmOJs_nermxb5BX0KCvV2EhwVwCxdMH5v3dis09jbaXQVS3GrkZhfu_YugYlyVfDKuprLz3pcI0JrdxsguXjCyp49mIkP3SzH105Qi__HO8qdNlq3TstvpDiqXmfqUv_alGeXdwOxbqlwNenb2fXtk5lUtO4vUBMZHSdT4wH1TPN9eZAaWPdZx-x4jMbi-xzyMoQXk0Pft47zrrFNA8OT1N-QLAhrcX1qoFloUJZHDRS0z0Q0uKpEgRCrm3BG-UO3qBwGI9ohjqHmG9LyNXkUXYIZVhJBT3oZABwaCQdwS2Kw9F0kPflZ0eZ5QwhlFibLS0fjeA4h9XJhXDRogc0C0vKcwiuyzFSYppSfB1DnzgWtvZ3fMhEJEc4fEDgFlYSY7KwsdXqBaLvMR7O0eHBgY3SM9idoZYV9VRZHmcF0Rr_tXM3FidOuw7p-8TITK1kKsJ4U_Gu0FxGiaM5R-WV4-BE00mjqcOsxRAWUmlyu1QqEeEQNp-NXSAguBla5mKSS-IoQwyTxDWOGVVdqZOjV1-PoqHY35raThh7IEbb5bYcEiyR36tZOIVuL6bbrkgCLMNzIpCAKFTBEkZRBHQVAGl1KTnjeKSnoesjdYuivfwzUhABIHgDJ1fQ-oii3KVDQ5gSI2lKn5o5nIDFF_tdL2YSDq-vXd9YVLvKodDNlIjjhUPGER2HuRRYCvFXHMHO-AcAkOlKEDTcH9JCNZx396OAXhZoaDL5dUjxDkJDauXKcNrC0z17Fu4TrAOSy5F5v5bRAicoxRat38Vn96DGEi07CC8xRsiGhNsyFSFEM32B7qzEJIXCGVjJIQ7rmpyRZJvG9031okc9nJIhEJED0h2uqaj7WaTzA8koDt5ihDXnfIrQ0ApOkJC9lJTxglI1TLFIEBLlvwrUYQMOgJF9jC4RrzebyPVIGGUXuSAKhaBGIeZ1ZTA3B-dfhBvYcKdhoA1LFHKRpKeZTa6NrxCprq3b8tq4ABDhq9QAW63gGD-y3KNiBt1XrE8S9w0gucesahAl3FkXIsZ5yysSKi-86KiHYaDab7k5bjngcx5JqmagijwRna68BYlQ5XdGAO15-YhQp0bzdQ2D3zDp3YRFCihze9pwhKc6YCVAhEIKQOjRi1Q0_Uvo126Ou8UCQFrWH5oWXJCIKK1xsq67S0wScN1FK0eFBUG5qn-ZBYVGgkSHrvtUb2bgjqPkp8WprZCH5jB3jBDuAwz3Rb9GhdnYGnGl36VPveowVKl_fp82iG1n1MUnxnMCo2NFSfPz7Z12461DG9EQP7HQxG3ayC4g71-_fEcdh1-P69cKxfhyuPUPP6N3DlleLOYTyaKRsrHFqx5TKtIXuUpKGgLj9aO2npfRhlJXwph3p1cLOmHbRwQAyqzx9Aopk3l5rNvLBz53dJF3rOwlf-Mig3jD3Ux0Pa6-Nh0cdRtw6atz-Gk7d_Ljvr1hIBWrPbGKjBCwbq9NFMZAhvDSTLhFtRQc9NIaVW9hg2llIQLCUOm9gq6pXBLmp2yw15hvaJOtryKe399IxoT16Iq-bv_4d__O9_-ae__b_-8p__j_-VsICgaNgdrmjwnUWE8oJFBFVNb4cpYwR_I9mLUXPBx7i64GGXxqzu0rimpOIunUvNu7QXVZwxvAuLPcAAJ3tgpQITQQGrwhw3XvdIKQs-oRhkNhGLzCZw18Wqb3mnYpA9HcNmT8co7Onpo5nIPEoEzsbzolipH4-GuCWBpTFgyrqPivLe1308Kd9R1awx8hRTOxlm6LJPqjs6Cbm5AFnc0h1SF9cRrtde-bJgOeS0YgU6MIbJmQFZ12wD7-JNgLKLN2Elu5WfNDdp0tykKWmAygG4lK8qFsFoJMhkdqYndZSd9G4yKYX2GuB9vabpqQs0SUVCIUkqM_jP8CfNBWd_Uof7XnKGJKiSu5I6uG7B3jEw-okTcZdTP3Gqop8Yd2o6KOHpoIQnRQnvOz2hli1LuMgbvxODrSlbV1RrjWyPtyWTVx0VIYJPGSCrBxlYUKMmxa1yZBCprKTJgE6V3bvSsoealmb2at64p7OUEQwWfCZpzZLLAdFIhvXGgs-YZr1-0yNJeb6rXUFwAgKd334CjkLENzW6DwENiczP_742JLLjXG8AogaYTA_kk7lsRH6ugsifPpqJDOETWOdirbdMmYvSkynzp2e-YPNt_tyaypwjY1sqCkcu9uMf3ClHBpAhTSZmnz53bbV-wnMWb2R3V8rDBTzMliGcUcCCZJnsaQtv9Z90aNMXwOy9mbLqAB3ZR4FTYQJnKevpMNyPRUrzBfZQyDkkbnIVIakY2cIopdYmcoYiJCOjyVBoLgg3GQrVxSWQaE3MBGdrolPd5PkN-nqeH8yKkN0LkyJmAEKpNbsXCi0ZgJcVsM0AvAnK2b-xnI7ZzWVnn4BEgGz4Ekrn1viUnCc2ACY72k-6lh0crSCSvygqfIfrDvhECGzdWmXjgofVWTHITLZVNeqOq14HJR20prXDYqlGW0bVqxAtsRzqmZde4h1dLA4OOVkcAh2YMQZZbYVKwvWaPdFwtlZJTdMOXk2ONqjCLmtjKgGUrHsY0SXr_vou5r-aCThsjd6z2daopkIeWv24wtPbnNFQqK0xgYmtMRecbY0Oj73kDClQJTcmEOztHhwYGCVq7Z5zbmvUx9aA42GrrVEVh3s3Ayp63HdmQFUd5rBbL4tD0eST0TzUyTxKGjtsCtIKQE4HTacrk0hwklGD6kaQDfo7Dzdgfa2Ywjit-icM0-LTSsY3YZatKMxugrKDO5lwcHDPZSdhJhKg9M2asc4_phRop8PzwHf-xaJT0KSUPB2GgfqMUTEynbYPql7SOVJwiXMOOwoxyt6OAjQWCcVvXH8iVhAihGEF9Xa5qehQOHGxCm3mpee0R7eUlqrk8o7DgggrrDl06MgBVTE4The4OEwMQYqLU07iBUWU6FjYHLcYVR52br65bK6XHRHtqmPhItN0LFxnB7jOcaECXUlDjqkHFmGiJufAnx5fxNl3MxwCjKQ8Qmp7sD1ZYye-du1WV36Z-0au58qnywDzz40n5vr_ksfE7EpWi4Akqx1CSHY5hBBhJi9Z_aHqD2WXdsqTHwAlaAAtWAz8i5Ju4N9N-DUb-PNHM5EhjA_qZDwJOR00VOZ0S0F5b3XmtxBbMGEbN0SZ41g7tjgzcREichormKpTbSKzARhjNpBQkXoCYC6fqzt6PsU17KblAhr_Kfn9qacKxT3P9K249FoGcDJ0PCmXsPvjBjghOnyfaoXr-1B-U79iCXL4eqNv51I2qAiJUKgbUxLZgcTIcKLJy3o827mQVkzXQ6xw5kYkvMWplmdyquWZ3Gp5ot0HxR6HDr498yynqkDbHVRFhMKtIe1VG4UymnAI3iVFIs7OWgScnbUIKJK1GC1HgA6yzySuhiq7M8SO4yq74_nmqzrey0rqOK8g8Ao8B53LW7284tdCSQVINtJv7ADbH_Q3wJoUPBE4fhJE8lHATibrhk9moov5a0PFKWY_Tr4AmMx-tGAcXoTkZH_cXZXsj0vOHfxxc9nZhJEINPEBk-QMxP_8J7jXeX849F7mkRyuM-GxjxR4leGdSffNvDHegcrO5wcNlXg5apyu52pKx05gA6x8HBVOx1M9CbP16lH35OMKAteBf1RkdmsX7uUGEr0KvixeBeRDM8GZDXWqO9dBp6_7wKG_0ib3a1Ze31veKZvOR3nT-Qqbzih-Ax_1w-jJJ8G0nz6aicwyztIFWAOkZCa15eKzbXF3mz7O9rhQE8RGQyH-vRn8p_v3loKTf2_AQy85QzJUyRx4HRzMHuwZuGuyd2bwo3ReLClOp-plVfYyXKnS7L15Ym5tynkWMqB2vuejg8DbtUzqi4RnJNCH3z1adyRTA6G6a4r9k-Lsrvbca2Yfpezotl79T8Jhfh6ovJZhhIsK3MclOM-_BiolvDVQKWEJgcrkQyY92nTmwRtdkaOUyN9hV-t8uSHs_K6hqdgS-wxBB-UFhEpq6Lc1UiW1678zmOu_K9QSDVcu2xUZApa00jC9p77B2SrAAS0fTbsdRFCZCGSog8C5Q1Aha3ong0hyIGhRUmiBLCFaqbxKCOivALAcgJLh-g4e51kdC-bWpspGHZvKzuqYRODPcM0FX54b4y6t4hONa_QtddLhAo5GXdvR66C0gNAbEWuXS_Lt2oDhhNvS0H_SMNr1orxUEe9Yc7zUqWrpojZYGHG1fxDiVUjaQ6QFGFXbJa4WVDdBIh4f-tIEaTSFlduaIQGSvKQjnPLOkrITn3PhhSs700czkaHKSqz18zgE04VtqEREZWTCIMrIXHZWRjq89pITxDuolWsbHRz34MLAXXgKffJEakbvf6FBT4KRuJZDV8d8Y-2pqbaukM2Y2rt60mZMbmyJKu00oCcgIIV42naUBA_KNIi02ziGyAoYhsRO4JJ96GOhAZAE4cvlk2NmCRFJCR82WPZjKtruTkXYqbB_AGaWDZTwKIxZhEJ6jsFYu5EKc-lZLIgkllEY9gRJEzRsfQ2FBCRKumPtN6jkT25qov0YcoAqtTflqyQu8pophvZn7keLVysz96PJKsxvYEaHSTlfAJJyvpCi5HdbOjDbsqMXO4t39OcFlnmDpXj6ONI-F230e2OHZ1X_yquWxSDSpsprTl7fVEDPrOIqywlsKUd4ltgqHrOroOIxy-nkMZvKzntRIkBrtqFv5VWUZpdwI1OS01czU_rTU16vrtdmVwbuqMvJgNUev3HUgWmcPdwYYcwhnpQtxJNc_CqeBDZMpmGX5bvuKczD5tgieN1WxAW9ceIBSp2ceOikG2CEwHG9a0PBvV7BXXZxG4wIFrX99S_-H__rv_yf_2j_xf_Dv_7P__Ff_rUpEm3uC74LRfhqUVMFAV0G6QGboqUKQlXSXi6qKltU9bfoqYKjo7hjusnMIYdUQYkOLAEGcYtdW5aLgTScbfCnoMGg3mOBM_G1Pl-UFEOsUwBosYYyxRqincYABo99J4eWcHW2nojR8CLfjbCUlqPitDTTLsfgaQTl_JtSteZgHk7p10ZsU5gKBoTWb3oKEyDQFCY0FYp9nlDgSZiZtmhJxelw72AspNxFXC8qTyyH8Eo5hFeKEl7hcJ1pl3SAH8JDxbyBb4QCWe67Fp4RVrFSV6azbeQGoSpt6FEswPD7MBay8l0cC5n6FidtcGCzVTxjRoRM1QNVVQhUIahooqmqgaC6BoIQItpuibd5EUAcchJAAh2YSQZZJQeAtw5awNkKqYpG7Qlll88OwyuIjho1QBFlSqU2NmHIdU77JLKjknxTcOhkN0FWFl_7YTcVJoqGyjNFiUioPUN0m5pZ5zRUBUFPzaw9c1RMzWSdmN4rAZdS7bEQ7lKqpfTOsSMzuCiVGEqHK9mWlS3qE1xn0jUe4O4NfMcegR2YPXts7ewhfAZqVbiNZVDJMzxSwuiAIwQYXIbX2QXel43qP8lG9Wdko_ozslH9GQvBySUwqG78BoO-7jeATgnMGUZCdKwBbGU6QG5xAWQTyRm2P93-vvFlyz-bk-U_l50sf5EArdnqCQPZuDVhYP5oJjL3MIB5fzUN0zjr5x7zrX3_pxv214y1oLm7ximOcnD_e4FmBLguslAcxab_04O5V6P84wR_nsYoDhXcNew7MKP95OLUwC_EsLOpLRfApvopjbIUw5bw1hi2hEVj2MBpcA2qjAYQ5Gwi3NNWsN1TWyB2WI5w1psBJsdmam2_sx0KivvpuS3bogdQ3rf3f7yHb-1pqNpGFSpChfxO2WW1NJ3vdgbH5bO_xmkSkKz255G1mNu3DAVzb5HLiVd-j5Sl3a9jXKZYAMNoWtFvl-VmZPsLL_Ts38pzxtq3bw5oVdzi_WEzbHXLYcqDZdnVoYEQzQmCsylBBGeLBdC1Mbp8ulrmK1ml8XYi_Sf3d-4v__S3_-Ev__Tv2itgJtHJ_9bZAI1ZDiHm-yEzlY3dz2MvbGz6aCYyrdKC3byZUUof0wUYzVpaEUjOEgf-6UHCqdAUIkRo7aXY9_vlw6cqFv9DYNwBCwMiq7NwTBGZ4j67Z6B3zqhk94AZM-3hrm44XMPdVlQgiUIKhRC_ZosOZUfNPNBM3BpSQ_WDPhTOPokaySDElQw32y6SJtLpilnLwL7cNbb-1xIyoVveGb9wDWfazWsjjpjdUELjrzJ_b68qlXXDO6KzT5D18kHcgC4OZkfyojO-ck62o8vtyZXnSaVJ3bnfuWlqrZc0HkAyiSANeEqZ9tvlz9PFPLdG4nJutdv74nWaDT5BDja4RAeE-IBsbllGrN3Nx5m-8n7A2d18jDjKzccUx8t3XnGc3dVZiCP5BrKt85QSdr0uOmDu8hJzuyVGtHRXlZsmkMKy_v6gX4GMLXwHWwx7oERpCBgh7Xt_Zploa143zvDdbOZPsJTSE7MB0x2_mfUbd4AwkOStyMJL27Cz8B108mU1-LqA95pJNT-7jYzHW8108uZkOk1lZ9NJIkBrDhudw0dB55g-monMMpxta4lxAcQI-7gAouW4fAsjXWHy-yCCWY4291kQ3GO49oQ289J-8gK4pbRQpZcXZMCGEhhaiHcq-JM5fb9JeEnMN6nX0REz9DJj4ZXc9MnBT6nXkT5ImO_XjVpl_nM_i_si9boRQFHin-Qr3-9LfWCZTIcrSsDl6qgYcIkwRjW3wzCXTVbz0-1AVfH2zvN7VRyGPnh9JQQ_5pIdOUWIuAJGqXWig9cXAbTEiDDD1iTZPwBB1VvbP8Goe4Ui6EE2pCCkyZDz7RdW-YVuduoQtbSLjxRgX_itvwQSnQqJKqRokGRVSFAhWYNko0K8CkkqRB2Doo5BUcegqGNQ1TGo6hhUdQya6qeA1FGwRh0Ga9RxsFYdCGvVkbBWHQrr1LGwTh0M6_TR8PpoeH00vD4aXh-NoI9G0Ecj6KMR9dGI-mhEfTSSPhpJ73LSu5z1Lme9y1nvciKT8v8DVKUiYwPFBwA=:5:2:6:2073761:8:10:9:30:10:108975:12:0:13:20:14:11730:17:1:43:0:25::18:10:19:11994:42:0:45:0:15:3:30:0:31:0:28:9 years:29:9 years:35:444085:36:29_566_73_98_29_29_54_424_29_29_237_132_177_29_29_29:37:3:38:1:39:10:46:48300:47:98730:40:1:57:14400:27:0:52:1234, 3456:53:5678, 9348 \ No newline at end of file diff --git a/tests/artifacts/level/897837/processed b/tests/artifacts/level/897837/processed index 2d6e6e3..3ba53a2 100644 --- a/tests/artifacts/level/897837/processed +++ b/tests/artifacts/level/897837/processed @@ -20,10 +20,10 @@ "coin_amount": 0, "coins_verified": false, "stars_requested": null, - "is_epic": false, + "epic": "None", "object_amount": 7092, - "index_46": "113", - "index_47": "0", + "editor_time": 113, + "editor_time_copies": 0, "level_data": { "level_data": { "meta": { @@ -63969,10 +63969,10 @@ "password": 3101, "time_since_upload": "10 years", "time_since_update": "4 years", - "index_36": "0_167_67_0_0_0_0_207_0_0_89_88_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0", - "index_40": "0", - "index_52": "", - "index_53": "", - "index_57": "0" + "extra_string": "0_167_67_0_0_0_0_207_0_0_89_88_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0", + "low_detail_mode": false, + "song_ids": "1234, 3456", + "sfx_ids": "5678, 9348", + "verification_time": 0 } } \ No newline at end of file diff --git a/tests/artifacts/level/897837/raw b/tests/artifacts/level/897837/raw index da5986f..c1f3502 100644 --- a/tests/artifacts/level/897837/raw +++ b/tests/artifacts/level/897837/raw @@ -1 +1 @@ -1:897837:2:time pressure:3:Rml4ZWQgdGhlIGJ1ZyBhdCA5MSUgMTUvMDkvMjAyMA==:4:H4sIAAAAAAAAC6R9aXLtuM7khqpfiOIc369eQy-gFtBb6MW3LQJiAgn5-chREdd1kjOJIUlR0P_9P3n8k_4tx7_nv-ms_-Z_z1r_TWn9OdefBbZ_03Ec__Z_07-pfv8z_j3-Hf-m_5f-vUof538tnd6WLv_-r7SqmGEV-Svb9z9f_5Uf2j___S4XVdDLdyfKNQF5_FBFeRzCebV_fk3lU-nH1r-LH1-FH4ueb4rJpOWw7HeGr1rrfy3-Neb6Y8e_8_5q6dpDNf_83_-d8j_H95-6_rT1p_zz9e_6_74Q-TO-__yfPK9f5_XvquBK-N_l-nelpmP9Sf8c_5P-yfWf85_81Uz-5_z6eZby_bt8_y5fv1v6_tn1Z6rjTk75P_WC-p1FoPM871zneWUah0Nq8VnyeffkC27_zONCO6Dfdc_799Vm-yf985XlHP989ews_8zr_7_H-J9rcF8lvvr1paZ32TW24y5bjq-iX5r-XbTWVTT7onn3H5q9Sh7nY8l0l0yu0f_e4aSzf5Xai1Gy_dn22vQ7t1mbVcJCDaBVz3T1TLOg36tVvubvu2trQdtu7Ou3-7kkYP-uh_mZSnL1SYHpRQaR5oElHNLTYwvRjfxTVm_mNQvptcyUlt7KzCr6ocykuZc2XeOa8x7XvKvO6bVQ1dNIUa129VeyEZmVxULDS9FCyr3E8nveMgTAWj2pA1bvRr5X78sO_K9lCObutkzJuHvVtP2W7DjWb-xhu03bWcb9O9W7obPMna3ufH0Dl6w2Mm-I1OyAc9X6XgZbfy2Dq-iHMniuMfV7wtf03a5AZiFtlc_udzG_LxOwf66OlVcSvFakH26JevNABmDeQJq42jvj10LFeH3AR4zn9IAXwhv050uIdsLkhJruEnmNaSHzAUke-RqHR0Z1TW4f2_9gL_t7e9nf2MslqzI3a9yAyCgRqYScgExCjlsLBMgeaB6YVnG69eLdevHe7M_bGZ-XkZTmWvVIJuTwSL8MFIjhKmSQY_uY1c-5f2f3u7nf1if1dz4pjW41-uITqOGXdQGgHi7DNZ5RHxRqnEHCd__HeCoxtk2YBNcNXz0ReJDCroZR9RaC6il5BuUBpV4jM8gABAZJ04HAsEo-6nslH-O1kq-ir5Rc5gqUcw0yQCohwyNoLGQ-EdlsAwtRma3lw9KqUe3Pmx0tBZXSoNaCZEIOj4BaYzWLJg2kScl2XIDd8bUi9Z0nvno0k3efq2M3fsufIF-N10USLkleqEjyqrH4GqV8ceUX2h266ujUq8uqzPk91PO4zQwCi9UYJKeNgBqvrqBCrgZRISfsW6H_CGxa2fZUtnwx2OMGlB93hywplPkDuZTOIdIJSU6YZ_GAI3_zZtdtz8D-uU8JLs-wfs9b5KA9AaC9azK-D1aChVQYpeGacEnAGU9HjiQkHS2GlRg5OdOE0-8LJOHbX6gEH5AfGaH0namlJDQST9PVs8I4s5c1yWWhybPhHaRUZRBPm6WiLZRfQL6l8tqjCfK9STMHNRtXL_GFvHMx44AVBqHUyTAQMT-dDAN5FidVcU1c0VYCndP9O7vfzf12ZE5rBLOvUGboIAgsv6lqaZ10bwKQPdA8YDmcLNnLbVlKN9UyqpZuava9rbdJI0haMp3QcQs0nqBaqWClXEi8JJeFiHpprsJ1bSb1Ve41HZKyrw6JdPLatpnQZxWyE6CMpgirwIRxwFygOiTkqgaqDJ0IEU3TZTupYKJyiYqBRibLuqQI_N68C9eYl5hXuFSYaU-isCeKQP-XcJ_pUbhXkpHRM8H0ImRk9ETCJM2U52YKN9MZKtxyf4BsZ8oTVLjgglKDmdmSK3AJBVpys_RKfpNw7ia_E2BKMvRGtGNJ1YlkVaDCUGfI8yjt0RbQ0xInyQC_96marGZ3NhRbZ0koNPc89QV8xOmoGY5BERjD5Xzy8W2jUu9-o_yVEhr8nGO4efjqUr55FbmHDFwMOVoG8gZbccXNXnzNXz5o9XJmiE6EFKoMDYKMucxMQzLRkEw0JBMNyZuGmGlHG5CJ22XidhmO-RaryExHMtORzHQkEx3BTBbK21EuQXp35vA1xPdeNvsTzV-dOiwdEslBg5kPEACEjCnMuEURiNl8zlwwc_WZO8HbgAzbgAZ92OYmO8aaHWPN-1ngsqQismBihe7fCdu2ZM8_s-ef2fPP7PjnkpC3_LMoQ_nqrLFTRbmkNS9lOHgpXGHuU-jMSApbiChMIQpTNnEwvUZNLf7wTkotQ36dXKTS8627jKzDDAst_7cgMXEGuoTrejhvyy3ZKkyOC5PjMrwEFsfQimNoZT83XCKxMoCHKkS1ClGt6k-X1mRXJFCSUymR3_fXh4MnqaRQHQaBZ4TLFdWyZdEw60oHNFI6gIyPqcjccHymnD_JkWJoX2qidatM26o505pQOT1E0zY8aalImRbFW1V0RPpGls2peMyVocsdkSKZPCmUvkBCgyq24FXHxKo7xBIABLESVap0jtXINTdyzY1cc5sx8m13zYw2YhCNGEQjBtGAQcxdzUtHLAN65YhX2XeOuLE_bex1G_vmxh680WNRhUzB9gDZFtmDN_bzjd11a9wJdv2NXX-jB78KLeFcarQKzuSFk3h5i-m6yNHajW0CPiENd14VhW-S2WtMwBsT8MYEvNEzWZXnyRAeesho912gDNLQt7NbRed9QUZ1eP92pKk50tQ2aZqgXmA5QEkVaYRsJc0wY2-5UCdW0YlV9NMZGczI8HDwmnmpBDfUvTI0GCK21IEtLbHueJokUH2ARB6woIXGA2Tr4gPFzseOnZ7bKlS5qwE0QE9X9ZNmuvD0baHtxQltdzyrO57VPc9aBYCLd0-8OlHVkULJGCWGewwTnxrEp0b3MzCI6gyiOqMTogdNVKxxOaS7I5GwjETCMvgYb_Ax3uDztcEHc6Nw9Z07wQdzg0_0Rue6yq5rHess5Hq0s2Rq5KkyJcC4gQaD20I1HIcawKGSWzVFCiF71UQjJnmueYSiNHMMtxj2nArrRumbRNkmUbYJlG1J0SRuNombTeJmcxJykIDOzFBjCH3gWvV5kCBMZkOTOdM8SPRmZqgxNB8g2wl-0jnzE1QZGlzX4BYDqHLBulVCRp3gpFMS2lNCvpVIgOm0anolmo5eTEcvpqMX09OLCccqyUmVIpMQkM5Lgs8j9P7nEXr_8_DeHysp1Zc3iHdn2jQi_qBEG-RSjYs1LgdaoAUnQkQZtCBDKJNS0EJEUhSqXLBy9QE0uODg6sctuTpEElAdVZBQb0FdZleybvt_HtmKrq72_l3db0cypMAEwJEMs8xLXFJIMs4UkowzhSRDKkEpTJ5kSFGUy-RJhraKiCcZWqpxscblUC4TkQwtaCBiAQIZiUtEH6T6CKpcsHL1ATS44ODqQS7TvtW0ZCo5knEmZx916fbv4n47koGrpognGbhqq2Mnm3oRizPkGucZco3zDLnGeYZcQ-pGITw915AaDeK5hvYREc81tGeIeK6h9TSuqHFNjatCaT6JMghkJPAk-qEQCtJJbl6hytBJkO0EkRTtPUO2LmIRUlcEVS5YORe87yZdBU04vSaclimorOzfzf12TEEygPk95waSEyRFQJCW-OWYKOSYKGRiBBkYwZKeTP4_k__P7O0zevs1r5l9b2bfm2m7LnUFBY1UZKYJmf14Bg-9ljJvK3cdmp75eqKCJ671--jz-MVNuDMTE8rIEkymxtDkgidDiwXDPGyxys7LZ-_lFwBSBeuryF7ffOwJwdOt_zIh11wel-bk8l32S1u-X5W9FGo3MZOiq51yTDvxKX01U81hdaF25i4KXQwOqwut2VLugh76OngUSJbDQI2hzNBBEDw0EaT4c9ezEKUpRGkKUZqSSMBKYQhIwXrob0ZkoWMLWHEevjgPX7r7vXozm1gaLEVot6gs5eyfq59I92oHjGjpHkgKiMxdrX0o2-u57lnBpym03f8TgI7wevSLeRSBTNdhPeYRAOtZklYnCV-dJKLVHL8jJNYnb8hWn73YCn7E4lyJ-FQiPpWITyXiU5FzYEUWops-OgaUeRy8ha5ZqzD7a9IaDn3_dt68Tvf7iH57-ZdReLSF6LRolnlwrur3FnMV_dBiihJVd3tE-oyAO_fAZRVVaeDVGRFFQGjJvcm0BLDVWAAbUZdG1KWRw250d1bbHFROeAR0FaWmOefcnHNuw_0-o5WXxj06LCrT3Mibw5AVGX4pOnhAhfam_AnotFqQR5Hu1w_yCGAua-adx6xxx_23gRpDmSE0iv3B_3byv538byf_29n_dva_nf0v9t9CYIt691LVnUfuziN355F76JF76JF76JF7ekGIs8zEa0q3ir4zUN3dB8GlVWQvbUbBfMUExj4ZIEA0AZAl95hlyeVCjKiOTAI9cNNvoAkcc9xvNBV4Avz1rxffQT55kE8e5JMH--TBPnmwTx7sk3FAFgIeOpxHHc4jD-eRh_PII_TAI_TAI_TAw3ngJaWDXsD_tW6M5oju73VjFX2nG8M775E9AN48ufWXrtML5L_ZES4JmOSVJx6Rp43s1ZvOc07nOecZrc0q9Wptpn8f7vdrM_17Wb_aiqa-ypYl99s1YUJHfEBjD3h6qMdcqTQJw13Fkzn0ujAd-9CFJHo1iV5N5lITuJRIHAiJLEh9cQax4mId1mHmYweXua41SgYxstcBhUIHQZ2QwsUIkeEaaJ1KYQ--rbW-ZDMu-DqaKnqrC1dY0pJP6yut3WLEeI_htfYBvl_dwPl0IiGzSmh_RsstAWbeBPHMSwoZhJiX9mJQOYFWXLL84Z5_xxZL_ijs13ZBi-YP7UJePUYJbQC1SrkOgjohhYsRokILzX13VCV0hXxLZXXMi6AkkCXTBG-xvvAUWTjBycLlVOfKH-jGSiO9kSB17e5xgJPOCB7DrDNreGRHc4r4h86qRyP-kdMMUcuLdHldXcXVAqqXPCM08iCIZ4RSjUGIEVoxwposBIxQlGzpZ_61fu73EvPpXx9BWBjyEvKzkgKdFdUMoYOgTkjhYnULxpnLas4LpCSwCq2E8lSANOK8SJlVOUhg1Tq7hPaLdGglkn6tHvR2jyXASbcWXh6y7-gEaaHXKy5lW84zuZ3g7-21rMwbey1FP7PXS8nO6CBHRdOjI0QtAdbOuFKgxqdn1UYEBfFkTDs6qJiFhtdQWZHPNHT1PIfsIYfsIYfsITs-5-OgZs8uMrGLTOwiE7vIzC4ys4tM7KKcr9lFaa_ZhRR9I60ldE07oKmMtJCjKGzyC_mOQr6jkO8o7CgKO4oCjkL66Jxdcc6uTPc7dLcldLfFudt10qOZ8_RQ_UZ-BFYhDbu78xhEMmXoWfqyxPvii0lJmHKdnWpKmS5izU74HzmBMi0iUrEL01cnonq-8dGXqFZ6WeLXClJ9KMrfK4gU_ZB-L8mrZFwrGddKxrWyca1sXOvwUu2ivubqbH8d7nfobWrobarfuGeoAlfZJNQH_Et0vJTdNUHe-lBH9XVowngoMJ4K1FhIRdI-FNK1Lo2cRiOn0chpNHYajZ1G637Nm_Npzfm01t3v0Iu20Is250XFbMkAyzZTiIhRMtAyMAv6Wh09-gHUWK8FDULOY255gUwGCTKJ0UWobtNlepphOliOZJpYjtZMBQX6jwWM3qwVplikmYKRZopGKqWMqPTsRcXFKM0uSGl2UUq1UicU3blC0yfTnPOOMnIZIFuMlcBzKN1gi3HXtEWzs5Pt3qdiHsoi0tvZ73bycF1dKUEdpL6DWzW9J68KLSBgVEOEAWZPJtsRlu6fOcgqEYMykYLXltfEpDTQfnt0bVYhVqVBCiGJEH1jHKHKkOmY7J9hVwm4HIJ-J54dNoujxSfXklBsIHLZxI4mx-B1vxabDhzPuqqGQyzXXbXLUB4Js-Z7WVwA0ewiiObhPPQIPfQIPfQIj9YzRTnNFOY0DyYcJqTqXPU7CtW-X1St3z7x-JF7ScH8y4Jw-KIxJd1RAqTJeZlZ8Hno6bPEGvILvzKUKH2s9PkPnnav-XXhMbOLj5mn87Qz9LQz9LTTedo1cTO5DeHvSasU_ZC0ThjHJ-ssJnLilvaK6Oyg4qFl5xBJCcql78j_69niPZ3JTlGvn8rWNePFhXksLsxjcWEeyzHd78g5Si2ERvtEqdGteTnS242KFv1wzeVTHuAYrpWzSHGIfPADrmYlKHSOYkbqAvgVF8CvpOF-R1ZPShHqrd4xN3zZ4j52aQSuc4-t-wpfEU7PSbhsIfaU3REv95xZqHhozRoi8uEPHUQyQ3Nh5YoLK1fO7n5HBkdKEeqpfb86Igd2R94zKZHiFssRqAPLOQZAXy5w29RlUiW9LLZOyZdTleRFrgTp0GyHZgchVzFZJultgYU-rwfAhnBKn2S-nvq0v3WBrZ3UPmYpIXBiD0-51757M3dzZixrh7O5czHhIw1UGToJwktw5Q7PSFBmCL4gcA_JCqsLQlZcELLigpCV7OxpDu1pDu1pDu1p9uduawWk4kukFToBwtYzCLlE7etT70Etock3AS8kMZKWfJED1AU7JXtOAyWCBiEVkRwjA4fSfL_mnrDqe9tAU934mikgYiq1o8qtmgcio6vUo1XNtd-6iaqpaxl1Yjkd7bRZZJ5OPy0ItBA4aSpP-CST_0rP772zFH3ByFSXPmFk4o81yqD6Y9HqjJs-A1WGToKMHbHxJRHKDB11T2V-ydxcsLnigs2V4thE8Ve8lvRLfDkry8WzhFK6sAQU23Kx63OaDYIU9map-PvWUoVsb5ODF_3o2bsHCbp3ZE9MNGqhr-iiN1H-0xOZgpvvRWQsVDy0iAwiIm8yf20vvKnJQCdBRq7KILkqg-SqmO9nANQJKVwMvJpKifVqLp5ccfHkSnUUrIYUrIYUrPor08v0SRVfEmAPM5ZFkkI9A1QAWjZSgv_B4pvi-aDiGSmfdKFAcam3UILpVskwjpU3_TiOlLdgSqunE-R68aXvepwgS1y-lqxeCzx2_gx9f2W6pegb013964W_2EzL_HUdh6EzGulRT9HlW4IQpFAEekc2lPEf7sn7b-2ti5RWXKS04iKlleaYXQuZXQuZXQuZXfOHmWsqpGLUBanVQI3UQ-LoLfWQZ6BYpdEPCfN3UJWqMgO6uOy2SPqSR0kptx6gQknnylNPRKPM4FDJpHzKpGBSPnF51DmNu-d0rl1sDXQX9U5DBjq9E3i4cUo_DyvMCudQxjUA5nRwtrC4rzsE4HZfFioeWu4LEf0i537UIlq0o_wtUXQR2IqLwFa6Ix89PMroIVPo8VHGquJbWoPF0Bh1bgF7TAr6xWaeqlqkptvE_zZs7eRq0HcMgfXGIbXa3HmLn0cRFZ2ch836SsbmZrzN7vKIGHJeyKDZg_fTJE8HETNIcUg9HCD0SMMoAlJDgm7iDBroJMiwJhOo0UCZoYOgTkjhYogQ1-pMyDrTts7krjMF7EwUO9NJjNw4YSVW6FSDdI-Ug_IkypMJaYTM-7lncfHzioufJ-le-Z9LieBKlL0Eh25DHoE_6MTYJ0qK4FneMqpDDuY8ghoiTZfkM51UDNzNKkXAuY3pjiSoOfjQb9Ch39iHfgY5CQFKKyPHI0uB-MhSR30-nBsOPP40HS_Tt2hig2PxfdEY15ukwu0RlswNiNptkO4RlO-hj0o80giZVDNoxSgPCPYnPSDdI6aHhXpYqIeFepgeEC5FbQX9ydTn_TJRGVfQh-uq0qebCCla6-ebiPUwGlr95ZNXLVh_WXCRDhS8fAdaEQMi8OVoT4_M6ZElxS4YYnHBEMsOhih2SkM0TnvKN-n0c8IJ6VK7SWewczfhzzpnBgNqjefMpoDYBKkdzzpXzUgfZrVnndpYAwbjGlsbimi0YD9X4wScNB9gYndcSZ2IiU1pj4c9z9K8enDtZu2-wVPIaM6TDnSxY6NGzFPDdjqB8cZwhlvG6baMQqxmfPIpUTSRntyBZAlqDE2CDD3BaKMWWvSkwWonQgoh3SNiwhDJhDRCwDiKFBVCukfQXM4MbSHSCOG2wFWIUBNiaj6o5oNqPqiHB43i2CO9ZKXu0JZL8gTIAFyboHxLdWsXCqEwECmEdEJkg2mg9UGdr6XZUINe2j1lxYCbAGy3VNebim_ckhZ94ZbqepEUWl2XZFMa_80xadH666IJRg_Pz9cEuWCS1QWTrBhMcu4C6z31sUu03QgGAL3uG9X1aqBB1ifTKtQ7p9S7PqGgLeObVeXbPNUd9W_dFasYtnGVvcMjQtmVoHaHEvRRDCQMSJjuKw8Daqs27drB1jte4q5QEtpT31TnqAuqnpSgNoLayFSVJBw0qQO7Cx_PqSa8I0L4IR4ZTgQVhjpC-QFalmmFDpSCFpoPkNRloMIFM1efQXaybvD9-mTdnttTurUUuUIiwBga0klPviNGW8m5Azv6xbtDGNoOrLnLYB4t1BiaCJ0MUWDEyoERKwZGNHXZXBRtsnJoycpBIysGjVSIwlRWE-kRc5mVF6gTZKQo43fNsBMWGk-5CkOdINvi4IKDC46HgkaUc2XoBGjJcHebsN_7HSn6od-5-lEaWPvLc5eR_zGHX4LM7pF0MlQZGgSd6RtaN8Y1F-p5SaE2SeL9sbNA14uGf75vC8OYztIhp94dcipeUli9JBao3qt5SaR0HIuycizKirEoRTxKIkku9MVaKWihRAJZEoltSSTchT6jptUzZKsvD5BtsT9ARitKYQjOL7JMan5LC6XoG1q4ROiVekrRT2nhJbn1TJttC7RUFnZ2CqGmCVQJWsqn0Kr-ZKgyNAgyiiy5IG6VdmJ5K4EOp6EC-3gaWkEMz1DNpVNLzQdMlkIH9AA1kONnVo6fWTF-pkhnPUgDa2aoMTQZOkjdamaoMcR8qjIRq0zXKpO6ejxARnVrZqgxxNSvMs-rjaHMEOynz1Ig1__ofsgFIa07CKnsfiS46rUvniBH-15PxYisV-A4bcUo20LcG13aHnxuFkW7bANWk9th_t6ASdE3BqxOd9z6ewMmRT80YBVGn_J_LrNQOy6EwSZgouZNCarV_lYdvNSTA5XWxkS1MZNszCQbE9XGTLIxk2zMJBuzv3Y-QLYu4MENkf3bnak0f6YisU1B5FeR_QaK1DkBuN4M32_naask8BRnNss6vH2aoEXfiHfrr49tpOin4t333FhR1si3BhuIiRz36UyLrB-dJrQRSzzvsBpvgRrvZBrvZBrvZBrsZNJqMKWbNK8OLl7crhsY576GsnNuZPH8tY247hZUee1zB3OtLphrdcFca-9OwDUe7BbwXpyA93NaAe_3OZlMu8bg9QLOgXEz9gotffLjWaK1an6lC1L0M10QadO4vyiBEuDVYp2Nbk9O1gpMK8O04YJxG1vMQYBr541PB7pvoYMgI7-dtzSdN1GddzmdNyudtzSdNz6dtzSdd0ydt22dt20d9kIVZnA9jhfkCgO23rutMH_4URHtOqjUjhQrh7Q7VKxozLhoM2jMDokqAj-IFmm0XvOBWm2LKJAJ_1tWm1tVRHsQWtpDT4h_rz30jPj3nmT145UnkaKvPImEIlb9nDDvop8173x6RHdCUfcVZCkNurmkZtw7MHeAwUGMqwliXGGNjYwP3iEM3kcMpPAV5Mrm4q3F4F3Q4B3C4H3XgN0Zjl0Pm9YsxVvMQVvMA3KbOeJt4eBN2uBN2uA94OCd4uCN1eCt3OBd1OC91uAd2cCd4hr61KPi4GR8niBqcVpzaRPSMp2cz5OkUWrTU3O7LnM4eGA1uC4cWVehytBJkDljn3xeP_Gy35rLiYfzCJkD9YmH8wYaBBlJmLwpmbybmbybmbx1mUwKJ29KJm9dJm9wJp_XT97NTCaYk2noHaOaCsJR-Wyvub4U_cxCLzPbMCrv5e8aRjzOkKdjnrIQRyLb0R2-fKWpARHUhLkr_oblph-UUGJgLL-WaNVc1hkHNAISrtBEqDDUvdBLQRQuKWghIksKJS7IEGpLM6GPDVQZOrmrQfUnQ5W6GkGDofPmNG1FWHjDabToZ5xGWvW74w9a9bvjXzOpr2l7e2auRT_T0waLVfZvd09CMkwA9ucxLi52qxbslVTOkYs1DBa8bITEHwaLsJCuTFm0XNGts1JXs_6u3XGtjb_T3KiIkhMV0QQhRsgIfDpIlO_w1pTr5FwMGaVORBg1V-K6dlDVlupb9q9F38hs8jv238ts8jv2X_mW1ep8rSlS9ENN6Xtlri_cqrS1WNoaixZRXIUSQVaOGsskXjmUjsy4I_T105aIfgtkO0LfNVXo5IL7bl1z8cObix_eEjxkmLsAGJfkvh6iYwDTouHs0dwkPiPXkgtbq3-erw27FH2jJOd4e1dBi36oJEso7hDlVihMMPO1kCfdY1GoMjQIymjST7fjkHUV-H4ZTTjWed8EsrtrTTC9pM2BQpWhQVCGL92284znhj4MK5Bt4uSO0GZDocQFd3zE5iJ1Nxepu53wiGLuDKAfELpbEXivfWnCycfuKgYWw2P3JYD5fHt9QIt-qDFLiiSg9uGkaMFjC1GGzHBM0XJy6ytwcfCSwtwfpDAzp8_M6TNz-sxEOTPpzky6M5PuzAw7M8POTPMzbwYybxky8_DMbD0DNW_Q0y2qLpB6y55IZrhwC6sJsgyB1UUiMx_Nay6LWbp5yVDR5wtWhgTer8WWGsEiMSXmmcV7_iVIJT8IUmECWjJDTBwKs83CNLUwTS1MQAsT0MLMtTAJKUxCTNB1hGwnmOOU5gVpx4pvUA_8diSieBJRDidYxZMICCyvCF61HoDAkkJGO2nMjgrTqkJXjLWJCYJb7Dl2R8E1WEBwirkEsEbuL0v8nmoUf1niN1RDil6-5JWTkKKfOYklBy7SeXORzlv1vrNWJycrB8gJhmpfUlE9URDYcyuBRyhDlQlDZcpVmd9UJjOVWZgJFW-gynVVrqtSQaOxlY5IpS4j25VuX-skGQhoXSoLktBhY4e_EPmuAW2pAW2pSFuWWNX2mnJL0TdnKTvuusgThIFXpHgJazFVaZ6qCExnldCQkZ3GtKExR2jMShqTi8ZEpTHDaYWkojGBakygWn-A8NlDw7j3BrItAlFZj0pbw0gH1wNVA6XRoasroj0hK-KDgerhkQYzU7Z8toDENCQx61Fz22H61wNNnRfk4-14zcel6JsdbPO7gN-rU_O7gN-7lVZf79al6IdjXU8K-k356ElBx0fH7hGcJMZX7TXR34U3ieOHxOwCYK8jKY1-36Wb-aKy_XoD7byquKTdRRxvLuJ4G95TDu8px-0p1_yO4-31SC36RiBGfntXTot-eD_o-jRAG2qKvy1D9cgyA4iseDEWWk-xBtpkA1WGBkE5MdQJKgfnWh-XvoRhmp4SJJngYP70iGTR779BHoCWZLl46c3FS287Xrr0d8qH4zxSCMG5mPpwj6CDoE5I4WKI7KcWIg_zYspn3vKAiKy-gaSmBU2C8slQZWhHSze1i4pOv72e_sHN7FaJp_clv1fi6X3J75V4-jeGf6_E078x_HsPNl-_L6ZFPzyDPWENVNkm682MdKsfcB906ZaBLt3pLnp9d9Hru4te33f0-iVWWiUYKIUyQZ0zHQwRUrbadHwsfKmNQS65NshSJAtJ3WQNu3libqBOUIFI1jKLE4DsgeaBqUBenX79ipUW_VCiteyWCBGbwmKDkE4CL3vhZS-07IWXvdyLrJ16bG8T8o4Pglc3EbnIcN_fCxDxSd4LG2SJTyIvbKHVoUReWKHK0CDIyF0il6u5CkPXbF4crZ-HcrQlU6eEXtvsb8FJzdY3F_3UdGnx-lHxtIvXzK3_XriluG391ya7n3QLo8jn177-56vg05ijGxh3tKOHh2X9vJ4LJ3-vTvDi36EXXG4kAIK3_U0VNlQDJtBlPk0YDwk2TkKChLLPVbp8bQFKrwcycIui50rIIOTUceFWSIoyPBx87cqlEtyoC4TbeYUqQydClaHB0Oo2IYVqKlSRGLQK40RkEFKh35gJT0w010BoMFRhpjBXBBUuaKAToLU-653eTgit2ILNlC4IJ0fKEmLGLdAgyIxIINPXQn3thJRQDFdGHlThQXWGiheLlcmMvNDIOyGFxEJaM9NTeHo6Q4VnrD9BhQvivNbDz-JCaLoWbOZmQTgTUpYQM0iBBkGm-wKZvmbqayNkEpJDqVhFGZ7x6DOPvjE0Gcpefla5QsXMPGaax0bIJCSTjEknK5WzK5J5RRpDk6HM69YYmk9Q4YKFq0cZaOS3GvmtRn6rxX6rxX6rxX6rsftp7H4au59GzqaRs2nkbNrp17eR-2nkftpJC97YIzX2SO2k1W3spBo7qXbS6jb2W42dVGNX1thvNfRbx0Xj25Qdin5eUdatkz_rsT_r7M86-ioDDYIMdenJL2UHR7jG0NmfdfU-wTGvJoZnwJLoz4BtYvmpWjp3njtRKKtEnseUjt9VxPpKtS-zaE0bXrWkdUrsth7fZLz9uG-RgvWXBfeOZQU3hxZ_v1uRovWjnco6WXEB4LsLAC_pZzZBKyXXRs-zbzghIs-KZ5SR4XqXLwu4XyaSMK6lLXj-c-_5rwCdfeyAd6uLLiB1d2Gs-w7ejPn9QOU826PdotdnNzXv_hgBoCsk9BqlRsIGpBDSCUnhlGmjHu4O7n6W5IwBj_XltAKhdTZhMk2cvnrL4GhOfH-rMFLwE4WRNZ9yEcaEd-1w6K1IJqS5o3LJUwjphOgp0gkQnj5N_XqUew-o34Fa7bcCugsv3F144T7dYeiOiYvlvYxKAFmPthCdoTprGGNAMiGNkEnIEcqo9JDgFsMzlGhc226kYgu0CXS7xB6hJeOIiIzLRNdbVPWpwwsTrU8dPjtMugRnHPxKpSTUp4QRitrYAWQbVAC_h_t9BuIipQh1PuEyf5rXGkXt3zaKOsYESCXEuxftn5OVu1EPe68zL7TMV4ZLC37s6aUfb8RIi34oRpe5HAkD3t22cNxHyg7uDl6SldKDyKXylICxCwwkl2YyQPeN1watbXl0kXFH6u535MR1hB51TlyEJHnvrCNDxHvne26dxKXQO99za23ZMIf7l53S3mxjhshluEyWiWu0txFS-0s5T2_lPL2X8_QHOT_1rjBegllCfR5WqJcknNYBSwXwu7nf0_2OHLB2w6ORA5Yave08m7eSp_fI0opBvEeW2g0SeuR75jwcemTt9IZljuF6siwIsK0l6Sfe8O6UKcO0BhblzE8JxLA0gTiZqMqZraoo3Cy7HWdxnv-3yiMFP1cefYT0Qnn0MdLn28GRHUHIjiDkkBDkkBDkkBBoEONkhSmT_8_b_2N9BXIQH8gxH8gxH8ieDywBznCTWpDqRVqaruYrHXeF1dp1rLCb4tv_3UGqt9Dm021Axh0W20MFkSdSmIkUirTfoaal27BO66lc3fXWLWY5v9QJKfi5TuT2Wiek6BudKCGpKCGpKD1EHY0pjsaU7iS7EAkpREIKkZDiaQVWxrDnJkvWS0wHS0wei2eJSxFK9xIfIHjvGzPh7QYd92QICWV5YqWlU8LSt3LTVTROkn2qEhR3yjHKeHfKoQU_kfklGzUkF9VRlh0iVVa3ElOoxBSqZwFrTaXofewqp6uSmH9KbHHiNXXVn9ZM7IMha0saMERrN1Xs9aWgrYJ0QozVrEwtllhoENO6Z9aRvzqjmaf1CWleDc9ZdNoQIcZWY2pWY2pWHTVb0q1osmSnuqBiCk-a2id6VZle1V1z3Qa8nm8PULTohzuCVXS8dFRS8HOldeFbhwvfOpo7Z2khrWohrWrh2btUgSLT6AClEWFqMWFqMWFqIxIraecymgUAuVLM0FK1hqda8qDmOlTVlFYJOip9KzLDVJkPhpgEfK1VaoTX9wP8wAdL0mHiT0tKWnrNSqToG1biAqgOF0BVfnvh6SE_6SkUqU4spBML6TG56PHBR--RAGkd9ovM2jzw8F4IAVqRYSyBkep8QLXWVWOjYiX6Dr-HOiE7hMjonsb1TdHGwGn5LnNdBDWInPl0__zFZloDH_iU45ouDd0p_nU1OLbHXO0BsOoGQNoHJGGWw58mD0dCdrxR-e1c59hvPw_oMZxvDGItg1jLIG85trfEjnkpH6GnHqGnHuGBzHAO1PSahH3ELnvELnv405S1qP6h8G9d1_CPhH-7xxreWf7emg3vLj94xjECRlZAAFA7R_CE7YS8x1PCHQ_PtmA-oHgC1HbUW4QPJKsrzNcYdd0UZ-gEkzbxIOHKNPdJwJK86ajDdNRh-lOQSY5_kuOfw2nIDAnHDAnHjAnHjKnDjKnD9GctGfoRWOtJzlYT-Hjjmsl5eHcxD3QX80K6sZHX-KRg2b-L-73fbcVqt9nSphMg3lFq075ZN9lSkNDowoGizhxpxW4FtGaCvUuuG12HPZeXnHdsynspFDoI6oQURB4csU6RaYIc8Ty8I9YFWuu8alrWb-qNqzWLyfqruUN-yVwk734kS_J1-PVJkWuZKXItM4WbQK3YL08KnYhWTbDf9x3QEZnFpQl3TDTr06cLezaT9dla1fRdSdSLtNVcJ5a0Wec3SHjYa2rluNe8Hoxpwk11EFjMBpGL_Jgsy2bcAbLMmci8w4w52B34rjlzkbGmi4w1d2Ssy2_Oc767a6YFP96far-dZJ6RU9Che3k9vduRjAbx-817ep3cnqHTuKfXOg1tmg3IefsGm1_3gARNhNjjLFW541qB18-XyGWVxctuHh5ae18DrSl18aemiz81d_yptc75fHs1UIu-ODb56vc77qkFPxfLHLrEHLrEHLpEDTiWLLqPdUWyMnnsTB47-wP2WxKcmObYs2Z_HJ9hkIH0ZvSLeCCiKXAgotDhRH0Hn3Nmk-PEidmEwGXLbAIgZhMQEXIiVgW2o9f3JifErVKkEbLN71pUF09runha08XTmjue1pK84h9J_V5Rin8o9eujGOmFF9ASMoUSMoXimIJIUCE-UjIh5H6LPzzWrngBLTG3KDG3KH6DmmGYgcTt8HN2MzYLbsYINpsr0YzyxBLKE0sovEVcgqphsswOwIXjmi4c16zOYdfjpcOWgh_vymfNrz2AFH0j2DXaD8pseMurIcBSgKLlrcQbKvGGSryheiaARUkm68OuUpsKEvjZ-DKPGNls2UcT62wZSISWhbSZYOC7ryh6oPlLTBttZps_-5zNXXmfzdGK5mjFDtK0JKN5WvFbMW6eVPxWjFt7bZ-l6BsxbiGtaCGtaOGtQF0PRIgvNOILLSYGLaYRLeYL7eFAW3vARKLxPrrxPrrRPrrhPvoSse5PmWe3p8xr0rpz2_122ylfPeqXna7TI6nfj9al0AtplIKfS2Pv6a00StE30tgdh-nT_Q55Qw95Qw9PGHp8wtCJTXRiE53YRCc20T0RwOoZjklG9yQjw-ADYe9PFKA_UYAeP5Se62FKhUYHR_I2CXSePMfD40ZNQNUbsNVczmHoYcKlssduyULnA4TBBhVqDGWGDmqxU6bCxRCphIwYKVSKEduhyt2uPN6nubNTMKD6imuZGZoI1T0rgsAtvLmyvCR_UvBzOzX8paPf26nhrx393k65yHvTRd6bO_Ie5ve2aIQUcoQUUiXeUkgVgm07BlHIQRRyEIUcMYUcnkJiGxteBz4DT5JUHABCITLnTfOOlOp8tYmyJXk5pLtJYKsz01MCXX9bFsgEnVsqMxNDwBAGlptcLoC4HNWdqFQhREn0_LnjBzeXGWrchQA6GApaxCWfFLFWClpoJGtL5lvOM99ynvme88z3nMfFUpwuluLcsRQxv7caM2TsM2bsk_j5JH4-idVPz8GxMoY9kceVLyQehaQDX4Ga0x_pYVl0STMRQhEjv6c_NiNfKfkxZVLK1bevlOY6Z-si03OXyJWxzYXubmaE8H3NcWDPssHg01ommyuaAww2NiabhdoDVLiuAAqbzEEDOejHEeQ7ghlpQRstKDsZi6YJPdHdBq5uNSkVoebEUnMFEFKzu-n94cfvyt6xrLvkx6bxu2tvedZd9oVxvAUIgOyB5oHpgWjXeMs-wdG-8dYeS8u2NCcH--tbN5QQ8jvKWwQM5PemW5Wtzd2yS3i40dy9t0b6nkKWwwCKJHga7AiwDJg1ypFCHVsvVqzJe-6AbjqsBvmuHWo6El58xadJd5K54ir9Syf1T1P48agYjkT7TcWMaU5-w3nXeQRYCzBjwhLtDW8sKMpQ4YKFRxBA1mom2o9qAw6jHTisA_c3B_nMkifYmisE0cUmFHxjRKXkCyOa_GWGD4xo8vcZPjCiyW5X7zUFYHgg2rHeS0pwtGfdApAi2BjH5Letdy4D-Y3rlidv2VK4dd1y6i1ewuMjq_CNDVI6A6nbxyPX3cl7BBlNlMFMSUPzTtpx3phRibM8YjnAWlBfhBnI73Q1V-EGAshaoZM2kTcW5LNmwsyJTN3p6b42G0DGIJz9ASpcvXVdJ32TRRtwGJmcs7971-Yu-fGtkXupACge6B6INpj3GAmO7prsBUxO_06_rbxXx0B-O7or9Ap9htvPvUz7Bb5JEn4MEo9j_CweTvzgmESWKh9v33i5y76y8dkT5XyEC5MPMr6ZCWYOX3-8s1aY0OyeenVA8_VV3evZ8Be0IqmK7hfAxLhINohRJ1wKy5f7VbwyHqtYsRSBj40BvVpxADUvQOuRtcXEIJtsOPDr_oUoZb5ejf2awnn9_3dc6v-c9PWRjI0sOrkewSy83Pcm_QQUCfD6PQGcuEIVNpxdwTLOjUS4zT_WcvhJDmu6PsajNroAprZc8p2CcWtDU8K-rLoa9SVqAz_Up33B7_lpPv0KIrd2X_p1rUW1XF_iwXjdIl9lEa1LxBWSQC8IbSp-D5UQrKlSTSqpUPCGomxQmwg0lFQECioE5XTw9zfowkWr12JLfExZCMGSqaXqFD7XIlEzYTHCmq4QPMWYlYVlYxRqEoxb65oS9qXIiHxfojZW6FejhIJlO3rBuLWVmxQwrOVSnzKzF8N6ObRWqocSytMj1ArKDmaz2MonooLZDLRyNZMLkc265rKrTc7veXJa0mmLFkrfNc0w-Qv7miLMV3TanmspLbsFCGtaO8ExsUXZHU7MdwrGrQ1NCfsie9Tp-xK1ce0PWzF9WVgy-Zpg3NraYQ5qDWoRaVpL1HP1EFwGc7kq5VJhesY6bokwm4FWLhGnts-9G2aSr1cZyJSDYyKF8LOuC1oxV1th0eynTmu0kP16vN06Lo5gZXIthRenD035of7kly5s4-JLKACygivwbG-wqP360MiycSZXmbCopqBAaRtHk6tMXGbMdmNRPmhB1h6LKgQlDdRxWTucVFmoMjQQurQuo_Fa1VtvN458q4uuA2K6DiPnHyRGSpiVW9hW0LtFuUHWsUXA7vIX1k6DTckX9yJrKqWs-lu_-yJ498ZbpmrF6M2omRKG-GAINVOCE-MSSoBd-CBSOlYg1Xqt4XUhwUI6V4jpXNl8FbFhsNsxRXMlJW4CKfoioXLRyyKkEo6YyK7JNqGNcZoN4vdZwPzavn0buR_OEKSs3Vye-dqs1B8_GPRV9vrW03i1qZWyn21q5zUrSaOBXuwgAVRgN2ET2lNCfko4HhKQKtqE8pSQHhLwmz82oT4lnA8J6WnkeE65li2tpywfLvlatm8D-_myadn8Wky13c9ETcRlBfeUD1vt-ZFIqW6baRPbT4n5p8Tjh0S_p7CJ5afE9EOi_5yATaw_JZ4_JPqNuE10MyTLla7r7u9ETMp-JiZJe1V0rfX4xCak3dU7pWsKDsSmFvB1y4KndH3gpPW-zbXB0jgIqwyJn0gpC_bVjjx71JSVO32nmEePktJzZ6wF2GRs1ABbTEAW46KK43tv9Ht9VyGob9zDIYXXtu1LvOSFdK10fda0vJKsVTZ9JFla9jrcub5J-vlErLLpM-N1sZUkkXUvnjMrQhfPsbkyQwdBi2xZqGD1i_uZuhZ0EFQ6Q4WhRND6vK2FrrpU7yTC8KV3RiElYWRS1RWCNlJxCU67lHhpl22a-5wTzxIeMEmd7dq9W4VdX0JcqjyhLYkMi-smUGPoYCgTZJZSIFxKCUB64sSu8Kc1k4WUhJOnbwU2rXk-pWxza_uS9g5Rqm_rdELsJWJqL00-mVHp2py3CaxB5ozj6CnASoB1xkYOsMN0aOzFR7-Xryga76yTlH1lnfJFj95ZJyn7oXVa87CiiFaUX4EyQcZWCFS44MG5EkHGfJREulAS6IKBEurCCmn6fex62wWXlO99uEvuLnkGqZtVGZtTlK0RVBk6CUqTIV6AxAuQeGrhCCethyCiN6KgiKmCmnyiDwWMoVFQk1kUSrDOmCiZxXKAzQBrpkOHtLut85Fg4N-vCAqxEFGuulHfcyQhYydDjaHM0AGmuK7viuOiV_3UOEEnQbYTC2oMZYYO6pfRFIEKQ4nqsgUXVBgyKlazUwWjKJLKWrbM2nocZHyVSSjZ-Z3Vg5Nn-eRZPnmWE89y4llOPMuJZ9l24iD9vsP4eiixvCWWt8TylvCBXFovbvfrUMLyk3pu3cDJa9eRTL30zUIHQaUzVBi69F40QOpqlCsPhipDJ0HnnCBL7RANY2stSR0FcAlSy1enuhckaYA7e_L0nDw9iacn8fSkRBBPhSFC68FQg0WVewOactyLKlZ9vQvd8dmdwdSqm3za2jKV14waq24yixVe2DgCLAdYY2yeQb5pOtS3TCPtWqt4vW3zMe2SsuUN7VpP3GZ7Q7ukbPmIdsmidn4gazBdVJNP5lBe5uVFNZllERbWjwDLAdYYG2eQzyzqeh0ZDZXMkLzZfBxvlnUV9iLxu2VdH0uc882yStn-0bLqVKyQnm3Lt6y3vFFr1hsxXW-TT-s8H5TYZJb1GScrsWA5wBpjVokln1lveY16DzIlydzEb14JJhDhV-J6OHsWgBpDc0NnKTCpgq2LbFpbToi1AJsbkwXWiKlbKvety5T_i1SuwiAdu-z3uv4olaO-l0op-5lUypxKwFGcZokoaqCBM58hW0ltvzVhcn-nyHPC1LDiXD1-37lePFHiE3UstISxzwAbiJWE2GX9w3zr_oZ24H6YUqADWqDPXYkdCd4c_wHT63I4b-Y6ms09uAY5nBI2MOEs6M5HT6lsStgePUOSp6O2BdEWjUCLGqThag1WA0yeut13FVI25dtWhOuo4lMlEkWYXgF_owha9mr3peZLw59pr5Yd8_2A12XKDzu91PSUcLWg56eEojVQZyhZ_TaFGe6hNZDscg_EYqn6vO7hGVoILVTAQjhsIHYmxK7LN2G-NrEDXTvAFuI82vT6CD12WgdVUUoiTV03GKRLciE143Qba7Hnj_OdjOHb1jd2X721vQrejlbReEzBkYhlMSOxmL7W17FutCKaz2LpCdvWxpYP8B5ap_O43Ec5isa8OKT6_IIETiz7xlB8ddO1-3tDca7XcV9ZRmn3nWXUhj8bsFiOdJDpSUQUz0RE8UyToSM0ThLymuAWwzM2ZRp9uhIG6qS4vgUbmTKNJo2mzGIDsY6mLK1ThyDfmdCUJb3iGJmypLfzQZmTHiKSmqf8mDI55cRFne7TA6ZG896AqTFMObyxsbVFBjW1bYbu3LiKFhuM6fPCjJhxAWn61tXgYW6LaY_MXDV4MfnOji9Yix3TqOJo2xLvhVR6LDYD7IjtpSgN4-0Bn7F91fxqX8VkLEF-Z-dWle_snCjQOzsnDb-ycxIf3Vubs8bwiGHayEkFFsKNXMaSkRafT5sKqTpM0ZdqH8qEKXTLzdYW6fb5tI2S2kyKCOLJWxjtlcV4W6OLRMItq8S42_KInJzneyd-lvdOfLX7Uril4VesZc3DywGv9Xq3vcnxLiXHu5TcY5j3SJn3SBn2SOpmc7C9yQ_bm_zT9iYH25scbG9ysL3JwfYmB9ub_NP2Jgfbm_y4vcnlSZc16jwFU9JZnn7rcX9pgPLqu3kZ2pRLkNQX671zsEXKtF2xKThOdcDYD4tZp5yDjUwONjI52Mjkh41MftjI5IeNjNbdtmqt147fbWKk7CtfV2LCXWLCXWYMx2S-8E6g8E6g8K6iwK5CVUKDwVfCmNMX_GKo19-SQShEfy02EDOcviTm9NIxw-mL7hEi_TXvzubkeuwlvjCbFs5c8pb4OzfOksUGY5YzF7idaFuP7EppjynBXsOsfjTK9nNKZAvKI5_HmbGYOULV_hgdLwEZLwFpLwG5Lw8EvTwQ9PJA0MsDQS-OoIu61Jiv1piv1pivVuarlflqRb4qwl7v1yusFlY9Ko-0sA4QQNFCiw2sfKIW1uv1yShfzpM6pgJ9wjDzfh7mEhLL2R6H95P1ZrbWz90zQrVYPc7QstVjDcbPXRcdsbtPrMJidlN7VwHCVgMSXgMSXgMSXh9IeH0g4TV-7nC3t587fBHL18fwZ80vTgS17PHmCF87nV8cJ4pqtZgft5gft5gfN-bHjflxC_hxu195CVxWO-Cdfy1w08caFBjgKHNyLfgzqFYgxbx1oUlGhxtS-YxDMpEFTEqkeC04xddPInB7kXNqTKHNYCPnhK1aTF5zNpi-l9uxPaODLSCvLSCv7YG8tgfy2hx5FdnpMdfrMY_sMY_szPk6nyR35o99suT2-1ROBiAupuORnLgYiw3EDNGTjwUYUtfvs9BAQ_oJpM50zFLWfh_RWmfZ9bJo5Cw7vEpyj8RgA7FqRnJR1jDfNKNr2oFodBXeETcjKXh7BUbh9WFXTynz5xR8b9PWFqY8Usz-SDH3wnrD1A11NljdV3zs6COdN_WIfvWAfPaAfPaAfPaAfPYH8tkfyGd35FN9sfahIXaExLP75y9maNCkiMy4OZDD9UX_SPgH3sIQ4bfYQKyh8I_riWGULx8o_OOmsIHwjz53JWYk9tRi8DWQON_5JPB7HigF2aOVuvuLE0ESnf2qQOKXKyxm3eqeGO887xqCZh-Pjwe91G2rk9AEpvdo_iOINyljPBQkPzTiLdLwF6ZErkdAgUdAlUdAqccDVR4PVHnE59Xrfl3Pb2jxuHwUlP2AFku77Q0rlnbrm-Pq0fr78V5Bc16Od7X7bryr3Q_HK9ZmfYNBrIhYm3mfeqJ9FJ2STwIERmOWx5T0c0qkuzNgwSf0YKVECcGFlEnnuLaZ6En1DM64TUqHlxGaGQ6agsn7ocl3qma8G5vxbmz6O1WixDPg4TPg6zPg9bPHhmE-8Pjpefzyn_mAd47Ef1psCVk-bmbKPjAfGFJOsVXJenBhsTNhxfqGk_X6indTZ4EboqZj8XXUbEKL6-gQ00ruL0dEoxtwpVSxVcmYjBUzivv7Xm50eE1kybbFluPL96cOvCZoSjU18AcwTErgWqUe49d1sTvpkl0V0xMTYkl7N4J8iVsKTI30-IeUMxiLvu1lT9y2jBEWzlX2Kc0UAUOgeQ1E-1JdFwOFG2Wpj-Fwoyz1kmHRjmaD8Ubhlk6D8cZD2_FGRbvFeLyp0EE71vKVPb_14vmojgH83otru2-8uLb7yovndB1Rm61_vkPxR3bMxOQXO2axgViZjB1ofu5PEjizlPTIGstPOCIw-fx5hOiPqUTsQTJn3idWYrQyCOiuKZVSROvvby44rTffXTBYsLuS-sOUpx2ZpEQ2KtXHFD27fypzkt1JtK_R2TAQ7WukIJmNFO5rpAK2Jon3Ndq-xXhfo3WSJUjxvkZ77C1ESo6rf2Ah1v23dxZC2n1lIaTdN_sambaX4x1v9mKi2Hfo_IgInQeEkxbLdELAB7VMBtOK9bw6rDjDAaJiq5I5GavcYzoyFdwcQO68YIVOfO5gTiqkz5ESn087pHw-7bfy_TEHTimPKU9bJEmJjNKeaWdGTtrtaMsGot2OTqW3F2e425EK2IycvNvR9i3Gux2tk8zFGT-duBf__v7lAQL8oSkQlZSyr9R53ch7p85nf31Moe2-Ml_S7ofjFRnMx5MW5IB2L0XMSLutIma8gGMwc5dFMbyVLZgYHuqIUaBj8Ym89i7l-4X-VUQkO-P37lagU8VW10w-MT0m3zZTzUwHals-Yv3JAanPMaHO-b285fZe3qTdV_Im7X4mbzpl5lXjE7F8r_u5fEge_T9XSv36C1XkoIoM663FyynF23_IwuZ495b9Ns10sZgvmble5pH_A-dWtmcYF9X3L4_zP1iveNC8nvetCOziwkVDFtZngA3ESgcvK2L9tSKBW8_ruWBQyYkPF29MA7orsd-4VqwFDm0Vtx-mtlwzGQbBrDY31lx8zpALk-_C5Lsw-S4x-S4x-S4P5LsE5LsE5LsE5LvEDxW0C4yHDxVy8Q8GMHzDf7Empbqt_QfWRNrd1iR9mRdt-b9YE2n3Q-8lDkDXV154ut2ACFjFiyGK3bz5VkhN6TvFJtwn5vZEtuIdE9Ga2iZjRfb6u0lNOdPujBgOW-hIzhuaMVls4Hlkxbs4xrFXvoqjWOda1Zdjvojm1kcCjPMapYR9JNIs2leZGlemxpWpcY2pcY2pcX2gxjWgxjWgxjWgxjU-8NcuMO6p9JTsjjJ8oOT1DxS1eor6gZLXNxRVnmcb2bSYuAQxBjJfZ9pvQHqD0NSPI8Y3Wkxu2LCK3jeIvKrqKthkHW897fqtjkuheuu46ZLRJdMlcz1JsRx30z4NEWw8ddN-x9UWKt4UtceD-UYH83cKD83UFpmJ-2slD7WFKffDqri2MKU9pjzdh9LaohSWKzlibfjxcM2NC2swa-obf5xLxShxvmXkm1kuNIqNH4o0fijS-KFIix-KtJhWt_ihSHt4KNKC_ZN-ZsZgwUORFjwUafGNKu0t4w8PS5p_uLKM24pJ9s4gt_76Koe2-8ogS7vvDLKRXzG-KiMPhrfDybwxO-arNSbzshFBZvPp-NwfT-k7ndLfKfRZCFtbZALuL9Y81BamPD6P6I_PMPrjc4_--MSl8xMXU1vwhLTzZqnzZqnzZqnHm6Ueb5b6w2apB5ulHmyWerBZ6g-bpf6wWerxZql7LvSB2q6Lke_UVtp9pbbS7oebJVGroRfoESukCSY3b3oGfk1Y9HJM3vRIvmDTc33gIt70SCHa9AzetGkKD8CUidRyPG6NxuOG5P7k0lNtUcrjyf94fMIweIMlG7pxb7C-6a19gHov1ta8wRulwRulwRulEW-URrxRGg8bpRFslEawURrBRmk8PEMYDxul4TdWU6Ty_UZJyn6opOILpZ8_bUQmv3WpKUrmnN5NIHSqLrOnXYvVJMl--q3FDPYugrXJnQFObsQ06L-m5MeUx-3CfNwuzMftwnzcLszH7cLk7YIo15w00BMT8LV4wYIblJpinoxooeAN3BNT7HNNk8QXInUyUXEnk_nJZH4ymZ8xmZ8xmZ8xmZ8PZH4GZH4GZH4GZH4GZH4-3HCaD6R9PpD86TYFovjzfHFCIgZHyr4j1RN26xYzr5htaQGDo8scG5ty0LuyS_HLkSgChWQOLg6X4ynOTOGvqdnaAmsgtYUpT9FxCn_nzbYTptD7us0MFBRBR2ggYsBS0CuAFGY4ZsDafDYYM2DtQICRTGsXGA_v6pTr3dh3DlKqfMWAtd03DFjbffNw_2v8rx-PaNkP-ywKmPBBgWJ8Qd_kpvcgFcd3Hr_EajK2WkvEvEs6027T8AWtvDu-UBK_E5Ce-q8pwc3_BJU9p4S1PcWQKfsTfe6ATWcAbxcoNryH12r4BkS5vx643bhixmTi8jYzO2g2ErFvHbWBQvYthRmO2bc2b4xGYvatHQjysRFJ8eML7YNzpF-y9uJ5oiiqlH3lSM0K3Rg-IDtwJX9i6eXEG_eKMb-1uT1zVxyZu2KNdfQsT5xeC3lOX-7PsXn_fg727-dTJJhoZJry9DKulAlTniLhmO5avT35TQnFzgAbjJlDcGkpsh1nfkwJ4vRkaGGfiDQzUFTik5i4FjYQMXGpi9T8DJm41MlwzMS1o0bZT2bi2lWLMRPXdsggnDET11EwHl6N-lLM9-zkfH-TWNt9xU7ONzeJ1Ujd8kWYaIQYLpWuB6OV8W0DxZi6m9xMNjJeyRdzIjUHxGJdCQuJhVTU3ZFe1CVNedyBZI69Y8o8p0RWLfMORDQ9Y8AhsU3ZvCth8vFxoeYefEVyr89OambgaBAy70gy70hyvCPJ8Y4kP-xIcrAjycGOJAc7kvywI8kPO5Ic70hyes_Qc3l9gUnbfaXz0u5nOj9RqOSbdAaaBMn37QxUuWBGqD5B3KJ8ZA9z2eord6JyV-uuS20Yao7FRMMO1KYfCVmBTYcxQOWgsC2l8M5Bc1_vZkuKmCvRzvK4rSlP0YOkoeeUyCyV8pjC2xqTEpmy8rjlKo9brvL0fERri1Lo-Ugz04ZGqfB-p_B-p8T7nRLvd8rDfqcE-50S7HdKsN8p8dMG7QLjbh8kki9fL0WVuT9o6iGjMgI1hlBvpTeNoUmQbbFwvwopvHR1cPWmE-kJagTZFhNXn7iraQ9IjYKKzINBuL8H6egLfjpStV6w7LdPFfYCxkzU61qreWBa8DOVruI-SV_q45aq8i0kIRr4IdE7N7Yq5AO_QWprjWwHfjhTTz4qbtEME6m8W6m8zam8zam8p6nxnqa6PY2qVw12KTXYpdRgl1KDXUp92HVUt-uYplvZfnnVJs4fEv03W21i_Slx_NRmc4koK0Yhzbd2DTQJMtonUGUI1VamGJEWI4WQTvUwIt-ERYgrspky1ZSpWKY-Zq7ooIoOquigiuArsspuUDUtZhlKfYqcYFMsQ6n3A-KHJPvk84R5-yHFcKGBk22-kaZY2S9pmWruyFvWnjTe2TTY2ahOtmAH0nAHMgF6t0v4w4W78ocLd-XVhbtppuq4HZTBewyXGE4bLjjnqAjt9IqwkEIIBhIVKAcYRIo7cKWtG5XyBx8vtOuBkdUGU-j7rSDr1FtlBdIiBeLAifjefXdi3e7bhXTpZo8NtPzOT5h84Ns0N_MWkO4fqH0g1L28F2pp95VQS7sfCvWagkG2SuR00KGtTRgPCcilbEJ5SugPCfi6nunu8rQqY_LlRgPV7R9WjfOWIMMz7i9YOnjE8Al-HiuwEJ8ATJBRCw2CcmKoMNQJKmg8pF8M2YKVq6_cicpdrTyg8QDZ6gd3YnBXB7cIwU-SWY_V1Wvhq3wv70IqIIWQbzsui2tg_FRwvT8OyBgyiCqf7jNQ39DAFnoEGTkTFMTvhFbMkaUWgGOMiTgEV5ACGQuMoCrcXGLtFqKtsXZ6EISyVO8PR3oI119zda6-IlQ4V-G6CrdYuF_9CeoMFYLkXaa5oOP71mKZL3xIlY8yzhc-RNq9Xmb81IdIuzW9IEY1HSRlkpCfEg6SqJQZaiRk6aAFuL_SB1B7ypU4V-Lqg1ydICNk-s04qiuAbF15Q2phUoUDD4udjJmXlRRrAZYD7AC7uKBOSHHW6f5ooLVOCXmBMTYJiYGkFEwxCzV5juB1Qe0D8oApUH6tdavsh5ojLufc9EOmTb6NRsh-vGfhCQM7bwZs5vysMTwsrKt74sNHi9UAQ5t9EpeoJ3EJ7U-nghFUuKCBiCUo1AkyQnASHdOuGgjomCz3mZyB_v2lGCn7oaHUsn8Q0fONiF47n5o3KRJgc5O8erY-udX2rOUUCpz_xJvC3cID21liKIQHoe_QKh6rB2fL6b9hNcBQqDOzjsy0ILN3z8wBMrOOzHQlM6nJzE1yJ6HOzL8y86_cSajzH4Q6vxFqsX-lOekq2UvX-pBT24avwB0LCzUnWeUIBe7-4JaD_d2vjLgVndJYdArcaDLDK9TT9R78cnDy4as09bmILG9htlKYrRRmK4VJR2HSUZh0FCYdhUlHAdIh3rsEPjphij_IUtyfe9UCe3fYuiueKT9zwcJcsAAXPI8DRebccacm5n1lZst1V-8d_17tvuPfq913bkUiLvxmvCkcdJ3vB137rwedwpGvxj8c-TpDrG3vuxXpt7qeIwFkMlGcQlPa3jrESvSrFZL7MtUTkeUbDHQVbAx92xyxUqb09yGqPiM7oR35clFmqDCEBdXZ5gArhIkDhtoMIgWnDGT-TfBWBW-ERxas74dFisATpd4X1BiCp1zjAKggAp4N8xhkOzsVlX48QEYq-35edkONoUnjoxfudEj3-3ZLeszgGZ4WNsNZYm5mwUIm-gX2VC_m4YAclgPseMSMwi1soi7pd64M1gJsde_wOiddZDw_4IfF05woGavNjCsi-_OCw980R55ObHwrRz_cbuEDQrfKvnMnPf96lxJrda8vmKiWzb_e5sTuZDX-4ciNqDs1zQqJ5gx4Uic6uT4ug6-HagI8MpQUaWrsJxSKnDFiujNOMhGjPkC24D6TMOMgw7C_fyQwDtF8Mcn00hqDMQKsPmKVh6jXfQbOjdFo_VKRwQYZCOkKOVtphhRZurQV3wwpgfdbART-oCfD87YP9GQ1_gc9WY1_SruWjMzkhUm-loJIJ-R-w8sI3P6sjoW7hXXdVi2GT60aLMSsSypsXhCmZ12mIcCz6W9Fqz6LVRq16jcOgjT3acwNFYY6Q9drgzCj7TiqR4DXLM1th7IfvbmEFqodQkNs4lJtLQqdkPzFQGQ1BeoGAiaDvfcL3-7vtDh4Ohj7_w2bCy-2VWN0mvnUihmWYRvSP5QhyTYNxGRDVyHAyA5pbxjPD_jhxFJHQKxTqgCT9ZXpb4S9HZ4b_N5kSePvTZY0_orUfE3_30iNVPCq92aFYIOhckIIanRCuiFQZWj4_YxCiFTfgVRJaBIxDMllofEAcddJk1ON4WFhW69V18RcoqWTVTgxb9AxW2w8YayA0jbjp8OzmYUT_IX2azp_sfEtd-n9sa6UfcWFtSvVI7jA62sCaNPXdwdgv9rO-7aF9y5n9y2chRBylgIZ0Ts3-7ghTz8kk-e7ktHz3ea_62E4b7s_dOFHY0XyxKcTpq8OS4-YEefzjstlRe8sFhcuq3UwXm43dkN9ezZBDsdOP7WY5_n6VFEa_4OvWI1_6ivM3KPw5MNLes6EMPfKMfcSkcn-xEmrQMSfQUnDBmHXn-nAScpZiGlaJpp2f0vC6s39HQcHTwcP7OKM1SnTqVPLB0NwcmRmx0LzCRpccPDQDVnXfCdPpMOORww3sjoP3WukTDLj7QGfP-Y_J-fFo6lmYvkXHOsOYKxOCb_icde5T-LsENCCyKPM9xYk5_cWJKc_WpDV-EsLkveeSBSjMI8rzOMK87hCPK4QjytnjHApYyUKc73CXK9srmfH4nW_uFuzovsFXzE3el_gqMdCgzpuoYATFvNmOI4mxE6eCH1Zf2B95Dxl8KR6MkzGgYO6vCcoS2l_O02S_r5TltX4H5RlNf5SWQrtHCoRy0rEsj4Sy0rEshKxrEwsKxPLuonADTHXrMQ199ctjILU-_GiUZD7AxakIDWxnNeAaNovTxis8nisS6qbDN5Qf4B411MfKGZ1lFTkpLZfX42NpbzO1_djpfE_SPlq_NWZaWvEIBsxyNZApOdBCdk_sG4N_LjITCNW2YhVNmKVjVhlY1bZmFU2ZpWNzwMbE802H6Dl_ByWA6xVP7t0ptDcVVWFp4MHjMU847DDtArW-AmlYEZzGp8ZyvRfH8PWtpU3U2Vb51S3mj8azGYSzElDu4m3JXU3jgM6s5OcDqTFQJ2QoBjWfUdN30PuN4uQl70OyXpF_oHPyWn-KlVIfoWH6kbBNe5nuPS9xvCwsA6k0mCJe3X_JE97REgJJsQItoZeDzCjFH08YR1JuvYDmYdUNybl20fKN35aXKzgWp_f2fAW2fCe3zOV1fjvbHiLbPhq_J0NH2nPu0KdofIAkXkaMR8YFHpExGgkL1iDiM0g8jOSF9lRCKFrU1LMGOhRGOoMBaRlBARl8JUqHaE5iNXg6AYrsWnU2Ooo3qM_5E3OjArspfNT8R71_a51Nf4H8V6NvxPveZDgTrxNtaZ-wnGAQuyQp3fImFMf1xllmPFzv5ktLAI7Dy_mkxjMpAcuMxPSYsTUQ4xqMnmZB0N0xUsqDyBrzGfAhGbAhObBajUDqjKD466Jj0ZFrTR2OaqPhjg3WGPPMt2DzBt3Dz7NFKYtMv04vRT1ozI0GApdvRRmOHT1_fC8RvtzIFQZohMbgTohaF619mww9v9af4Zl0pH5Kws6Ed6Ra1vo9LUtizkiYCeC54GngfIUmtBCeZY6SBc-eOoQWd-vRl5bX2n8vfWVxl9Z357gXeNlLbvGCraMtyflHA6-aUPl3JHJ7cnRElmV5AmDICjRyVMI7SwidIdFB5kD7DBYecK6gYh9CFS4AWP9dOiDixqLqPm8VdOxoVXU8pzXEQ0zqXA94wv5_SWFUPhTfn3zUhr_g_Cvxl9dz-jJH-t8PPL5dl8gunD685l-ZkIaIf5xmdRDXufMMRweUEi15KNOT3qkWoN4-iKVGTU86SBHarIQHeRIXQU15KQjD6nLQo2hGXsxmRPGc-zdVj34LEoX4c4qQnL6B1K_v0shZV-9ptIzXRnuGVy9Qsxssmc2WFijPBkRyeSrM_nqTJ45jxjphBgxynRBWSBjkTM9UtI-GfOe-VBDMUN-MjxPubG6-eiNjU1wbFkWoewJlcAf3IQPrVJ-fxNeGv-DPc5vbsLLq0P9jpx8YTnACmEinXfsVoDgHqyIdsF3gw8o2Akx5LqUJygj1J-gxtBBUCekcDHuAfeckEKlGDGqUgLGVDAsjWLlEZsGg2_UqVpIrNuEJlSWrBisB9gqO71KSXnGu8VFWMv4_YOhUNVWBX8gABiaUyG46SGSjBH84EFMr_BtBjiyk1qN1NX8BKG41vYEobhitEYDdc7ENXEPKvWcShWquVCeTjUzYjt9PEGZoE5IocqNT6r8rEP7YLH2iBm1qvMRM55KJKqgBxKRsthk9RM5KwFGKiV1Tsd2qj-n_4DtrLLv2E7TcxynIhpCknB1_CGOb-nKEjc6qml0stCI4zTiQY3OLBo9uFIoB5ihKi2gNI2ZT6OL0wIZTt3gwObGAo7T-N6yThvRZwnFSNIjsRK9QW4rENWYbw2yrMCu4APu03w0qk-5z2q8ft9__lh-u8Zks-LYxVt7mB7WmPzmRV1JoDj14m46n5d0uoAikM1F9KPzJRHt6eCSIVYNxjeZFcNLU9KG2eZ1ejtLSmaG9u0pW5quPOtg3MOb3kf-m9SuCt5InqzuoM-raoL6eL_sYztaRTIhjRA6Px_u_qbNmN1ZwqALtgpVhOjOrUJI3EdjaD5Ay72aghai-7tafdCJwbkG12Wke_BNXV0B4_hHQBAGMLcbCwjCQIJwrA3V_Y7wHTof5cXognSneJmXcKlzS-tYkQY_s7GiKqvshyZSy47fO4foGpdU8ErNllTPM5TqeZJA3ZFfCaoMGVGZ_EAKIo4qUgkZD3m8Zs4Rj6HyGAZDlYdVeVh0d7ZjbFULVYaMmszKoj7h0EoZhYSQReoq85YcRZUntK_kd5V9J7-z_N5NhPK7Kngjv1pBe6-5q-yn5OaSuQEBchXphBRCkhPggRkzXJjTFD1s8VdaB4bPNRCK8jjKE1QR6k_Q4OorQ4OrRwMwDj5n0TFH2DRYf8IMY5I2UFkkm4WS1Z9T4Fz_JMdSwXs5_nJV860cS9lPNXgdRm7Zu7AzwAphIs3JP8YZyT_GkTz-FQfJ6F9vGgkeFQniX80XhGt0r1eJOCZ_QqNNI0J8TSEU7ERUSSFTkNjTSJMhYmKaC3cGghUD0bMk6UUAJQPRNWQd5MklHcYvb2l3jQqm_WxK9S3RMyXtm3-_Q9elO9WUt2Xfq-aq4A-qmcp71Vxl37mY8_TKdFZC_DtFUorU46wxPEKtOf3TLy2PiD_1kVJG0E_iTeMk3qQFKxesXNBoCIQIvqHKED1f0yZPLumwwXK-8hmhlsnwwnvWv_EjqeAPwnv21_xIyr4T3py8YObkJHBNZpZHNeYOzpC4rPYuuuQlIc49FOLsHx0JYkQvM6nJTJAyE6RMr66PzIdFI9NZlI6CISOgmU6stK8nl3RYQI-k0Q4vDavs5h3j54bgXSZBxvybJGcfmPPDneqAcLiK-MOcUfxhjpZqTmR8lFxbnmA4WRqINH-mLglGLArTi8LEodBx0Ch0HCQdjKDKBYPqB-caXJeR4BKwhMIHPTrTFuMnQTrH-OBUyhqmXiZDB0Or0WL3w6MkZ3c_sHqr7Kv98JA3MN9rCb3_-amWVH-MMqo_ahnVH6xoHi_3tcawi8IvwlPpgEgqsNB4gipDg-sKIFOQqUTlIykdsBHMWgNssLDWfSxzQ6eKIVnXVYMRWqnUC2093vPMVfad0Nb-R9O-KviD0DY6TGnJSd6a5-bfXh7NB8yTPP7tecmokTOM4DbmAI2ZQmOH3wKHz4GTR2MO0JgDtMTi1wKP3wKP3_hR02jAWVTymgt8IwvYjvfmsh3vzaXM1XvJaz744qf0uM33Otfmq2OXEwVgHX_JUYzFCmEi4BirWRB_w3Z0OojpdFizIyAb6x4HOh4-0LFp2sc_HhC7WRF_MWZQWOhBYaFHZzrFoZUHh1YeQWjl0fk4hSPVSv0Womu-Iwi1PIKQzCMI3TxMiGfxIDt-MnmQzmcoPT9AFEBgUMBmgfvfnjxJBX9QPVmNV6q3yr7bmQ46VoHIv4rQsUoc5XdQlF8sT1pBoYkHhS8eg45VBh-rDOZCQajgMfh0hEMaDw6PPEzsYFPSYYMleezTEQsZ4ZaFHO2PYrgq-IMYDk_ZPxDDMd6I4ZqTSS88j0lnJhPitwzIY7_gajL7Fz-lIf9y0ph0OgLRcxVh4jLpTaExmfFMPs-YfJ4xOVzfmInFeAaPlCZQLzV1c1-asRBbxenDsAg8-9_EcVXwngrPg94dnnfgYYAyQ5OhMJjH3KF-LRy-Nzx9vOAlGvPwDn4e3sHPwzv4CVGPTR6D-KuoE4K5GgQFc0Jc5Buil4TnkRmiF47ncZDATRPZ2DTgMH4deAZxjHUCLZatVIq0ToyNfGPudeAsM3M6g_p7cyZlX3nVybF0J0fcnRhe1-QisYuD204f3FbkAUKqKuLvrU6Iq2uQRKUMQjdZJ8fsnRzZd3L835n4IGIGEXh1kGjbtK9GBDAq743VyLDN9MEt1MiwSQXv_awuyCvBTG9uocpOy4ih7LQcVp7E9Y656yH_cvA89W0a8xhjahjfCPWkcZ4llO3TO-oJYVUVoeOEeZLvnhzed0Lk4RuieCXzJA8_T_bT82Q_PYNwvtOE8xXTd3LAEm2DzCHE2LWQ3-XM01vDT8X-PP8o9jLnr8R-lX1njyG6rSL-acmEKLkimRru1jx2mzk8KJh3_FYHh-cHM7vzA5Fcjk87OYrtzLTBn5k3-JMD-M7M-_SJAXBvjPfpM_M-XfLh4a9m8xxTmsbnatqKF9Pst8KfimmW_fBvxLRGtLP47e0a2h3jkw5NJwcYnSXwZYV3khOjiWr7Pmj9kaE4fprQJLQHPI07QfuG71fimFEhILSr6VdCT1zoVqYO0h2rzpJeHy5K2Xe6f0e7tBp4B7d0cI9hfyY_IfinKWpmp9KdO6nJQvSFvoIV4qmow07C1mf1bDZp5a_Wv35g_Wtk_esfrH_9g_VvZOshLqXodqPT0AkBLtW4LQjfOpGqrvU0lZMI3YEdHeyjRmKfHyoxMibNNXv-MX3sQRknBFrUjLpXcW-QzDtuoHvnZN4hE31rFgrjJ00fKlEsEsU7nBTvcHK8w3mHaWTsYMzQuM67FAjDeEN0HDgxDmXCWcVTPh2nDwqgvcMISTMInzjjMIlTvoL3XovpS3afanH3b2l8oMV9vNhTq5nr6DzFHFqsECZCNxKJ56BHSXN0hmLnMWLnMXoo2xQqcUIURIPgq8oTIz4qRKEFJkVmnBS9cVJkxgkxF0093MlEeYzKDAiJcGMlwDDggKiI5DNiL_UFGKnC-OPz0kkfq_tUFcb756VS9p1DmwcJ6jxC0cPgixYipBNirN6EF-e1zczdaAzF7nB6B7dk4g76aDddc8bDyzw8CmcwITyiIj6awaRojHP6x6-TYjhOig45ITqjQQrVY2eXQzPOIDTjxDCZN5ZZtWZjNZJ8FvPRkgT-q2rNv6rW_INqzdeqdR4QZfGSyC_kficQjq2-4Js2Wdht3bTaGiiB1u3EWuEtMncnEKmE-FBKChUDeY6jUDeQP6-9u3Bw_Xg2e3cMzmF39-8rtF_Q_NNlcK3g9S7_a3_qHjB-IZ2QQog_8sSMDEePG7USXDwf51CLmuWkKIRazkL-2eLd6cElHUaxCjXfxOVM_shRWwhyuWcy3znf3sHXsu8024fiU4QW5_TnZgoVhuAI5QvK1EKOW_AvJClUGLKLcbpnYtrOpJZbjJCEntETTc2N8kgB_xSynfa-7W54cEk3Nv_ClGaz0L7sc2qmP22JtII_WJIcG_0cPbX7gv0zvgEwhPPTCoBzaVMQv0ihjgh5Cx-uT5FC9TBilpzj6d2VB5hxIBTET6HCkJWWOzIhdcRKEAbsU3nRMRjMPRbMOm1vd7Za9p1lwmBsCvn3S7-g_pCLWEXxZ5gDmvnujTu-vntQPVIIQTErhZAeI4VKJcqTKI-RC4yqd2MUz_ceSoBZWSnu0FVlQ7sHBqb87eRUK3jNhu_5eCWZ5fXJ6ZeL85s-hYiy-rhuihRCjAHACG26MlrVhJKZ6nJP9G7E9LUxNBmKNqhamGG3E71HklnuMLTejfnYeloUtcbHu9NijBQqxUiiUsnMd4s1QQuj5awuBK5MCoYpU8hfbtFc5A-b956rWy3edzXnPXVeW-A3WuA32giwYGvT2FtR4DWtDZelke_1IeIUKVTKIOSNIbCcLl0L9lotuvPyBXsT8qkRa2JHXhux1t5u6bXsOyPW_b2VL8h_TEchEs7ut3tr4nsULF_h_eBYxYSDpN0NWozuh9xYrVRf5-o6lwwglKpOjr6XGElUyiCdpLPDZ3Ms5m-x3BWe4Ha7jzTwqcT28vs3sUKJ7f3tm0Ba9tWjDhCvC8sBVggTgR20zR20LR20ORy0gRzRNRetjOHomotWy_ow6B7KXbPF6B7KXafFeH8-eEM9eG86yIkOIjIQN08RcquD3erwsenv4RyOfVPQvHsyBjVqIdxFL7Ebf3s-qBX8QWkoitsHSqMB3N6Y-ckcZDIHmTEHmTEHmbwnvwO_wS5KpYnDvd0NWizgIBg9zmKFoc4tdG4AackkWjKJlkCIOZOnUJ6Cgj4pXOw9ajLz090NknX_4_MGreAPUvv-eYOWfSW16SBykg4iJ-kID53T4TnLWDCeFCBk9_4iLOlgcpI4btldh8WIiEjRziUDCLdBUlchJCHimUg6_OGBIIVKXYgIog7QHaSmRBvelGhfmRJtIlOirWZK0YUfrY_hcLuZUrzd1I7ilkV7ajHegmr3ce6T36yn5LfcKXk_qJ0gpFMpg_jNbUp-45qS98sp-Q2wDo3yoBe-M03E3I2ppcba5msjJBW8MULC-7YQbN7nsEKYCMrpL2V9QXgpaw39hOCjKg4n734VywarAUbH0en0W9l0ei-iVVEpRjqVMoj3c9qhg6DCmbhDeA6lXRooOhIUy4U717wBPiwuknK-vgurZd95m-zvQX1BFHnyzmZWOvvPodzZDoI6IYWLIeJ3nCn7o-WUyfLnwmuWC69Zdt8SEVXIGEz07DhL6j_voOwGP2K8x3B5qOWG0wnwWTaX1wpASnL5Wutva9J-YY6y50TfhmgVH9WU92wo5fdsSMq-CYFxphUh6Qp2tMve1vP8Tw6DrM92FW7Xl-IuCfmqYCPi95cYQ_SRseQAY0qMSpCaZsTqwdlyQmzFLclYVGKUZOwHxvHXHkKwiBvKDE2Gtj_UVnZQC23lTmmPKZNSRoZu2HFI12aAtQDLAXYw1hkqQcm6F38Uv_gLMaRvXKZz7rUfJ609Qrr2iMnam2wZs8FXfLS7A-Irrf5-bXVdfwXB_p5Hy8uCfAu9mgtNu7K3KWkrjkBJkDbLf5ZpXC2uuxMJ60_rWy67v5q1c9ZrNq9z5loWMmiiDPb9miJhy3TbsrlxPmn1PLRVmbazdbvwghSYyIV0HPu5TcMNTRrjeX3tcdvxG7_CcvdwGVZjM1wG6exp12F9QM00nM97chUZfrozKppMN2I63QaT6TZlZboNJl0ryc9uKTFSCOmwAlLqwDGWK0IKOM874cySEEyv1HTa-V3ae97vY93aayCdJcSW9hpIFktf11qLpaAMf4FJ0HoFecKA0Ba7813qixGMLCbDqMB-qkd0EADJGLAYViQnhJUgW9XCTF0L0s5fUC526G0t1sQhIXbnuz54outQCJMew1s30mF8D0f6C5B0F4tN6MNCmkdkJ4WQVgTFpEfrdR7TJYDuPgGmdQGks7A81yEGQOahX-ZEQpFJTsRkSPLwAYYEiA4JIOkGFltDkldYDhgSQjokxLQugLSjC2p2SOOSfiM_8s6BOCHpyiBRHyzqg0R9kKgLkrmmHFSVua5Vknt_j-i6XZo7ZgNITB_cCDdIISQodTCUGWoMTYKWUlmogkGe-OxgidckjZmsMZM0BpC2Flpuzl-lOiAJ9sHnLGl1AOiupFyRcOWAi1PXC8aH2n-Nl5SxbXxFbc1BhrvQBimEdEJwYRTKDDWGJkGyMBWhuicvy01vAKqbTcmSEkJ1Qwdmynvq8nHFgvrecrlJ1X50N6lpNKjriyDcumUg0S2D1YOgtDYNtmjGUZtNg2JIqPO-en93UqZJ7jgDUDzQ_UTKpWucSLl1baC-hXdgJqBnOV0BRuXgfc-vTGBaM1_6nkCEdAIRkwlESCfQFJXJWpidLMGy7ZMrYaY8Ja89d0rOsV6dfuOV4eKyQTohRq_OTHp1ZtKrM5NenbAJtRAYvIx3p0Ug5GYyANMDhxcZuQqO8iEXow207Z2ooyCojmd6sID5rD9YQBkJWUCVtRNDYFeCVNYQqwdBKmumaMZpMrv5e3wGU6_mzw0096HPMGTZ4E6yQQohnRAjS3hN2kKTICM4Gc7MRXDuO9tbLOReNACnl5PMNlruHYMTzDk_WeTcQ3uiqTW01zrx98VxUmO9Gb4TFrvJBfi-DKp4i1q8RS1kUQtb1MIWtZBF1W95fyFrN2o6pKKNX_wW0UZIRRsxEW2EVLRN0YxjtmJs7g9nbKLb1REZqnAdxUInQUYe8Xu_FsoMHQR1QgoXA8G-763uBa2HW2G5ZgtA80te2SLKzVcDNb_k-rVkEOvanyzi6uujzcMPaItgIKSCgZgIBkIqGKZoxomwNg_vF98Y27yMjWOKTGnzjK9VDww_6Y3NS6sMjf_f17UlBwyCwCvV-EDvf7F2AugumP5uFMiq-MCRSLoVQtJHuU42xuxIJyoEObMjnagQ5MxS1Yp_y8yOjsyiCuWP3d1I7s54k-jBJHowSR5MsgeT7MEkeTDPtgvcYeJa445y2Rp3iBl3CDl3VLWi-cydSOZO5IOhmRY48wxn8xyz7g0eIZIQckrTV4wJGhlaCaJ9kkE9V-zQk2f5mk1n_282tat1X7Pp_InkeQNTptmeIG9gxKyBEXJpCzq9SUPIpSFm0hAyaQ3u4WwIIl09FnIELu2qwit06qkJNwSqKZMN7wYEzKLNFbFF5Xz9Fh1re2DvOBWByLAC2ZaTbinKO1mU9Cb3rkGZFitiI620WoVhqEbA62sG1ABAeh5XgIl0yMrm95KS_a2eL7cqi87VQy3L_8DhqrZz0qRamMCGsXGxbH39DJ7a0OhzafCfyhWkbTGgRwDeJivvKrcNsTAEMrDUBoGLXho_dQhGvvqj5ikGfPvixTU0kSTvBmlUfC8IaBHZ4ePRoPbLc8IoLhzPCRFK1qWVSVFX-if1daVFQP3ZJ5BVm7-Az9DlSqmodIxjHb77QlA0eD-Ac7ix4rGDEy7Ug10WLr70aoeZPq9kzUSW60C6TcJiCQ-gqdHn6sygBtqaPWtfW4LGC4lp10nvPQ1S7fYKixTo5msH0F4lv7Y86ovdkQIA:5:1:6:842519:8:10:9:10:10:8015151:12:14:13:21:14:298336:17:1:43:3:25::18:10:19:700:42:0:45:7092:15:3:30:897837:31:0:28:10 years:29:4 years:35:0:36:0_167_67_0_0_0_0_207_0_0_89_88_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0:37:0:38:0:39:0:46:113:47:0:40:0:57:0:27:AwYDBQUCBw== \ No newline at end of file +1:897837:2:time pressure:3:Rml4ZWQgdGhlIGJ1ZyBhdCA5MSUgMTUvMDkvMjAyMA==:4:H4sIAAAAAAAAC6R9aXLtuM7khqpfiOIc369eQy-gFtBb6MW3LQJiAgn5-chREdd1kjOJIUlR0P_9P3n8k_4tx7_nv-ms_-Z_z1r_TWn9OdefBbZ_03Ec__Z_07-pfv8z_j3-Hf-m_5f-vUof538tnd6WLv_-r7SqmGEV-Svb9z9f_5Uf2j___S4XVdDLdyfKNQF5_FBFeRzCebV_fk3lU-nH1r-LH1-FH4ueb4rJpOWw7HeGr1rrfy3-Neb6Y8e_8_5q6dpDNf_83_-d8j_H95-6_rT1p_zz9e_6_74Q-TO-__yfPK9f5_XvquBK-N_l-nelpmP9Sf8c_5P-yfWf85_81Uz-5_z6eZby_bt8_y5fv1v6_tn1Z6rjTk75P_WC-p1FoPM871zneWUah0Nq8VnyeffkC27_zONCO6Dfdc_799Vm-yf985XlHP989ews_8zr_7_H-J9rcF8lvvr1paZ32TW24y5bjq-iX5r-XbTWVTT7onn3H5q9Sh7nY8l0l0yu0f_e4aSzf5Xai1Gy_dn22vQ7t1mbVcJCDaBVz3T1TLOg36tVvubvu2trQdtu7Ou3-7kkYP-uh_mZSnL1SYHpRQaR5oElHNLTYwvRjfxTVm_mNQvptcyUlt7KzCr6ocykuZc2XeOa8x7XvKvO6bVQ1dNIUa129VeyEZmVxULDS9FCyr3E8nveMgTAWj2pA1bvRr5X78sO_K9lCObutkzJuHvVtP2W7DjWb-xhu03bWcb9O9W7obPMna3ufH0Dl6w2Mm-I1OyAc9X6XgZbfy2Dq-iHMniuMfV7wtf03a5AZiFtlc_udzG_LxOwf66OlVcSvFakH26JevNABmDeQJq42jvj10LFeH3AR4zn9IAXwhv050uIdsLkhJruEnmNaSHzAUke-RqHR0Z1TW4f2_9gL_t7e9nf2MslqzI3a9yAyCgRqYScgExCjlsLBMgeaB6YVnG69eLdevHe7M_bGZ-XkZTmWvVIJuTwSL8MFIjhKmSQY_uY1c-5f2f3u7nf1if1dz4pjW41-uITqOGXdQGgHi7DNZ5RHxRqnEHCd__HeCoxtk2YBNcNXz0ReJDCroZR9RaC6il5BuUBpV4jM8gABAZJ04HAsEo-6nslH-O1kq-ir5Rc5gqUcw0yQCohwyNoLGQ-EdlsAwtRma3lw9KqUe3Pmx0tBZXSoNaCZEIOj4BaYzWLJg2kScl2XIDd8bUi9Z0nvno0k3efq2M3fsufIF-N10USLkleqEjyqrH4GqV8ceUX2h266ujUq8uqzPk91PO4zQwCi9UYJKeNgBqvrqBCrgZRISfsW6H_CGxa2fZUtnwx2OMGlB93hywplPkDuZTOIdIJSU6YZ_GAI3_zZtdtz8D-uU8JLs-wfs9b5KA9AaC9azK-D1aChVQYpeGacEnAGU9HjiQkHS2GlRg5OdOE0-8LJOHbX6gEH5AfGaH0namlJDQST9PVs8I4s5c1yWWhybPhHaRUZRBPm6WiLZRfQL6l8tqjCfK9STMHNRtXL_GFvHMx44AVBqHUyTAQMT-dDAN5FidVcU1c0VYCndP9O7vfzf12ZE5rBLOvUGboIAgsv6lqaZ10bwKQPdA8YDmcLNnLbVlKN9UyqpZuava9rbdJI0haMp3QcQs0nqBaqWClXEi8JJeFiHpprsJ1bSb1Ve41HZKyrw6JdPLatpnQZxWyE6CMpgirwIRxwFygOiTkqgaqDJ0IEU3TZTupYKJyiYqBRibLuqQI_N68C9eYl5hXuFSYaU-isCeKQP-XcJ_pUbhXkpHRM8H0ImRk9ETCJM2U52YKN9MZKtxyf4BsZ8oTVLjgglKDmdmSK3AJBVpys_RKfpNw7ia_E2BKMvRGtGNJ1YlkVaDCUGfI8yjt0RbQ0xInyQC_96marGZ3NhRbZ0koNPc89QV8xOmoGY5BERjD5Xzy8W2jUu9-o_yVEhr8nGO4efjqUr55FbmHDFwMOVoG8gZbccXNXnzNXz5o9XJmiE6EFKoMDYKMucxMQzLRkEw0JBMNyZuGmGlHG5CJ22XidhmO-RaryExHMtORzHQkEx3BTBbK21EuQXp35vA1xPdeNvsTzV-dOiwdEslBg5kPEACEjCnMuEURiNl8zlwwc_WZO8HbgAzbgAZ92OYmO8aaHWPN-1ngsqQismBihe7fCdu2ZM8_s-ef2fPP7PjnkpC3_LMoQ_nqrLFTRbmkNS9lOHgpXGHuU-jMSApbiChMIQpTNnEwvUZNLf7wTkotQ36dXKTS8627jKzDDAst_7cgMXEGuoTrejhvyy3ZKkyOC5PjMrwEFsfQimNoZT83XCKxMoCHKkS1ClGt6k-X1mRXJFCSUymR3_fXh4MnqaRQHQaBZ4TLFdWyZdEw60oHNFI6gIyPqcjccHymnD_JkWJoX2qidatM26o505pQOT1E0zY8aalImRbFW1V0RPpGls2peMyVocsdkSKZPCmUvkBCgyq24FXHxKo7xBIABLESVap0jtXINTdyzY1cc5sx8m13zYw2YhCNGEQjBtGAQcxdzUtHLAN65YhX2XeOuLE_bex1G_vmxh680WNRhUzB9gDZFtmDN_bzjd11a9wJdv2NXX-jB78KLeFcarQKzuSFk3h5i-m6yNHajW0CPiENd14VhW-S2WtMwBsT8MYEvNEzWZXnyRAeesho912gDNLQt7NbRed9QUZ1eP92pKk50tQ2aZqgXmA5QEkVaYRsJc0wY2-5UCdW0YlV9NMZGczI8HDwmnmpBDfUvTI0GCK21IEtLbHueJokUH2ARB6woIXGA2Tr4gPFzseOnZ7bKlS5qwE0QE9X9ZNmuvD0baHtxQltdzyrO57VPc9aBYCLd0-8OlHVkULJGCWGewwTnxrEp0b3MzCI6gyiOqMTogdNVKxxOaS7I5GwjETCMvgYb_Ax3uDztcEHc6Nw9Z07wQdzg0_0Rue6yq5rHess5Hq0s2Rq5KkyJcC4gQaD20I1HIcawKGSWzVFCiF71UQjJnmueYSiNHMMtxj2nArrRumbRNkmUbYJlG1J0SRuNombTeJmcxJykIDOzFBjCH3gWvV5kCBMZkOTOdM8SPRmZqgxNB8g2wl-0jnzE1QZGlzX4BYDqHLBulVCRp3gpFMS2lNCvpVIgOm0anolmo5eTEcvpqMX09OLCccqyUmVIpMQkM5Lgs8j9P7nEXr_8_DeHysp1Zc3iHdn2jQi_qBEG-RSjYs1LgdaoAUnQkQZtCBDKJNS0EJEUhSqXLBy9QE0uODg6sctuTpEElAdVZBQb0FdZleybvt_HtmKrq72_l3db0cypMAEwJEMs8xLXFJIMs4UkowzhSRDKkEpTJ5kSFGUy-RJhraKiCcZWqpxscblUC4TkQwtaCBiAQIZiUtEH6T6CKpcsHL1ATS44ODqQS7TvtW0ZCo5knEmZx916fbv4n47koGrpognGbhqq2Mnm3oRizPkGucZco3zDLnGeYZcQ-pGITw915AaDeK5hvYREc81tGeIeK6h9TSuqHFNjatCaT6JMghkJPAk-qEQCtJJbl6hytBJkO0EkRTtPUO2LmIRUlcEVS5YORe87yZdBU04vSaclimorOzfzf12TEEygPk95waSEyRFQJCW-OWYKOSYKGRiBBkYwZKeTP4_k__P7O0zevs1r5l9b2bfm2m7LnUFBY1UZKYJmf14Bg-9ljJvK3cdmp75eqKCJ671--jz-MVNuDMTE8rIEkymxtDkgidDiwXDPGyxys7LZ-_lFwBSBeuryF7ffOwJwdOt_zIh11wel-bk8l32S1u-X5W9FGo3MZOiq51yTDvxKX01U81hdaF25i4KXQwOqwut2VLugh76OngUSJbDQI2hzNBBEDw0EaT4c9ezEKUpRGkKUZqSSMBKYQhIwXrob0ZkoWMLWHEevjgPX7r7vXozm1gaLEVot6gs5eyfq59I92oHjGjpHkgKiMxdrX0o2-u57lnBpym03f8TgI7wevSLeRSBTNdhPeYRAOtZklYnCV-dJKLVHL8jJNYnb8hWn73YCn7E4lyJ-FQiPpWITyXiU5FzYEUWops-OgaUeRy8ha5ZqzD7a9IaDn3_dt68Tvf7iH57-ZdReLSF6LRolnlwrur3FnMV_dBiihJVd3tE-oyAO_fAZRVVaeDVGRFFQGjJvcm0BLDVWAAbUZdG1KWRw250d1bbHFROeAR0FaWmOefcnHNuw_0-o5WXxj06LCrT3Mibw5AVGX4pOnhAhfam_AnotFqQR5Hu1w_yCGAua-adx6xxx_23gRpDmSE0iv3B_3byv538byf_29n_dva_nf0v9t9CYIt691LVnUfuziN355F76JF76JF76JF7ekGIs8zEa0q3ir4zUN3dB8GlVWQvbUbBfMUExj4ZIEA0AZAl95hlyeVCjKiOTAI9cNNvoAkcc9xvNBV4Avz1rxffQT55kE8e5JMH--TBPnmwTx7sk3FAFgIeOpxHHc4jD-eRh_PII_TAI_TAI_TAw3ngJaWDXsD_tW6M5oju73VjFX2nG8M775E9AN48ufWXrtML5L_ZES4JmOSVJx6Rp43s1ZvOc07nOecZrc0q9Wptpn8f7vdrM_17Wb_aiqa-ypYl99s1YUJHfEBjD3h6qMdcqTQJw13Fkzn0ujAd-9CFJHo1iV5N5lITuJRIHAiJLEh9cQax4mId1mHmYweXua41SgYxstcBhUIHQZ2QwsUIkeEaaJ1KYQ--rbW-ZDMu-DqaKnqrC1dY0pJP6yut3WLEeI_htfYBvl_dwPl0IiGzSmh_RsstAWbeBPHMSwoZhJiX9mJQOYFWXLL84Z5_xxZL_ijs13ZBi-YP7UJePUYJbQC1SrkOgjohhYsRokILzX13VCV0hXxLZXXMi6AkkCXTBG-xvvAUWTjBycLlVOfKH-jGSiO9kSB17e5xgJPOCB7DrDNreGRHc4r4h86qRyP-kdMMUcuLdHldXcXVAqqXPCM08iCIZ4RSjUGIEVoxwposBIxQlGzpZ_61fu73EvPpXx9BWBjyEvKzkgKdFdUMoYOgTkjhYnULxpnLas4LpCSwCq2E8lSANOK8SJlVOUhg1Tq7hPaLdGglkn6tHvR2jyXASbcWXh6y7-gEaaHXKy5lW84zuZ3g7-21rMwbey1FP7PXS8nO6CBHRdOjI0QtAdbOuFKgxqdn1UYEBfFkTDs6qJiFhtdQWZHPNHT1PIfsIYfsIYfsITs-5-OgZs8uMrGLTOwiE7vIzC4ys4tM7KKcr9lFaa_ZhRR9I60ldE07oKmMtJCjKGzyC_mOQr6jkO8o7CgKO4oCjkL66Jxdcc6uTPc7dLcldLfFudt10qOZ8_RQ_UZ-BFYhDbu78xhEMmXoWfqyxPvii0lJmHKdnWpKmS5izU74HzmBMi0iUrEL01cnonq-8dGXqFZ6WeLXClJ9KMrfK4gU_ZB-L8mrZFwrGddKxrWyca1sXOvwUu2ivubqbH8d7nfobWrobarfuGeoAlfZJNQH_Et0vJTdNUHe-lBH9XVowngoMJ4K1FhIRdI-FNK1Lo2cRiOn0chpNHYajZ1G637Nm_Npzfm01t3v0Iu20Is250XFbMkAyzZTiIhRMtAyMAv6Wh09-gHUWK8FDULOY255gUwGCTKJ0UWobtNlepphOliOZJpYjtZMBQX6jwWM3qwVplikmYKRZopGKqWMqPTsRcXFKM0uSGl2UUq1UicU3blC0yfTnPOOMnIZIFuMlcBzKN1gi3HXtEWzs5Pt3qdiHsoi0tvZ73bycF1dKUEdpL6DWzW9J68KLSBgVEOEAWZPJtsRlu6fOcgqEYMykYLXltfEpDTQfnt0bVYhVqVBCiGJEH1jHKHKkOmY7J9hVwm4HIJ-J54dNoujxSfXklBsIHLZxI4mx-B1vxabDhzPuqqGQyzXXbXLUB4Js-Z7WVwA0ewiiObhPPQIPfQIPfQIj9YzRTnNFOY0DyYcJqTqXPU7CtW-X1St3z7x-JF7ScH8y4Jw-KIxJd1RAqTJeZlZ8Hno6bPEGvILvzKUKH2s9PkPnnav-XXhMbOLj5mn87Qz9LQz9LTTedo1cTO5DeHvSasU_ZC0ThjHJ-ssJnLilvaK6Oyg4qFl5xBJCcql78j_69niPZ3JTlGvn8rWNePFhXksLsxjcWEeyzHd78g5Si2ERvtEqdGteTnS242KFv1wzeVTHuAYrpWzSHGIfPADrmYlKHSOYkbqAvgVF8CvpOF-R1ZPShHqrd4xN3zZ4j52aQSuc4-t-wpfEU7PSbhsIfaU3REv95xZqHhozRoi8uEPHUQyQ3Nh5YoLK1fO7n5HBkdKEeqpfb86Igd2R94zKZHiFssRqAPLOQZAXy5w29RlUiW9LLZOyZdTleRFrgTp0GyHZgchVzFZJultgYU-rwfAhnBKn2S-nvq0v3WBrZ3UPmYpIXBiD0-51757M3dzZixrh7O5czHhIw1UGToJwktw5Q7PSFBmCL4gcA_JCqsLQlZcELLigpCV7OxpDu1pDu1pDu1p9uduawWk4kukFToBwtYzCLlE7etT70Etock3AS8kMZKWfJED1AU7JXtOAyWCBiEVkRwjA4fSfL_mnrDqe9tAU934mikgYiq1o8qtmgcio6vUo1XNtd-6iaqpaxl1Yjkd7bRZZJ5OPy0ItBA4aSpP-CST_0rP772zFH3ByFSXPmFk4o81yqD6Y9HqjJs-A1WGToKMHbHxJRHKDB11T2V-ydxcsLnigs2V4thE8Ve8lvRLfDkry8WzhFK6sAQU23Kx63OaDYIU9map-PvWUoVsb5ODF_3o2bsHCbp3ZE9MNGqhr-iiN1H-0xOZgpvvRWQsVDy0iAwiIm8yf20vvKnJQCdBRq7KILkqg-SqmO9nANQJKVwMvJpKifVqLp5ccfHkSnUUrIYUrIYUrPor08v0SRVfEmAPM5ZFkkI9A1QAWjZSgv_B4pvi-aDiGSmfdKFAcam3UILpVskwjpU3_TiOlLdgSqunE-R68aXvepwgS1y-lqxeCzx2_gx9f2W6pegb013964W_2EzL_HUdh6EzGulRT9HlW4IQpFAEekc2lPEf7sn7b-2ti5RWXKS04iKlleaYXQuZXQuZXQuZXfOHmWsqpGLUBanVQI3UQ-LoLfWQZ6BYpdEPCfN3UJWqMgO6uOy2SPqSR0kptx6gQknnylNPRKPM4FDJpHzKpGBSPnF51DmNu-d0rl1sDXQX9U5DBjq9E3i4cUo_DyvMCudQxjUA5nRwtrC4rzsE4HZfFioeWu4LEf0i537UIlq0o_wtUXQR2IqLwFa6Ix89PMroIVPo8VHGquJbWoPF0Bh1bgF7TAr6xWaeqlqkptvE_zZs7eRq0HcMgfXGIbXa3HmLn0cRFZ2ch836SsbmZrzN7vKIGHJeyKDZg_fTJE8HETNIcUg9HCD0SMMoAlJDgm7iDBroJMiwJhOo0UCZoYOgTkjhYogQ1-pMyDrTts7krjMF7EwUO9NJjNw4YSVW6FSDdI-Ug_IkypMJaYTM-7lncfHzioufJ-le-Z9LieBKlL0Eh25DHoE_6MTYJ0qK4FneMqpDDuY8ghoiTZfkM51UDNzNKkXAuY3pjiSoOfjQb9Ch39iHfgY5CQFKKyPHI0uB-MhSR30-nBsOPP40HS_Tt2hig2PxfdEY15ukwu0RlswNiNptkO4RlO-hj0o80giZVDNoxSgPCPYnPSDdI6aHhXpYqIeFepgeEC5FbQX9ydTn_TJRGVfQh-uq0qebCCla6-ebiPUwGlr95ZNXLVh_WXCRDhS8fAdaEQMi8OVoT4_M6ZElxS4YYnHBEMsOhih2SkM0TnvKN-n0c8IJ6VK7SWewczfhzzpnBgNqjefMpoDYBKkdzzpXzUgfZrVnndpYAwbjGlsbimi0YD9X4wScNB9gYndcSZ2IiU1pj4c9z9K8enDtZu2-wVPIaM6TDnSxY6NGzFPDdjqB8cZwhlvG6baMQqxmfPIpUTSRntyBZAlqDE2CDD3BaKMWWvSkwWonQgoh3SNiwhDJhDRCwDiKFBVCukfQXM4MbSHSCOG2wFWIUBNiaj6o5oNqPqiHB43i2CO9ZKXu0JZL8gTIAFyboHxLdWsXCqEwECmEdEJkg2mg9UGdr6XZUINe2j1lxYCbAGy3VNebim_ckhZ94ZbqepEUWl2XZFMa_80xadH666IJRg_Pz9cEuWCS1QWTrBhMcu4C6z31sUu03QgGAL3uG9X1aqBB1ifTKtQ7p9S7PqGgLeObVeXbPNUd9W_dFasYtnGVvcMjQtmVoHaHEvRRDCQMSJjuKw8Daqs27drB1jte4q5QEtpT31TnqAuqnpSgNoLayFSVJBw0qQO7Cx_PqSa8I0L4IR4ZTgQVhjpC-QFalmmFDpSCFpoPkNRloMIFM1efQXaybvD9-mTdnttTurUUuUIiwBga0klPviNGW8m5Azv6xbtDGNoOrLnLYB4t1BiaCJ0MUWDEyoERKwZGNHXZXBRtsnJoycpBIysGjVSIwlRWE-kRc5mVF6gTZKQo43fNsBMWGk-5CkOdINvi4IKDC46HgkaUc2XoBGjJcHebsN_7HSn6od-5-lEaWPvLc5eR_zGHX4LM7pF0MlQZGgSd6RtaN8Y1F-p5SaE2SeL9sbNA14uGf75vC8OYztIhp94dcipeUli9JBao3qt5SaR0HIuycizKirEoRTxKIkku9MVaKWihRAJZEoltSSTchT6jptUzZKsvD5BtsT9ARitKYQjOL7JMan5LC6XoG1q4ROiVekrRT2nhJbn1TJttC7RUFnZ2CqGmCVQJWsqn0Kr-ZKgyNAgyiiy5IG6VdmJ5K4EOp6EC-3gaWkEMz1DNpVNLzQdMlkIH9AA1kONnVo6fWTF-pkhnPUgDa2aoMTQZOkjdamaoMcR8qjIRq0zXKpO6ejxARnVrZqgxxNSvMs-rjaHMEOynz1Ig1__ofsgFIa07CKnsfiS46rUvniBH-15PxYisV-A4bcUo20LcG13aHnxuFkW7bANWk9th_t6ASdE3BqxOd9z6ewMmRT80YBVGn_J_LrNQOy6EwSZgouZNCarV_lYdvNSTA5XWxkS1MZNszCQbE9XGTLIxk2zMJBuzv3Y-QLYu4MENkf3bnak0f6YisU1B5FeR_QaK1DkBuN4M32_naask8BRnNss6vH2aoEXfiHfrr49tpOin4t333FhR1si3BhuIiRz36UyLrB-dJrQRSzzvsBpvgRrvZBrvZBrvZBrsZNJqMKWbNK8OLl7crhsY576GsnNuZPH8tY247hZUee1zB3OtLphrdcFca-9OwDUe7BbwXpyA93NaAe_3OZlMu8bg9QLOgXEz9gotffLjWaK1an6lC1L0M10QadO4vyiBEuDVYp2Nbk9O1gpMK8O04YJxG1vMQYBr541PB7pvoYMgI7-dtzSdN1GddzmdNyudtzSdNz6dtzSdd0ydt22dt20d9kIVZnA9jhfkCgO23rutMH_4URHtOqjUjhQrh7Q7VKxozLhoM2jMDokqAj-IFmm0XvOBWm2LKJAJ_1tWm1tVRHsQWtpDT4h_rz30jPj3nmT145UnkaKvPImEIlb9nDDvop8173x6RHdCUfcVZCkNurmkZtw7MHeAwUGMqwliXGGNjYwP3iEM3kcMpPAV5Mrm4q3F4F3Q4B3C4H3XgN0Zjl0Pm9YsxVvMQVvMA3KbOeJt4eBN2uBN2uA94OCd4uCN1eCt3OBd1OC91uAd2cCd4hr61KPi4GR8niBqcVpzaRPSMp2cz5OkUWrTU3O7LnM4eGA1uC4cWVehytBJkDljn3xeP_Gy35rLiYfzCJkD9YmH8wYaBBlJmLwpmbybmbybmbx1mUwKJ29KJm9dJm9wJp_XT97NTCaYk2noHaOaCsJR-Wyvub4U_cxCLzPbMCrv5e8aRjzOkKdjnrIQRyLb0R2-fKWpARHUhLkr_oblph-UUGJgLL-WaNVc1hkHNAISrtBEqDDUvdBLQRQuKWghIksKJS7IEGpLM6GPDVQZOrmrQfUnQ5W6GkGDofPmNG1FWHjDabToZ5xGWvW74w9a9bvjXzOpr2l7e2auRT_T0waLVfZvd09CMkwA9ucxLi52qxbslVTOkYs1DBa8bITEHwaLsJCuTFm0XNGts1JXs_6u3XGtjb_T3KiIkhMV0QQhRsgIfDpIlO_w1pTr5FwMGaVORBg1V-K6dlDVlupb9q9F38hs8jv238ts8jv2X_mW1ep8rSlS9ENN6Xtlri_cqrS1WNoaixZRXIUSQVaOGsskXjmUjsy4I_T105aIfgtkO0LfNVXo5IL7bl1z8cObix_eEjxkmLsAGJfkvh6iYwDTouHs0dwkPiPXkgtbq3-erw27FH2jJOd4e1dBi36oJEso7hDlVihMMPO1kCfdY1GoMjQIymjST7fjkHUV-H4ZTTjWed8EsrtrTTC9pM2BQpWhQVCGL92284znhj4MK5Bt4uSO0GZDocQFd3zE5iJ1Nxepu53wiGLuDKAfELpbEXivfWnCycfuKgYWw2P3JYD5fHt9QIt-qDFLiiSg9uGkaMFjC1GGzHBM0XJy6ytwcfCSwtwfpDAzp8_M6TNz-sxEOTPpzky6M5PuzAw7M8POTPMzbwYybxky8_DMbD0DNW_Q0y2qLpB6y55IZrhwC6sJsgyB1UUiMx_Nay6LWbp5yVDR5wtWhgTer8WWGsEiMSXmmcV7_iVIJT8IUmECWjJDTBwKs83CNLUwTS1MQAsT0MLMtTAJKUxCTNB1hGwnmOOU5gVpx4pvUA_8diSieBJRDidYxZMICCyvCF61HoDAkkJGO2nMjgrTqkJXjLWJCYJb7Dl2R8E1WEBwirkEsEbuL0v8nmoUf1niN1RDil6-5JWTkKKfOYklBy7SeXORzlv1vrNWJycrB8gJhmpfUlE9URDYcyuBRyhDlQlDZcpVmd9UJjOVWZgJFW-gynVVrqtSQaOxlY5IpS4j25VuX-skGQhoXSoLktBhY4e_EPmuAW2pAW2pSFuWWNX2mnJL0TdnKTvuusgThIFXpHgJazFVaZ6qCExnldCQkZ3GtKExR2jMShqTi8ZEpTHDaYWkojGBakygWn-A8NlDw7j3BrItAlFZj0pbw0gH1wNVA6XRoasroj0hK-KDgerhkQYzU7Z8toDENCQx61Fz22H61wNNnRfk4-14zcel6JsdbPO7gN-rU_O7gN-7lVZf79al6IdjXU8K-k356ElBx0fH7hGcJMZX7TXR34U3ieOHxOwCYK8jKY1-36Wb-aKy_XoD7byquKTdRRxvLuJ4G95TDu8px-0p1_yO4-31SC36RiBGfntXTot-eD_o-jRAG2qKvy1D9cgyA4iseDEWWk-xBtpkA1WGBkE5MdQJKgfnWh-XvoRhmp4SJJngYP70iGTR779BHoCWZLl46c3FS287Xrr0d8qH4zxSCMG5mPpwj6CDoE5I4WKI7KcWIg_zYspn3vKAiKy-gaSmBU2C8slQZWhHSze1i4pOv72e_sHN7FaJp_clv1fi6X3J75V4-jeGf6_E078x_HsPNl-_L6ZFPzyDPWENVNkm682MdKsfcB906ZaBLt3pLnp9d9Hru4te33f0-iVWWiUYKIUyQZ0zHQwRUrbadHwsfKmNQS65NshSJAtJ3WQNu3libqBOUIFI1jKLE4DsgeaBqUBenX79ipUW_VCiteyWCBGbwmKDkE4CL3vhZS-07IWXvdyLrJ16bG8T8o4Pglc3EbnIcN_fCxDxSd4LG2SJTyIvbKHVoUReWKHK0CDIyF0il6u5CkPXbF4crZ-HcrQlU6eEXtvsb8FJzdY3F_3UdGnx-lHxtIvXzK3_XriluG391ya7n3QLo8jn177-56vg05ijGxh3tKOHh2X9vJ4LJ3-vTvDi36EXXG4kAIK3_U0VNlQDJtBlPk0YDwk2TkKChLLPVbp8bQFKrwcycIui50rIIOTUceFWSIoyPBx87cqlEtyoC4TbeYUqQydClaHB0Oo2IYVqKlSRGLQK40RkEFKh35gJT0w010BoMFRhpjBXBBUuaKAToLU-653eTgit2ILNlC4IJ0fKEmLGLdAgyIxIINPXQn3thJRQDFdGHlThQXWGiheLlcmMvNDIOyGFxEJaM9NTeHo6Q4VnrD9BhQvivNbDz-JCaLoWbOZmQTgTUpYQM0iBBkGm-wKZvmbqayNkEpJDqVhFGZ7x6DOPvjE0Gcpefla5QsXMPGaax0bIJCSTjEknK5WzK5J5RRpDk6HM69YYmk9Q4YKFq0cZaOS3GvmtRn6rxX6rxX6rxX6rsftp7H4au59GzqaRs2nkbNrp17eR-2nkftpJC97YIzX2SO2k1W3spBo7qXbS6jb2W42dVGNX1thvNfRbx0Xj25Qdin5eUdatkz_rsT_r7M86-ioDDYIMdenJL2UHR7jG0NmfdfU-wTGvJoZnwJLoz4BtYvmpWjp3njtRKKtEnseUjt9VxPpKtS-zaE0bXrWkdUrsth7fZLz9uG-RgvWXBfeOZQU3hxZ_v1uRovWjnco6WXEB4LsLAC_pZzZBKyXXRs-zbzghIs-KZ5SR4XqXLwu4XyaSMK6lLXj-c-_5rwCdfeyAd6uLLiB1d2Gs-w7ejPn9QOU826PdotdnNzXv_hgBoCsk9BqlRsIGpBDSCUnhlGmjHu4O7n6W5IwBj_XltAKhdTZhMk2cvnrL4GhOfH-rMFLwE4WRNZ9yEcaEd-1w6K1IJqS5o3LJUwjphOgp0gkQnj5N_XqUew-o34Fa7bcCugsv3F144T7dYeiOiYvlvYxKAFmPthCdoTprGGNAMiGNkEnIEcqo9JDgFsMzlGhc226kYgu0CXS7xB6hJeOIiIzLRNdbVPWpwwsTrU8dPjtMugRnHPxKpSTUp4QRitrYAWQbVAC_h_t9BuIipQh1PuEyf5rXGkXt3zaKOsYESCXEuxftn5OVu1EPe68zL7TMV4ZLC37s6aUfb8RIi34oRpe5HAkD3t22cNxHyg7uDl6SldKDyKXylICxCwwkl2YyQPeN1watbXl0kXFH6u535MR1hB51TlyEJHnvrCNDxHvne26dxKXQO99za23ZMIf7l53S3mxjhshluEyWiWu0txFS-0s5T2_lPL2X8_QHOT_1rjBegllCfR5WqJcknNYBSwXwu7nf0_2OHLB2w6ORA5Yave08m7eSp_fI0opBvEeW2g0SeuR75jwcemTt9IZljuF6siwIsK0l6Sfe8O6UKcO0BhblzE8JxLA0gTiZqMqZraoo3Cy7HWdxnv-3yiMFP1cefYT0Qnn0MdLn28GRHUHIjiDkkBDkkBDkkBBoEONkhSmT_8_b_2N9BXIQH8gxH8gxH8ieDywBznCTWpDqRVqaruYrHXeF1dp1rLCb4tv_3UGqt9Dm021Axh0W20MFkSdSmIkUirTfoaal27BO66lc3fXWLWY5v9QJKfi5TuT2Wiek6BudKCGpKCGpKD1EHY0pjsaU7iS7EAkpREIKkZDiaQVWxrDnJkvWS0wHS0wei2eJSxFK9xIfIHjvGzPh7QYd92QICWV5YqWlU8LSt3LTVTROkn2qEhR3yjHKeHfKoQU_kfklGzUkF9VRlh0iVVa3ElOoxBSqZwFrTaXofewqp6uSmH9KbHHiNXXVn9ZM7IMha0saMERrN1Xs9aWgrYJ0QozVrEwtllhoENO6Z9aRvzqjmaf1CWleDc9ZdNoQIcZWY2pWY2pWHTVb0q1osmSnuqBiCk-a2id6VZle1V1z3Qa8nm8PULTohzuCVXS8dFRS8HOldeFbhwvfOpo7Z2khrWohrWrh2btUgSLT6AClEWFqMWFqMWFqIxIraecymgUAuVLM0FK1hqda8qDmOlTVlFYJOip9KzLDVJkPhpgEfK1VaoTX9wP8wAdL0mHiT0tKWnrNSqToG1biAqgOF0BVfnvh6SE_6SkUqU4spBML6TG56PHBR--RAGkd9ovM2jzw8F4IAVqRYSyBkep8QLXWVWOjYiX6Dr-HOiE7hMjonsb1TdHGwGn5LnNdBDWInPl0__zFZloDH_iU45ouDd0p_nU1OLbHXO0BsOoGQNoHJGGWw58mD0dCdrxR-e1c59hvPw_oMZxvDGItg1jLIG85trfEjnkpH6GnHqGnHuGBzHAO1PSahH3ELnvELnv405S1qP6h8G9d1_CPhH-7xxreWf7emg3vLj94xjECRlZAAFA7R_CE7YS8x1PCHQ_PtmA-oHgC1HbUW4QPJKsrzNcYdd0UZ-gEkzbxIOHKNPdJwJK86ajDdNRh-lOQSY5_kuOfw2nIDAnHDAnHjAnHjKnDjKnD9GctGfoRWOtJzlYT-Hjjmsl5eHcxD3QX80K6sZHX-KRg2b-L-73fbcVqt9nSphMg3lFq075ZN9lSkNDowoGizhxpxW4FtGaCvUuuG12HPZeXnHdsynspFDoI6oQURB4csU6RaYIc8Ty8I9YFWuu8alrWb-qNqzWLyfqruUN-yVwk734kS_J1-PVJkWuZKXItM4WbQK3YL08KnYhWTbDf9x3QEZnFpQl3TDTr06cLezaT9dla1fRdSdSLtNVcJ5a0Wec3SHjYa2rluNe8Hoxpwk11EFjMBpGL_Jgsy2bcAbLMmci8w4w52B34rjlzkbGmi4w1d2Ssy2_Oc767a6YFP96far-dZJ6RU9Che3k9vduRjAbx-817ep3cnqHTuKfXOg1tmg3IefsGm1_3gARNhNjjLFW541qB18-XyGWVxctuHh5ae18DrSl18aemiz81d_yptc75fHs1UIu-ODb56vc77qkFPxfLHLrEHLrEHLpEDTiWLLqPdUWyMnnsTB47-wP2WxKcmObYs2Z_HJ9hkIH0ZvSLeCCiKXAgotDhRH0Hn3Nmk-PEidmEwGXLbAIgZhMQEXIiVgW2o9f3JifErVKkEbLN71pUF09runha08XTmjue1pK84h9J_V5Rin8o9eujGOmFF9ASMoUSMoXimIJIUCE-UjIh5H6LPzzWrngBLTG3KDG3KH6DmmGYgcTt8HN2MzYLbsYINpsr0YzyxBLKE0sovEVcgqphsswOwIXjmi4c16zOYdfjpcOWgh_vymfNrz2AFH0j2DXaD8pseMurIcBSgKLlrcQbKvGGSryheiaARUkm68OuUpsKEvjZ-DKPGNls2UcT62wZSISWhbSZYOC7ryh6oPlLTBttZps_-5zNXXmfzdGK5mjFDtK0JKN5WvFbMW6eVPxWjFt7bZ-l6BsxbiGtaCGtaOGtQF0PRIgvNOILLSYGLaYRLeYL7eFAW3vARKLxPrrxPrrRPrrhPvoSse5PmWe3p8xr0rpz2_122ylfPeqXna7TI6nfj9al0AtplIKfS2Pv6a00StE30tgdh-nT_Q55Qw95Qw9PGHp8wtCJTXRiE53YRCc20T0RwOoZjklG9yQjw-ADYe9PFKA_UYAeP5Se62FKhUYHR_I2CXSePMfD40ZNQNUbsNVczmHoYcKlssduyULnA4TBBhVqDGWGDmqxU6bCxRCphIwYKVSKEduhyt2uPN6nubNTMKD6imuZGZoI1T0rgsAtvLmyvCR_UvBzOzX8paPf26nhrx393k65yHvTRd6bO_Ie5ve2aIQUcoQUUiXeUkgVgm07BlHIQRRyEIUcMYUcnkJiGxteBz4DT5JUHABCITLnTfOOlOp8tYmyJXk5pLtJYKsz01MCXX9bFsgEnVsqMxNDwBAGlptcLoC4HNWdqFQhREn0_LnjBzeXGWrchQA6GApaxCWfFLFWClpoJGtL5lvOM99ynvme88z3nMfFUpwuluLcsRQxv7caM2TsM2bsk_j5JH4-idVPz8GxMoY9kceVLyQehaQDX4Ga0x_pYVl0STMRQhEjv6c_NiNfKfkxZVLK1bevlOY6Z-si03OXyJWxzYXubmaE8H3NcWDPssHg01ommyuaAww2NiabhdoDVLiuAAqbzEEDOejHEeQ7ghlpQRstKDsZi6YJPdHdBq5uNSkVoebEUnMFEFKzu-n94cfvyt6xrLvkx6bxu2tvedZd9oVxvAUIgOyB5oHpgWjXeMs-wdG-8dYeS8u2NCcH--tbN5QQ8jvKWwQM5PemW5Wtzd2yS3i40dy9t0b6nkKWwwCKJHga7AiwDJg1ypFCHVsvVqzJe-6AbjqsBvmuHWo6El58xadJd5K54ir9Syf1T1P48agYjkT7TcWMaU5-w3nXeQRYCzBjwhLtDW8sKMpQ4YKFRxBA1mom2o9qAw6jHTisA_c3B_nMkifYmisE0cUmFHxjRKXkCyOa_GWGD4xo8vcZPjCiyW5X7zUFYHgg2rHeS0pwtGfdApAi2BjH5Letdy4D-Y3rlidv2VK4dd1y6i1ewuMjq_CNDVI6A6nbxyPX3cl7BBlNlMFMSUPzTtpx3phRibM8YjnAWlBfhBnI73Q1V-EGAshaoZM2kTcW5LNmwsyJTN3p6b42G0DGIJz9ASpcvXVdJ32TRRtwGJmcs7971-Yu-fGtkXupACge6B6INpj3GAmO7prsBUxO_06_rbxXx0B-O7or9Ap9htvPvUz7Bb5JEn4MEo9j_CweTvzgmESWKh9v33i5y76y8dkT5XyEC5MPMr6ZCWYOX3-8s1aY0OyeenVA8_VV3evZ8Be0IqmK7hfAxLhINohRJ1wKy5f7VbwyHqtYsRSBj40BvVpxADUvQOuRtcXEIJtsOPDr_oUoZb5ejf2awnn9_3dc6v-c9PWRjI0sOrkewSy83Pcm_QQUCfD6PQGcuEIVNpxdwTLOjUS4zT_WcvhJDmu6PsajNroAprZc8p2CcWtDU8K-rLoa9SVqAz_Up33B7_lpPv0KIrd2X_p1rUW1XF_iwXjdIl9lEa1LxBWSQC8IbSp-D5UQrKlSTSqpUPCGomxQmwg0lFQECioE5XTw9zfowkWr12JLfExZCMGSqaXqFD7XIlEzYTHCmq4QPMWYlYVlYxRqEoxb65oS9qXIiHxfojZW6FejhIJlO3rBuLWVmxQwrOVSnzKzF8N6ObRWqocSytMj1ArKDmaz2MonooLZDLRyNZMLkc265rKrTc7veXJa0mmLFkrfNc0w-Qv7miLMV3TanmspLbsFCGtaO8ExsUXZHU7MdwrGrQ1NCfsie9Tp-xK1ce0PWzF9WVgy-Zpg3NraYQ5qDWoRaVpL1HP1EFwGc7kq5VJhesY6bokwm4FWLhGnts-9G2aSr1cZyJSDYyKF8LOuC1oxV1th0eynTmu0kP16vN06Lo5gZXIthRenD035of7kly5s4-JLKACygivwbG-wqP360MiycSZXmbCopqBAaRtHk6tMXGbMdmNRPmhB1h6LKgQlDdRxWTucVFmoMjQQurQuo_Fa1VtvN458q4uuA2K6DiPnHyRGSpiVW9hW0LtFuUHWsUXA7vIX1k6DTckX9yJrKqWs-lu_-yJ498ZbpmrF6M2omRKG-GAINVOCE-MSSoBd-CBSOlYg1Xqt4XUhwUI6V4jpXNl8FbFhsNsxRXMlJW4CKfoioXLRyyKkEo6YyK7JNqGNcZoN4vdZwPzavn0buR_OEKSs3Vye-dqs1B8_GPRV9vrW03i1qZWyn21q5zUrSaOBXuwgAVRgN2ET2lNCfko4HhKQKtqE8pSQHhLwmz82oT4lnA8J6WnkeE65li2tpywfLvlatm8D-_myadn8Wky13c9ETcRlBfeUD1vt-ZFIqW6baRPbT4n5p8Tjh0S_p7CJ5afE9EOi_5yATaw_JZ4_JPqNuE10MyTLla7r7u9ETMp-JiZJe1V0rfX4xCak3dU7pWsKDsSmFvB1y4KndH3gpPW-zbXB0jgIqwyJn0gpC_bVjjx71JSVO32nmEePktJzZ6wF2GRs1ABbTEAW46KK43tv9Ht9VyGob9zDIYXXtu1LvOSFdK10fda0vJKsVTZ9JFla9jrcub5J-vlErLLpM-N1sZUkkXUvnjMrQhfPsbkyQwdBi2xZqGD1i_uZuhZ0EFQ6Q4WhRND6vK2FrrpU7yTC8KV3RiElYWRS1RWCNlJxCU67lHhpl22a-5wTzxIeMEmd7dq9W4VdX0JcqjyhLYkMi-smUGPoYCgTZJZSIFxKCUB64sSu8Kc1k4WUhJOnbwU2rXk-pWxza_uS9g5Rqm_rdELsJWJqL00-mVHp2py3CaxB5ozj6CnASoB1xkYOsMN0aOzFR7-Xryga76yTlH1lnfJFj95ZJyn7oXVa87CiiFaUX4EyQcZWCFS44MG5EkHGfJREulAS6IKBEurCCmn6fex62wWXlO99uEvuLnkGqZtVGZtTlK0RVBk6CUqTIV6AxAuQeGrhCCethyCiN6KgiKmCmnyiDwWMoVFQk1kUSrDOmCiZxXKAzQBrpkOHtLut85Fg4N-vCAqxEFGuulHfcyQhYydDjaHM0AGmuK7viuOiV_3UOEEnQbYTC2oMZYYO6pfRFIEKQ4nqsgUXVBgyKlazUwWjKJLKWrbM2nocZHyVSSjZ-Z3Vg5Nn-eRZPnmWE89y4llOPMuJZ9l24iD9vsP4eiixvCWWt8TylvCBXFovbvfrUMLyk3pu3cDJa9eRTL30zUIHQaUzVBi69F40QOpqlCsPhipDJ0HnnCBL7RANY2stSR0FcAlSy1enuhckaYA7e_L0nDw9iacn8fSkRBBPhSFC68FQg0WVewOactyLKlZ9vQvd8dmdwdSqm3za2jKV14waq24yixVe2DgCLAdYY2yeQb5pOtS3TCPtWqt4vW3zMe2SsuUN7VpP3GZ7Q7ukbPmIdsmidn4gazBdVJNP5lBe5uVFNZllERbWjwDLAdYYG2eQzyzqeh0ZDZXMkLzZfBxvlnUV9iLxu2VdH0uc882yStn-0bLqVKyQnm3Lt6y3vFFr1hsxXW-TT-s8H5TYZJb1GScrsWA5wBpjVokln1lveY16DzIlydzEb14JJhDhV-J6OHsWgBpDc0NnKTCpgq2LbFpbToi1AJsbkwXWiKlbKvety5T_i1SuwiAdu-z3uv4olaO-l0op-5lUypxKwFGcZokoaqCBM58hW0ltvzVhcn-nyHPC1LDiXD1-37lePFHiE3UstISxzwAbiJWE2GX9w3zr_oZ24H6YUqADWqDPXYkdCd4c_wHT63I4b-Y6ms09uAY5nBI2MOEs6M5HT6lsStgePUOSp6O2BdEWjUCLGqThag1WA0yeut13FVI25dtWhOuo4lMlEkWYXgF_owha9mr3peZLw59pr5Yd8_2A12XKDzu91PSUcLWg56eEojVQZyhZ_TaFGe6hNZDscg_EYqn6vO7hGVoILVTAQjhsIHYmxK7LN2G-NrEDXTvAFuI82vT6CD12WgdVUUoiTV03GKRLciE143Qba7Hnj_OdjOHb1jd2X721vQrejlbReEzBkYhlMSOxmL7W17FutCKaz2LpCdvWxpYP8B5ap_O43Ec5isa8OKT6_IIETiz7xlB8ddO1-3tDca7XcV9ZRmn3nWXUhj8bsFiOdJDpSUQUz0RE8UyToSM0ThLymuAWwzM2ZRp9uhIG6qS4vgUbmTKNJo2mzGIDsY6mLK1ThyDfmdCUJb3iGJmypLfzQZmTHiKSmqf8mDI55cRFne7TA6ZG896AqTFMObyxsbVFBjW1bYbu3LiKFhuM6fPCjJhxAWn61tXgYW6LaY_MXDV4MfnOji9Yix3TqOJo2xLvhVR6LDYD7IjtpSgN4-0Bn7F91fxqX8VkLEF-Z-dWle_snCjQOzsnDb-ycxIf3Vubs8bwiGHayEkFFsKNXMaSkRafT5sKqTpM0ZdqH8qEKXTLzdYW6fb5tI2S2kyKCOLJWxjtlcV4W6OLRMItq8S42_KInJzneyd-lvdOfLX7Uril4VesZc3DywGv9Xq3vcnxLiXHu5TcY5j3SJn3SBn2SOpmc7C9yQ_bm_zT9iYH25scbG9ysL3JwfYmB9ub_NP2Jgfbm_y4vcnlSZc16jwFU9JZnn7rcX9pgPLqu3kZ2pRLkNQX671zsEXKtF2xKThOdcDYD4tZp5yDjUwONjI52Mjkh41MftjI5IeNjNbdtmqt147fbWKk7CtfV2LCXWLCXWYMx2S-8E6g8E6g8K6iwK5CVUKDwVfCmNMX_GKo19-SQShEfy02EDOcviTm9NIxw-mL7hEi_TXvzubkeuwlvjCbFs5c8pb4OzfOksUGY5YzF7idaFuP7EppjynBXsOsfjTK9nNKZAvKI5_HmbGYOULV_hgdLwEZLwFpLwG5Lw8EvTwQ9PJA0MsDQS-OoIu61Jiv1piv1pivVuarlflqRb4qwl7v1yusFlY9Ko-0sA4QQNFCiw2sfKIW1uv1yShfzpM6pgJ9wjDzfh7mEhLL2R6H95P1ZrbWz90zQrVYPc7QstVjDcbPXRcdsbtPrMJidlN7VwHCVgMSXgMSXgMSXh9IeH0g4TV-7nC3t587fBHL18fwZ80vTgS17PHmCF87nV8cJ4pqtZgft5gft5gfN-bHjflxC_hxu195CVxWO-Cdfy1w08caFBjgKHNyLfgzqFYgxbx1oUlGhxtS-YxDMpEFTEqkeC04xddPInB7kXNqTKHNYCPnhK1aTF5zNpi-l9uxPaODLSCvLSCv7YG8tgfy2hx5FdnpMdfrMY_sMY_szPk6nyR35o99suT2-1ROBiAupuORnLgYiw3EDNGTjwUYUtfvs9BAQ_oJpM50zFLWfh_RWmfZ9bJo5Cw7vEpyj8RgA7FqRnJR1jDfNKNr2oFodBXeETcjKXh7BUbh9WFXTynz5xR8b9PWFqY8Usz-SDH3wnrD1A11NljdV3zs6COdN_WIfvWAfPaAfPaAfPaAfPYH8tkfyGd35FN9sfahIXaExLP75y9maNCkiMy4OZDD9UX_SPgH3sIQ4bfYQKyh8I_riWGULx8o_OOmsIHwjz53JWYk9tRi8DWQON_5JPB7HigF2aOVuvuLE0ESnf2qQOKXKyxm3eqeGO887xqCZh-Pjwe91G2rk9AEpvdo_iOINyljPBQkPzTiLdLwF6ZErkdAgUdAlUdAqccDVR4PVHnE59Xrfl3Pb2jxuHwUlP2AFku77Q0rlnbrm-Pq0fr78V5Bc16Od7X7bryr3Q_HK9ZmfYNBrIhYm3mfeqJ9FJ2STwIERmOWx5T0c0qkuzNgwSf0YKVECcGFlEnnuLaZ6En1DM64TUqHlxGaGQ6agsn7ocl3qma8G5vxbmz6O1WixDPg4TPg6zPg9bPHhmE-8Pjpefzyn_mAd47Ef1psCVk-bmbKPjAfGFJOsVXJenBhsTNhxfqGk_X6indTZ4EboqZj8XXUbEKL6-gQ00ruL0dEoxtwpVSxVcmYjBUzivv7Xm50eE1kybbFluPL96cOvCZoSjU18AcwTErgWqUe49d1sTvpkl0V0xMTYkl7N4J8iVsKTI30-IeUMxiLvu1lT9y2jBEWzlX2Kc0UAUOgeQ1E-1JdFwOFG2Wpj-Fwoyz1kmHRjmaD8Ubhlk6D8cZD2_FGRbvFeLyp0EE71vKVPb_14vmojgH83otru2-8uLb7yovndB1Rm61_vkPxR3bMxOQXO2axgViZjB1ofu5PEjizlPTIGstPOCIw-fx5hOiPqUTsQTJn3idWYrQyCOiuKZVSROvvby44rTffXTBYsLuS-sOUpx2ZpEQ2KtXHFD27fypzkt1JtK_R2TAQ7WukIJmNFO5rpAK2Jon3Ndq-xXhfo3WSJUjxvkZ77C1ESo6rf2Ah1v23dxZC2n1lIaTdN_sambaX4x1v9mKi2Hfo_IgInQeEkxbLdELAB7VMBtOK9bw6rDjDAaJiq5I5GavcYzoyFdwcQO68YIVOfO5gTiqkz5ESn087pHw-7bfy_TEHTimPKU9bJEmJjNKeaWdGTtrtaMsGot2OTqW3F2e425EK2IycvNvR9i3Gux2tk8zFGT-duBf__v7lAQL8oSkQlZSyr9R53ch7p85nf31Moe2-Ml_S7ofjFRnMx5MW5IB2L0XMSLutIma8gGMwc5dFMbyVLZgYHuqIUaBj8Ym89i7l-4X-VUQkO-P37lagU8VW10w-MT0m3zZTzUwHals-Yv3JAanPMaHO-b285fZe3qTdV_Im7X4mbzpl5lXjE7F8r_u5fEge_T9XSv36C1XkoIoM663FyynF23_IwuZ495b9Ns10sZgvmble5pH_A-dWtmcYF9X3L4_zP1iveNC8nvetCOziwkVDFtZngA3ESgcvK2L9tSKBW8_ruWBQyYkPF29MA7orsd-4VqwFDm0Vtx-mtlwzGQbBrDY31lx8zpALk-_C5Lsw-S4x-S4x-S4P5LsE5LsE5LsE5LvEDxW0C4yHDxVy8Q8GMHzDf7Empbqt_QfWRNrd1iR9mRdt-b9YE2n3Q-8lDkDXV154ut2ACFjFiyGK3bz5VkhN6TvFJtwn5vZEtuIdE9Ga2iZjRfb6u0lNOdPujBgOW-hIzhuaMVls4Hlkxbs4xrFXvoqjWOda1Zdjvojm1kcCjPMapYR9JNIs2leZGlemxpWpcY2pcY2pcX2gxjWgxjWgxjWgxjU-8NcuMO6p9JTsjjJ8oOT1DxS1eor6gZLXNxRVnmcb2bSYuAQxBjJfZ9pvQHqD0NSPI8Y3Wkxu2LCK3jeIvKrqKthkHW897fqtjkuheuu46ZLRJdMlcz1JsRx30z4NEWw8ddN-x9UWKt4UtceD-UYH83cKD83UFpmJ-2slD7WFKffDqri2MKU9pjzdh9LaohSWKzlibfjxcM2NC2swa-obf5xLxShxvmXkm1kuNIqNH4o0fijS-KFIix-KtJhWt_ihSHt4KNKC_ZN-ZsZgwUORFjwUafGNKu0t4w8PS5p_uLKM24pJ9s4gt_76Koe2-8ogS7vvDLKRXzG-KiMPhrfDybwxO-arNSbzshFBZvPp-NwfT-k7ndLfKfRZCFtbZALuL9Y81BamPD6P6I_PMPrjc4_--MSl8xMXU1vwhLTzZqnzZqnzZqnHm6Ueb5b6w2apB5ulHmyWerBZ6g-bpf6wWerxZql7LvSB2q6Lke_UVtp9pbbS7oebJVGroRfoESukCSY3b3oGfk1Y9HJM3vRIvmDTc33gIt70SCHa9AzetGkKD8CUidRyPG6NxuOG5P7k0lNtUcrjyf94fMIweIMlG7pxb7C-6a19gHov1ta8wRulwRulwRulEW-URrxRGg8bpRFslEawURrBRmk8PEMYDxul4TdWU6Ty_UZJyn6opOILpZ8_bUQmv3WpKUrmnN5NIHSqLrOnXYvVJMl--q3FDPYugrXJnQFObsQ06L-m5MeUx-3CfNwuzMftwnzcLszH7cLk7YIo15w00BMT8LV4wYIblJpinoxooeAN3BNT7HNNk8QXInUyUXEnk_nJZH4ymZ8xmZ8xmZ8xmZ8PZH4GZH4GZH4GZH4GZH4-3HCaD6R9PpD86TYFovjzfHFCIgZHyr4j1RN26xYzr5htaQGDo8scG5ty0LuyS_HLkSgChWQOLg6X4ynOTOGvqdnaAmsgtYUpT9FxCn_nzbYTptD7us0MFBRBR2ggYsBS0CuAFGY4ZsDafDYYM2DtQICRTGsXGA_v6pTr3dh3DlKqfMWAtd03DFjbffNw_2v8rx-PaNkP-ywKmPBBgWJ8Qd_kpvcgFcd3Hr_EajK2WkvEvEs6027T8AWtvDu-UBK_E5Ce-q8pwc3_BJU9p4S1PcWQKfsTfe6ATWcAbxcoNryH12r4BkS5vx643bhixmTi8jYzO2g2ErFvHbWBQvYthRmO2bc2b4xGYvatHQjysRFJ8eML7YNzpF-y9uJ5oiiqlH3lSM0K3Rg-IDtwJX9i6eXEG_eKMb-1uT1zVxyZu2KNdfQsT5xeC3lOX-7PsXn_fg727-dTJJhoZJry9DKulAlTniLhmO5avT35TQnFzgAbjJlDcGkpsh1nfkwJ4vRkaGGfiDQzUFTik5i4FjYQMXGpi9T8DJm41MlwzMS1o0bZT2bi2lWLMRPXdsggnDET11EwHl6N-lLM9-zkfH-TWNt9xU7ONzeJ1Ujd8kWYaIQYLpWuB6OV8W0DxZi6m9xMNjJeyRdzIjUHxGJdCQuJhVTU3ZFe1CVNedyBZI69Y8o8p0RWLfMORDQ9Y8AhsU3ZvCth8vFxoeYefEVyr89OambgaBAy70gy70hyvCPJ8Y4kP-xIcrAjycGOJAc7kvywI8kPO5Ic70hyes_Qc3l9gUnbfaXz0u5nOj9RqOSbdAaaBMn37QxUuWBGqD5B3KJ8ZA9z2eord6JyV-uuS20Yao7FRMMO1KYfCVmBTYcxQOWgsC2l8M5Bc1_vZkuKmCvRzvK4rSlP0YOkoeeUyCyV8pjC2xqTEpmy8rjlKo9brvL0fERri1Lo-Ugz04ZGqfB-p_B-p8T7nRLvd8rDfqcE-50S7HdKsN8p8dMG7QLjbh8kki9fL0WVuT9o6iGjMgI1hlBvpTeNoUmQbbFwvwopvHR1cPWmE-kJagTZFhNXn7iraQ9IjYKKzINBuL8H6egLfjpStV6w7LdPFfYCxkzU61qreWBa8DOVruI-SV_q45aq8i0kIRr4IdE7N7Yq5AO_QWprjWwHfjhTTz4qbtEME6m8W6m8zam8zam8p6nxnqa6PY2qVw12KTXYpdRgl1KDXUp92HVUt-uYplvZfnnVJs4fEv03W21i_Slx_NRmc4koK0Yhzbd2DTQJMtonUGUI1VamGJEWI4WQTvUwIt-ERYgrspky1ZSpWKY-Zq7ooIoOquigiuArsspuUDUtZhlKfYqcYFMsQ6n3A-KHJPvk84R5-yHFcKGBk22-kaZY2S9pmWruyFvWnjTe2TTY2ahOtmAH0nAHMgF6t0v4w4W78ocLd-XVhbtppuq4HZTBewyXGE4bLjjnqAjt9IqwkEIIBhIVKAcYRIo7cKWtG5XyBx8vtOuBkdUGU-j7rSDr1FtlBdIiBeLAifjefXdi3e7bhXTpZo8NtPzOT5h84Ns0N_MWkO4fqH0g1L28F2pp95VQS7sfCvWagkG2SuR00KGtTRgPCcilbEJ5SugPCfi6nunu8rQqY_LlRgPV7R9WjfOWIMMz7i9YOnjE8Al-HiuwEJ8ATJBRCw2CcmKoMNQJKmg8pF8M2YKVq6_cicpdrTyg8QDZ6gd3YnBXB7cIwU-SWY_V1Wvhq3wv70IqIIWQbzsui2tg_FRwvT8OyBgyiCqf7jNQ39DAFnoEGTkTFMTvhFbMkaUWgGOMiTgEV5ACGQuMoCrcXGLtFqKtsXZ6EISyVO8PR3oI119zda6-IlQ4V-G6CrdYuF_9CeoMFYLkXaa5oOP71mKZL3xIlY8yzhc-RNq9Xmb81IdIuzW9IEY1HSRlkpCfEg6SqJQZaiRk6aAFuL_SB1B7ypU4V-Lqg1ydICNk-s04qiuAbF15Q2phUoUDD4udjJmXlRRrAZYD7AC7uKBOSHHW6f5ooLVOCXmBMTYJiYGkFEwxCzV5juB1Qe0D8oApUH6tdavsh5ojLufc9EOmTb6NRsh-vGfhCQM7bwZs5vysMTwsrKt74sNHi9UAQ5t9EpeoJ3EJ7U-nghFUuKCBiCUo1AkyQnASHdOuGgjomCz3mZyB_v2lGCn7oaHUsn8Q0fONiF47n5o3KRJgc5O8erY-udX2rOUUCpz_xJvC3cID21liKIQHoe_QKh6rB2fL6b9hNcBQqDOzjsy0ILN3z8wBMrOOzHQlM6nJzE1yJ6HOzL8y86_cSajzH4Q6vxFqsX-lOekq2UvX-pBT24avwB0LCzUnWeUIBe7-4JaD_d2vjLgVndJYdArcaDLDK9TT9R78cnDy4as09bmILG9htlKYrRRmK4VJR2HSUZh0FCYdhUlHAdIh3rsEPjphij_IUtyfe9UCe3fYuiueKT9zwcJcsAAXPI8DRebccacm5n1lZst1V-8d_17tvuPfq913bkUiLvxmvCkcdJ3vB137rwedwpGvxj8c-TpDrG3vuxXpt7qeIwFkMlGcQlPa3jrESvSrFZL7MtUTkeUbDHQVbAx92xyxUqb09yGqPiM7oR35clFmqDCEBdXZ5gArhIkDhtoMIgWnDGT-TfBWBW-ERxas74dFisATpd4X1BiCp1zjAKggAp4N8xhkOzsVlX48QEYq-35edkONoUnjoxfudEj3-3ZLeszgGZ4WNsNZYm5mwUIm-gX2VC_m4YAclgPseMSMwi1soi7pd64M1gJsde_wOiddZDw_4IfF05woGavNjCsi-_OCw980R55ObHwrRz_cbuEDQrfKvnMnPf96lxJrda8vmKiWzb_e5sTuZDX-4ciNqDs1zQqJ5gx4Uic6uT4ug6-HagI8MpQUaWrsJxSKnDFiujNOMhGjPkC24D6TMOMgw7C_fyQwDtF8Mcn00hqDMQKsPmKVh6jXfQbOjdFo_VKRwQYZCOkKOVtphhRZurQV3wwpgfdbART-oCfD87YP9GQ1_gc9WY1_SruWjMzkhUm-loJIJ-R-w8sI3P6sjoW7hXXdVi2GT60aLMSsSypsXhCmZ12mIcCz6W9Fqz6LVRq16jcOgjT3acwNFYY6Q9drgzCj7TiqR4DXLM1th7IfvbmEFqodQkNs4lJtLQqdkPzFQGQ1BeoGAiaDvfcL3-7vtDh4Ohj7_w2bCy-2VWN0mvnUihmWYRvSP5QhyTYNxGRDVyHAyA5pbxjPD_jhxFJHQKxTqgCT9ZXpb4S9HZ4b_N5kSePvTZY0_orUfE3_30iNVPCq92aFYIOhckIIanRCuiFQZWj4_YxCiFTfgVRJaBIxDMllofEAcddJk1ON4WFhW69V18RcoqWTVTgxb9AxW2w8YayA0jbjp8OzmYUT_IX2azp_sfEtd-n9sa6UfcWFtSvVI7jA62sCaNPXdwdgv9rO-7aF9y5n9y2chRBylgIZ0Ts3-7ghTz8kk-e7ktHz3ea_62E4b7s_dOFHY0XyxKcTpq8OS4-YEefzjstlRe8sFhcuq3UwXm43dkN9ezZBDsdOP7WY5_n6VFEa_4OvWI1_6ivM3KPw5MNLes6EMPfKMfcSkcn-xEmrQMSfQUnDBmHXn-nAScpZiGlaJpp2f0vC6s39HQcHTwcP7OKM1SnTqVPLB0NwcmRmx0LzCRpccPDQDVnXfCdPpMOORww3sjoP3WukTDLj7QGfP-Y_J-fFo6lmYvkXHOsOYKxOCb_icde5T-LsENCCyKPM9xYk5_cWJKc_WpDV-EsLkveeSBSjMI8rzOMK87hCPK4QjytnjHApYyUKc73CXK9srmfH4nW_uFuzovsFXzE3el_gqMdCgzpuoYATFvNmOI4mxE6eCH1Zf2B95Dxl8KR6MkzGgYO6vCcoS2l_O02S_r5TltX4H5RlNf5SWQrtHCoRy0rEsj4Sy0rEshKxrEwsKxPLuonADTHXrMQ199ctjILU-_GiUZD7AxakIDWxnNeAaNovTxis8nisS6qbDN5Qf4B411MfKGZ1lFTkpLZfX42NpbzO1_djpfE_SPlq_NWZaWvEIBsxyNZApOdBCdk_sG4N_LjITCNW2YhVNmKVjVhlY1bZmFU2ZpWNzwMbE802H6Dl_ByWA6xVP7t0ptDcVVWFp4MHjMU847DDtArW-AmlYEZzGp8ZyvRfH8PWtpU3U2Vb51S3mj8azGYSzElDu4m3JXU3jgM6s5OcDqTFQJ2QoBjWfUdN30PuN4uQl70OyXpF_oHPyWn-KlVIfoWH6kbBNe5nuPS9xvCwsA6k0mCJe3X_JE97REgJJsQItoZeDzCjFH08YR1JuvYDmYdUNybl20fKN35aXKzgWp_f2fAW2fCe3zOV1fjvbHiLbPhq_J0NH2nPu0KdofIAkXkaMR8YFHpExGgkL1iDiM0g8jOSF9lRCKFrU1LMGOhRGOoMBaRlBARl8JUqHaE5iNXg6AYrsWnU2Ooo3qM_5E3OjArspfNT8R71_a51Nf4H8V6NvxPveZDgTrxNtaZ-wnGAQuyQp3fImFMf1xllmPFzv5ktLAI7Dy_mkxjMpAcuMxPSYsTUQ4xqMnmZB0N0xUsqDyBrzGfAhGbAhObBajUDqjKD466Jj0ZFrTR2OaqPhjg3WGPPMt2DzBt3Dz7NFKYtMv04vRT1ozI0GApdvRRmOHT1_fC8RvtzIFQZohMbgTohaF619mww9v9af4Zl0pH5Kws6Ed6Ra1vo9LUtizkiYCeC54GngfIUmtBCeZY6SBc-eOoQWd-vRl5bX2n8vfWVxl9Z357gXeNlLbvGCraMtyflHA6-aUPl3JHJ7cnRElmV5AmDICjRyVMI7SwidIdFB5kD7DBYecK6gYh9CFS4AWP9dOiDixqLqPm8VdOxoVXU8pzXEQ0zqXA94wv5_SWFUPhTfn3zUhr_g_Cvxl9dz-jJH-t8PPL5dl8gunD685l-ZkIaIf5xmdRDXufMMRweUEi15KNOT3qkWoN4-iKVGTU86SBHarIQHeRIXQU15KQjD6nLQo2hGXsxmRPGc-zdVj34LEoX4c4qQnL6B1K_v0shZV-9ptIzXRnuGVy9Qsxssmc2WFijPBkRyeSrM_nqTJ45jxjphBgxynRBWSBjkTM9UtI-GfOe-VBDMUN-MjxPubG6-eiNjU1wbFkWoewJlcAf3IQPrVJ-fxNeGv-DPc5vbsLLq0P9jpx8YTnACmEinXfsVoDgHqyIdsF3gw8o2Akx5LqUJygj1J-gxtBBUCekcDHuAfeckEKlGDGqUgLGVDAsjWLlEZsGg2_UqVpIrNuEJlSWrBisB9gqO71KSXnGu8VFWMv4_YOhUNVWBX8gABiaUyG46SGSjBH84EFMr_BtBjiyk1qN1NX8BKG41vYEobhitEYDdc7ENXEPKvWcShWquVCeTjUzYjt9PEGZoE5IocqNT6r8rEP7YLH2iBm1qvMRM55KJKqgBxKRsthk9RM5KwFGKiV1Tsd2qj-n_4DtrLLv2E7TcxynIhpCknB1_CGOb-nKEjc6qml0stCI4zTiQY3OLBo9uFIoB5ihKi2gNI2ZT6OL0wIZTt3gwObGAo7T-N6yThvRZwnFSNIjsRK9QW4rENWYbw2yrMCu4APu03w0qk-5z2q8ft9__lh-u8Zks-LYxVt7mB7WmPzmRV1JoDj14m46n5d0uoAikM1F9KPzJRHt6eCSIVYNxjeZFcNLU9KG2eZ1ejtLSmaG9u0pW5quPOtg3MOb3kf-m9SuCt5InqzuoM-raoL6eL_sYztaRTIhjRA6Px_u_qbNmN1ZwqALtgpVhOjOrUJI3EdjaD5Ay72aghai-7tafdCJwbkG12Wke_BNXV0B4_hHQBAGMLcbCwjCQIJwrA3V_Y7wHTof5cXognSneJmXcKlzS-tYkQY_s7GiKqvshyZSy47fO4foGpdU8ErNllTPM5TqeZJA3ZFfCaoMGVGZ_EAKIo4qUgkZD3m8Zs4Rj6HyGAZDlYdVeVh0d7ZjbFULVYaMmszKoj7h0EoZhYSQReoq85YcRZUntK_kd5V9J7-z_N5NhPK7Kngjv1pBe6-5q-yn5OaSuQEBchXphBRCkhPggRkzXJjTFD1s8VdaB4bPNRCK8jjKE1QR6k_Q4OorQ4OrRwMwDj5n0TFH2DRYf8IMY5I2UFkkm4WS1Z9T4Fz_JMdSwXs5_nJV860cS9lPNXgdRm7Zu7AzwAphIs3JP8YZyT_GkTz-FQfJ6F9vGgkeFQniX80XhGt0r1eJOCZ_QqNNI0J8TSEU7ERUSSFTkNjTSJMhYmKaC3cGghUD0bMk6UUAJQPRNWQd5MklHcYvb2l3jQqm_WxK9S3RMyXtm3-_Q9elO9WUt2Xfq-aq4A-qmcp71Vxl37mY8_TKdFZC_DtFUorU46wxPEKtOf3TLy2PiD_1kVJG0E_iTeMk3qQFKxesXNBoCIQIvqHKED1f0yZPLumwwXK-8hmhlsnwwnvWv_EjqeAPwnv21_xIyr4T3py8YObkJHBNZpZHNeYOzpC4rPYuuuQlIc49FOLsHx0JYkQvM6nJTJAyE6RMr66PzIdFI9NZlI6CISOgmU6stK8nl3RYQI-k0Q4vDavs5h3j54bgXSZBxvybJGcfmPPDneqAcLiK-MOcUfxhjpZqTmR8lFxbnmA4WRqINH-mLglGLArTi8LEodBx0Ch0HCQdjKDKBYPqB-caXJeR4BKwhMIHPTrTFuMnQTrH-OBUyhqmXiZDB0Or0WL3w6MkZ3c_sHqr7Kv98JA3MN9rCb3_-amWVH-MMqo_ahnVH6xoHi_3tcawi8IvwlPpgEgqsNB4gipDg-sKIFOQqUTlIykdsBHMWgNssLDWfSxzQ6eKIVnXVYMRWqnUC2093vPMVfad0Nb-R9O-KviD0DY6TGnJSd6a5-bfXh7NB8yTPP7tecmokTOM4DbmAI2ZQmOH3wKHz4GTR2MO0JgDtMTi1wKP3wKP3_hR02jAWVTymgt8IwvYjvfmsh3vzaXM1XvJaz744qf0uM33Otfmq2OXEwVgHX_JUYzFCmEi4BirWRB_w3Z0OojpdFizIyAb6x4HOh4-0LFp2sc_HhC7WRF_MWZQWOhBYaFHZzrFoZUHh1YeQWjl0fk4hSPVSv0Womu-Iwi1PIKQzCMI3TxMiGfxIDt-MnmQzmcoPT9AFEBgUMBmgfvfnjxJBX9QPVmNV6q3yr7bmQ46VoHIv4rQsUoc5XdQlF8sT1pBoYkHhS8eg45VBh-rDOZCQajgMfh0hEMaDw6PPEzsYFPSYYMleezTEQsZ4ZaFHO2PYrgq-IMYDk_ZPxDDMd6I4ZqTSS88j0lnJhPitwzIY7_gajL7Fz-lIf9y0ph0OgLRcxVh4jLpTaExmfFMPs-YfJ4xOVzfmInFeAaPlCZQLzV1c1-asRBbxenDsAg8-9_EcVXwngrPg94dnnfgYYAyQ5OhMJjH3KF-LRy-Nzx9vOAlGvPwDn4e3sHPwzv4CVGPTR6D-KuoE4K5GgQFc0Jc5Buil4TnkRmiF47ncZDATRPZ2DTgMH4deAZxjHUCLZatVIq0ToyNfGPudeAsM3M6g_p7cyZlX3nVybF0J0fcnRhe1-QisYuD204f3FbkAUKqKuLvrU6Iq2uQRKUMQjdZJ8fsnRzZd3L835n4IGIGEXh1kGjbtK9GBDAq743VyLDN9MEt1MiwSQXv_awuyCvBTG9uocpOy4ih7LQcVp7E9Y656yH_cvA89W0a8xhjahjfCPWkcZ4llO3TO-oJYVUVoeOEeZLvnhzed0Lk4RuieCXzJA8_T_bT82Q_PYNwvtOE8xXTd3LAEm2DzCHE2LWQ3-XM01vDT8X-PP8o9jLnr8R-lX1njyG6rSL-acmEKLkimRru1jx2mzk8KJh3_FYHh-cHM7vzA5Fcjk87OYrtzLTBn5k3-JMD-M7M-_SJAXBvjPfpM_M-XfLh4a9m8xxTmsbnatqKF9Pst8KfimmW_fBvxLRGtLP47e0a2h3jkw5NJwcYnSXwZYV3khOjiWr7Pmj9kaE4fprQJLQHPI07QfuG71fimFEhILSr6VdCT1zoVqYO0h2rzpJeHy5K2Xe6f0e7tBp4B7d0cI9hfyY_IfinKWpmp9KdO6nJQvSFvoIV4qmow07C1mf1bDZp5a_Wv35g_Wtk_esfrH_9g_VvZOshLqXodqPT0AkBLtW4LQjfOpGqrvU0lZMI3YEdHeyjRmKfHyoxMibNNXv-MX3sQRknBFrUjLpXcW-QzDtuoHvnZN4hE31rFgrjJ00fKlEsEsU7nBTvcHK8w3mHaWTsYMzQuM67FAjDeEN0HDgxDmXCWcVTPh2nDwqgvcMISTMInzjjMIlTvoL3XovpS3afanH3b2l8oMV9vNhTq5nr6DzFHFqsECZCNxKJ56BHSXN0hmLnMWLnMXoo2xQqcUIURIPgq8oTIz4qRKEFJkVmnBS9cVJkxgkxF0093MlEeYzKDAiJcGMlwDDggKiI5DNiL_UFGKnC-OPz0kkfq_tUFcb756VS9p1DmwcJ6jxC0cPgixYipBNirN6EF-e1zczdaAzF7nB6B7dk4g76aDddc8bDyzw8CmcwITyiIj6awaRojHP6x6-TYjhOig45ITqjQQrVY2eXQzPOIDTjxDCZN5ZZtWZjNZJ8FvPRkgT-q2rNv6rW_INqzdeqdR4QZfGSyC_kficQjq2-4Js2Wdht3bTaGiiB1u3EWuEtMncnEKmE-FBKChUDeY6jUDeQP6-9u3Bw_Xg2e3cMzmF39-8rtF_Q_NNlcK3g9S7_a3_qHjB-IZ2QQog_8sSMDEePG7USXDwf51CLmuWkKIRazkL-2eLd6cElHUaxCjXfxOVM_shRWwhyuWcy3znf3sHXsu8024fiU4QW5_TnZgoVhuAI5QvK1EKOW_AvJClUGLKLcbpnYtrOpJZbjJCEntETTc2N8kgB_xSynfa-7W54cEk3Nv_ClGaz0L7sc2qmP22JtII_WJIcG_0cPbX7gv0zvgEwhPPTCoBzaVMQv0ihjgh5Cx-uT5FC9TBilpzj6d2VB5hxIBTET6HCkJWWOzIhdcRKEAbsU3nRMRjMPRbMOm1vd7Za9p1lwmBsCvn3S7-g_pCLWEXxZ5gDmvnujTu-vntQPVIIQTErhZAeI4VKJcqTKI-RC4yqd2MUz_ceSoBZWSnu0FVlQ7sHBqb87eRUK3jNhu_5eCWZ5fXJ6ZeL85s-hYiy-rhuihRCjAHACG26MlrVhJKZ6nJP9G7E9LUxNBmKNqhamGG3E71HklnuMLTejfnYeloUtcbHu9NijBQqxUiiUsnMd4s1QQuj5awuBK5MCoYpU8hfbtFc5A-b956rWy3edzXnPXVeW-A3WuA32giwYGvT2FtR4DWtDZelke_1IeIUKVTKIOSNIbCcLl0L9lotuvPyBXsT8qkRa2JHXhux1t5u6bXsOyPW_b2VL8h_TEchEs7ut3tr4nsULF_h_eBYxYSDpN0NWozuh9xYrVRf5-o6lwwglKpOjr6XGElUyiCdpLPDZ3Ms5m-x3BWe4Ha7jzTwqcT28vs3sUKJ7f3tm0Ba9tWjDhCvC8sBVggTgR20zR20LR20ORy0gRzRNRetjOHomotWy_ow6B7KXbPF6B7KXafFeH8-eEM9eG86yIkOIjIQN08RcquD3erwsenv4RyOfVPQvHsyBjVqIdxFL7Ebf3s-qBX8QWkoitsHSqMB3N6Y-ckcZDIHmTEHmTEHmbwnvwO_wS5KpYnDvd0NWizgIBg9zmKFoc4tdG4AackkWjKJlkCIOZOnUJ6Cgj4pXOw9ajLz090NknX_4_MGreAPUvv-eYOWfSW16SBykg4iJ-kID53T4TnLWDCeFCBk9_4iLOlgcpI4btldh8WIiEjRziUDCLdBUlchJCHimUg6_OGBIIVKXYgIog7QHaSmRBvelGhfmRJtIlOirWZK0YUfrY_hcLuZUrzd1I7ilkV7ajHegmr3ce6T36yn5LfcKXk_qJ0gpFMpg_jNbUp-45qS98sp-Q2wDo3yoBe-M03E3I2ppcba5msjJBW8MULC-7YQbN7nsEKYCMrpL2V9QXgpaw39hOCjKg4n734VywarAUbH0en0W9l0ei-iVVEpRjqVMoj3c9qhg6DCmbhDeA6lXRooOhIUy4U717wBPiwuknK-vgurZd95m-zvQX1BFHnyzmZWOvvPodzZDoI6IYWLIeJ3nCn7o-WUyfLnwmuWC69Zdt8SEVXIGEz07DhL6j_voOwGP2K8x3B5qOWG0wnwWTaX1wpASnL5Wutva9J-YY6y50TfhmgVH9WU92wo5fdsSMq-CYFxphUh6Qp2tMve1vP8Tw6DrM92FW7Xl-IuCfmqYCPi95cYQ_SRseQAY0qMSpCaZsTqwdlyQmzFLclYVGKUZOwHxvHXHkKwiBvKDE2Gtj_UVnZQC23lTmmPKZNSRoZu2HFI12aAtQDLAXYw1hkqQcm6F38Uv_gLMaRvXKZz7rUfJ609Qrr2iMnam2wZs8FXfLS7A-Irrf5-bXVdfwXB_p5Hy8uCfAu9mgtNu7K3KWkrjkBJkDbLf5ZpXC2uuxMJ60_rWy67v5q1c9ZrNq9z5loWMmiiDPb9miJhy3TbsrlxPmn1PLRVmbazdbvwghSYyIV0HPu5TcMNTRrjeX3tcdvxG7_CcvdwGVZjM1wG6exp12F9QM00nM97chUZfrozKppMN2I63QaT6TZlZboNJl0ryc9uKTFSCOmwAlLqwDGWK0IKOM874cySEEyv1HTa-V3ae97vY93aayCdJcSW9hpIFktf11qLpaAMf4FJ0HoFecKA0Ba7813qixGMLCbDqMB-qkd0EADJGLAYViQnhJUgW9XCTF0L0s5fUC526G0t1sQhIXbnuz54outQCJMew1s30mF8D0f6C5B0F4tN6MNCmkdkJ4WQVgTFpEfrdR7TJYDuPgGmdQGks7A81yEGQOahX-ZEQpFJTsRkSPLwAYYEiA4JIOkGFltDkldYDhgSQjokxLQugLSjC2p2SOOSfiM_8s6BOCHpyiBRHyzqg0R9kKgLkrmmHFSVua5Vknt_j-i6XZo7ZgNITB_cCDdIISQodTCUGWoMTYKWUlmogkGe-OxgidckjZmsMZM0BpC2Flpuzl-lOiAJ9sHnLGl1AOiupFyRcOWAi1PXC8aH2n-Nl5SxbXxFbc1BhrvQBimEdEJwYRTKDDWGJkGyMBWhuicvy01vAKqbTcmSEkJ1Qwdmynvq8nHFgvrecrlJ1X50N6lpNKjriyDcumUg0S2D1YOgtDYNtmjGUZtNg2JIqPO-en93UqZJ7jgDUDzQ_UTKpWucSLl1baC-hXdgJqBnOV0BRuXgfc-vTGBaM1_6nkCEdAIRkwlESCfQFJXJWpidLMGy7ZMrYaY8Ja89d0rOsV6dfuOV4eKyQTohRq_OTHp1ZtKrM5NenbAJtRAYvIx3p0Ug5GYyANMDhxcZuQqO8iEXow207Z2ooyCojmd6sID5rD9YQBkJWUCVtRNDYFeCVNYQqwdBKmumaMZpMrv5e3wGU6_mzw0096HPMGTZ4E6yQQohnRAjS3hN2kKTICM4Gc7MRXDuO9tbLOReNACnl5PMNlruHYMTzDk_WeTcQ3uiqTW01zrx98VxUmO9Gb4TFrvJBfi-DKp4i1q8RS1kUQtb1MIWtZBF1W95fyFrN2o6pKKNX_wW0UZIRRsxEW2EVLRN0YxjtmJs7g9nbKLb1REZqnAdxUInQUYe8Xu_FsoMHQR1QgoXA8G-763uBa2HW2G5ZgtA80te2SLKzVcDNb_k-rVkEOvanyzi6uujzcMPaItgIKSCgZgIBkIqGKZoxomwNg_vF98Y27yMjWOKTGnzjK9VDww_6Y3NS6sMjf_f17UlBwyCwCvV-EDvf7F2AugumP5uFMiq-MCRSLoVQtJHuU42xuxIJyoEObMjnagQ5MxS1Yp_y8yOjsyiCuWP3d1I7s54k-jBJHowSR5MsgeT7MEkeTDPtgvcYeJa445y2Rp3iBl3CDl3VLWi-cydSOZO5IOhmRY48wxn8xyz7g0eIZIQckrTV4wJGhlaCaJ9kkE9V-zQk2f5mk1n_282tat1X7Pp_InkeQNTptmeIG9gxKyBEXJpCzq9SUPIpSFm0hAyaQ3u4WwIIl09FnIELu2qwit06qkJNwSqKZMN7wYEzKLNFbFF5Xz9Fh1re2DvOBWByLAC2ZaTbinKO1mU9Cb3rkGZFitiI620WoVhqEbA62sG1ABAeh5XgIl0yMrm95KS_a2eL7cqi87VQy3L_8DhqrZz0qRamMCGsXGxbH39DJ7a0OhzafCfyhWkbTGgRwDeJivvKrcNsTAEMrDUBoGLXho_dQhGvvqj5ikGfPvixTU0kSTvBmlUfC8IaBHZ4ePRoPbLc8IoLhzPCRFK1qWVSVFX-if1daVFQP3ZJ5BVm7-Az9DlSqmodIxjHb77QlA0eD-Ac7ix4rGDEy7Ug10WLr70aoeZPq9kzUSW60C6TcJiCQ-gqdHn6sygBtqaPWtfW4LGC4lp10nvPQ1S7fYKixTo5msH0F4lv7Y86ovdkQIA:5:1:6:842519:8:10:9:10:10:8015151:12:14:13:21:14:298336:17:1:43:3:25::18:10:19:700:42:0:45:7092:15:3:30:897837:31:0:28:10 years:29:4 years:35:0:36:0_167_67_0_0_0_0_207_0_0_89_88_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0:37:0:38:0:39:0:46:113:47:0:40:0:57:0:27:AwYDBQUCBw==:52:1234, 3456:53:5678, 9348 \ No newline at end of file diff --git a/tests/artifacts/level_comment/258976/processed b/tests/artifacts/level_comment/258976/processed index e590163..ffc7ce8 100644 --- a/tests/artifacts/level_comment/258976/processed +++ b/tests/artifacts/level_comment/258976/processed @@ -1 +1,18 @@ -{"user":null,"content":"Special thanks to Hado, Cinci, Synactive, Cool, Prism, Subwoofer, and Hado for playtesting.","user_id":7226087,"likes":104,"comment_id":258976,"is_flagged_spam":false,"time_since_post":"5 days","progress":0,"mod_level":"Elder","special_color":{"Known":[75,255,75]}} \ No newline at end of file +{ + "user":null, + "content": "Special thanks to Hado, Cinci, Synactive, Cool, Prism, Subwoofer, and Hado for playtesting.", + "user_id": 7226087, + "likes": 104, + "comment_id": 258976, + "is_flagged_spam": false, + "time_since_post": "5 days", + "progress": 0, + "mod_level": "Elder", + "special_color": { + "Known": [ + 75, + 255, + 75 + ] + } +} \ No newline at end of file diff --git a/tests/artifacts/listed_level/11774780/processed b/tests/artifacts/listed_level/11774780/processed index 08ab8f9..2baf00f 100644 --- a/tests/artifacts/listed_level/11774780/processed +++ b/tests/artifacts/listed_level/11774780/processed @@ -20,9 +20,9 @@ "coin_amount": 3, "coins_verified": true, "stars_requested": 10, - "is_epic": false, + "epic": "None", "object_amount": null, - "index_46": "1", - "index_47": "2", + "editor_time": 1, + "editor_time_copies": 2, "level_data": null } \ No newline at end of file diff --git a/tests/artifacts/listed_level/24904605/processed b/tests/artifacts/listed_level/24904605/processed index 18c4d0b..d7ba368 100644 --- a/tests/artifacts/listed_level/24904605/processed +++ b/tests/artifacts/listed_level/24904605/processed @@ -20,9 +20,9 @@ "coin_amount": 3, "coins_verified": true, "stars_requested": 10, - "is_epic": false, + "epic": "None", "object_amount": null, - "index_46": "1", - "index_47": "2", + "editor_time": 1, + "editor_time_copies": 2, "level_data": null } \ No newline at end of file diff --git a/tests/artifacts/listed_level/63355989/processed b/tests/artifacts/listed_level/63355989/processed index b9d9213..24083be 100644 --- a/tests/artifacts/listed_level/63355989/processed +++ b/tests/artifacts/listed_level/63355989/processed @@ -19,8 +19,9 @@ "coins_verified": true, "stars_requested": 7, "is_epic": false, + "epic": "None", "object_amount": 37865, - "index_46": "1", - "index_47": "2", + "editor_time": 1, + "editor_time_copies": 2, "level_data": null } \ No newline at end of file diff --git a/tests/artifacts/listed_level/72540/processed b/tests/artifacts/listed_level/72540/processed index 6a44315..11b633d 100644 --- a/tests/artifacts/listed_level/72540/processed +++ b/tests/artifacts/listed_level/72540/processed @@ -20,9 +20,9 @@ "coin_amount": 0, "coins_verified": false, "stars_requested": null, - "is_epic": false, + "epic": "None", "object_amount": null, - "index_46": "1", - "index_47": "2", + "editor_time": 1, + "editor_time_copies": 2, "level_data": null } \ No newline at end of file diff --git a/tests/artifacts/listed_level/97598449/processed b/tests/artifacts/listed_level/97598449/processed index 771e373..bcee1bd 100644 --- a/tests/artifacts/listed_level/97598449/processed +++ b/tests/artifacts/listed_level/97598449/processed @@ -18,9 +18,9 @@ "coin_amount": 0, "coins_verified": false, "stars_requested": 6, - "is_epic": true, + "epic": "Epic", "object_amount": 18374, - "index_46": "1", - "index_47": "2", + "editor_time": 1, + "editor_time_copies": 2, "level_data": null } \ No newline at end of file diff --git a/tests/artifacts/profile/8451/processed b/tests/artifacts/profile/8451/processed index 8e3a88b..8e20ef4 100644 --- a/tests/artifacts/profile/8451/processed +++ b/tests/artifacts/profile/8451/processed @@ -4,6 +4,7 @@ "stars": 13791, "demons": 483, "creator_points": 19, + "icon_id": "Cube", "primary_color": { "Known": [ 255, @@ -21,8 +22,8 @@ "secret_coins": 151, "account_id": 8451, "user_coins": 499, - "index_18": "0", - "index_19": "0", + "message_state": "Anyone", + "friends_state": "Anyone", "youtube_url": "stardust19710", "cube_index": 95, "ship_index": 48, @@ -31,21 +32,17 @@ "wave_index": 11, "robot_index": 10, "has_glow": true, - "index_29": "1", + "is_registered": true, "global_rank": 13731, - "index_31": "0", - "index_38": null, - "index_39": null, - "index_40": null, + "friend_state": "NoRequest", + "unread_messages_count": null, + "unread_friend_request_count": null, + "new_friends_count": null, "spider_index": 2, "twitter_url": "stadust1971", "twitch_url": null, "diamonds": 2621, "death_effect_index": 13, "mod_level": "None", - "index_50": "0", - "index_51": "63", - "moons": 16, - "swing_index": 3, - "jetpack_index": 1 + "comment_history_state": "Anyone" } \ No newline at end of file diff --git a/tests/artifacts/profile/8451/raw b/tests/artifacts/profile/8451/raw index ce6eb8b..bfdcf59 100644 --- a/tests/artifacts/profile/8451/raw +++ b/tests/artifacts/profile/8451/raw @@ -1 +1 @@ -1:stardust1971:2:2073761:13:151:17:499:10:9:11:10:51:63:3:13791:52:16:46:2621:4:483:8:19:18:0:19:0:50:0:20:stardust19710:21:95:22:48:23:33:24:18:25:11:26:10:28:1:43:2:48:13:53:3:54:1:30:13731:16:8451:31:0:44:stadust1971:45::49:0:29:1 \ No newline at end of file +1:stardust1971:2:2073761:6::7::13:151:17:499:10:9:9:0:11:10:3:13791:46:2621:4:483:8:19:18:0:19:0:50:0:20:stardust19710:21:95:22:48:23:33:24:18:25:11:26:10:28:1:43:2:48:13:30:13731:16:8451:31:0:38::39::40::41::42::44:stadust1971:45::49:0:29:1 \ No newline at end of file diff --git a/tests/framework.rs b/tests/framework.rs index 0c4d1fc..3deb68b 100644 --- a/tests/framework.rs +++ b/tests/framework.rs @@ -6,7 +6,7 @@ use std::{ marker::PhantomData, path::{Path, PathBuf}, }; - +use std::collections::BTreeSet; use dash_rs::{GJFormat, IndexedDeserializer}; use pretty_assertions::assert_eq; use serde::Deserialize; @@ -114,7 +114,7 @@ fn assert_indexed_strings_equal<'a, D: GJFormat<'a>>(a: &str, b: &str) { let map_b = BTreeMap::<&str, &str>::deserialize(&mut deserializer_b).unwrap(); // BTreeMap + pretty_assertions will make sure that this is easily interpretable - assert_eq!(map_a, map_b); + assert_maps_equal(&map_a, &map_b); } else { let vec_a = Vec::<&str>::deserialize(&mut deserializer_a).unwrap(); let vec_b = Vec::<&str>::deserialize(&mut deserializer_b).unwrap(); @@ -122,3 +122,18 @@ fn assert_indexed_strings_equal<'a, D: GJFormat<'a>>(a: &str, b: &str) { assert_eq!(vec_a, vec_b); } } + +/// This functions asserts equality on maps and ignores missing indexes or indexes with empty values +/// This is because for some reason Rob chose to omit the LevelID index and value completely if +/// a level/list comment instead of just returning the index with no values as he does +/// literally everywhere else +fn assert_maps_equal(left: &BTreeMap<&str, &str>, right: &BTreeMap<&str, &str>) { + let all_keys: BTreeSet<&str> = left.keys().chain(right.keys()).copied().collect(); + + for key in all_keys { + let l = left.get(key).filter(|v| !v.is_empty()); + let r = right.get(key).filter(|v| !v.is_empty()); + + assert_eq!(l, r, "mismatch at index {key}"); + } +} \ No newline at end of file diff --git a/tests/level.rs b/tests/level.rs index 40eedcc..b0125ff 100644 --- a/tests/level.rs +++ b/tests/level.rs @@ -51,7 +51,13 @@ impl framework::Testable for FullLevelTester { #[test] fn test_full_level() { - let units = load_test_units::(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join("artifacts").join("level")); + let units = load_test_units::( + Path::new(env!("CARGO_MANIFEST_DIR") + ) + .join("tests") + .join("artifacts") + .join("level") + ); for (path, unit) in units { println!("Testing case {:?}", path); From 4691ecf23f86d59ae2c79d0bdd2cb879ede09ddd Mon Sep 17 00:00:00 2001 From: Ryder Date: Tue, 9 Jun 2026 12:07:45 -1000 Subject: [PATCH 29/29] Updated for 2.2 and added unknown indexes --- src/request/level.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/request/level.rs b/src/request/level.rs index a04d752..c33bb1d 100644 --- a/src/request/level.rs +++ b/src/request/level.rs @@ -280,7 +280,7 @@ impl SearchFilters { pub enum LevelRequestType { /// A search request. /// - /// Setting this variant will enabled all the available search filters + /// Setting this variant will enable all the available search filters /// /// ## GD Internals: /// This variant is represented by the value `0` in requests @@ -312,7 +312,7 @@ pub enum LevelRequestType { Recent, /// Retrieve levels by the user whose ID was specified in [`LevelsRequest::search_string`] - /// (Note that is has to be the user Id, not the account id) + /// (Note that is has to be the user ID, not the account id) /// /// ## GD Internals: /// This variant is represented by the value `5` in requests @@ -659,8 +659,8 @@ mod tests { }; use crate::request::level::LevelRequest; - #[tokio::test] - async fn serialize_levels_request() { + #[test] + fn serialize_levels_request() { let request = LevelsRequest::default() .request_type(LevelRequestType::MostLiked) @@ -680,8 +680,8 @@ mod tests { ); } - #[tokio::test] - async fn serialize_level_request() { + #[test] + fn serialize_level_request() { let request = LevelRequest::default() .level_id(17448979);