feat: added ability to decline TOS (#751)
* feat: added ability to decline TOS; added blockades for hosts while TOS is declined * fix: allowed datastorage-v2, meta.polyfrost.org and status.polyfrost.org * chore: changed typography * chore: changed TOS decline warning to a modal
This commit is contained in:
parent
815b78e227
commit
606a15905d
15 changed files with 359 additions and 22 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4558,6 +4558,7 @@ dependencies = [
|
|||
"serde",
|
||||
"strum",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -244,9 +244,38 @@ impl Actions {
|
|||
}
|
||||
|
||||
pub fn accept_tos(&self, terms_version: u32, privacy_version: u32) {
|
||||
if let Some(updated) = self.mutate_settings(|settings| {
|
||||
let mut was_declined = false;
|
||||
|
||||
let Some(updated) = self.mutate_settings(|settings| {
|
||||
was_declined = settings.declined_tos;
|
||||
settings.accepted_tos_version = terms_version;
|
||||
settings.accepted_privacy_version = privacy_version;
|
||||
settings.declined_tos = false;
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.persist(updated);
|
||||
|
||||
if was_declined {
|
||||
self.notify("Restart to finish")
|
||||
.body(
|
||||
"Thanks. OneClient reconnects to Polyfrost services the next time you start \
|
||||
it.",
|
||||
)
|
||||
.icon(IconType::RefreshCw01)
|
||||
.send();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decline_tos(&self) {
|
||||
oneclient_common::consent::decline();
|
||||
|
||||
if let Some(updated) = self.mutate_settings(|settings| {
|
||||
settings.declined_tos = true;
|
||||
settings.accepted_tos_version = 0;
|
||||
settings.accepted_privacy_version = 0;
|
||||
settings.seen_onboarding = true;
|
||||
}) {
|
||||
self.persist(updated);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ fn main() {
|
|||
|
||||
let settings = rt.block_on(oneclient_core::settings::store::load_settings(None));
|
||||
|
||||
oneclient_common::consent::init(settings.declined_tos);
|
||||
|
||||
if settings.log_debug {
|
||||
oneclient_core::logger::init_debug()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use freya::prelude::*;
|
||||
use freya::router::RouterContext;
|
||||
|
||||
use super::settings_page;
|
||||
use crate::components::{IconType, link_button, toggle};
|
||||
use crate::components::{Button, IconType, link_button, toggle};
|
||||
use crate::hooks::{use_dispatch, use_launcher, use_settings_snapshot};
|
||||
use crate::platform;
|
||||
use crate::routes::Route;
|
||||
use crate::view::app::settings::{section_header, settings_row};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
@ -45,6 +47,20 @@ impl Component for SettingsLauncher {
|
|||
let folder = data_dir.clone();
|
||||
let open_folder = link_button().on_press(move |_| platform::open_url(&folder));
|
||||
|
||||
// The only way back for someone who declined during onboarding
|
||||
let consent_summary = if settings.declined_tos {
|
||||
"Declined. Poly+ and crash reporting stay off until you accept and restart OneClient."
|
||||
} else {
|
||||
"Accepted. Review them again at any time."
|
||||
};
|
||||
let review_terms = Button::new()
|
||||
.secondary()
|
||||
.small()
|
||||
.on_press(|_| {
|
||||
let _ = RouterContext::get().replace(Route::OnboardingTerms {});
|
||||
})
|
||||
.text("Review");
|
||||
|
||||
settings_page()
|
||||
.child(section_header("GENERAL"))
|
||||
.child(settings_row(
|
||||
|
|
@ -59,6 +75,12 @@ impl Component for SettingsLauncher {
|
|||
"Send anonymous crash and error reports to help fix bugs. Applies on restart.",
|
||||
toggle(crash_reporting),
|
||||
))
|
||||
.child(settings_row(
|
||||
IconType::File02,
|
||||
"Terms & Privacy",
|
||||
consent_summary,
|
||||
review_terms,
|
||||
))
|
||||
.child(section_header("FOLDERS AND FILES"))
|
||||
.child(settings_row(
|
||||
IconType::Folder,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ use freya::prelude::*;
|
|||
use freya::query::UseQuery;
|
||||
use freya::router::RouterContext;
|
||||
|
||||
use crate::components::{Button, Icon, IconType, ScrollArea, Segment, SegmentedControl, toggle};
|
||||
use crate::components::{
|
||||
Button, Icon, IconType, OverlayPopup, ScrollArea, Segment, SegmentedControl, toggle,
|
||||
};
|
||||
use crate::hooks::{
|
||||
TermsQuery, has_migration_data, terms_document, terms_error, terms_is_loading, use_dispatch,
|
||||
use_migration, use_settings_snapshot, use_terms,
|
||||
|
|
@ -13,6 +15,8 @@ use crate::theme::colors;
|
|||
use crate::ui::border_all_color;
|
||||
use crate::view::onboarding::{onboarding_illustration, onboarding_page, step_heading};
|
||||
|
||||
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum LegalTab {
|
||||
Terms,
|
||||
|
|
@ -35,6 +39,7 @@ impl Component for OnboardingTerms {
|
|||
|
||||
let accepted = use_state(|| false);
|
||||
let tab = use_state(|| LegalTab::Terms);
|
||||
let confirming_decline = use_state(|| false);
|
||||
|
||||
let returning = settings.seen_onboarding;
|
||||
let next = if returning {
|
||||
|
|
@ -88,14 +93,33 @@ impl Component for OnboardingTerms {
|
|||
.child(accept_row(accepted))
|
||||
.into_element();
|
||||
|
||||
onboarding_page(
|
||||
onboarding_illustration(IconType::File02),
|
||||
content,
|
||||
terms_nav(back, *accepted.read() && !loading, move || {
|
||||
dispatch.accept_tos(terms_version, privacy_version);
|
||||
let accept_dispatch = dispatch.clone();
|
||||
let nav = terms_nav(back, !loading, move || {
|
||||
if *accepted.read() {
|
||||
accept_dispatch.accept_tos(terms_version, privacy_version);
|
||||
let _ = RouterContext::get().replace(next.clone());
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
let mut confirming = confirming_decline;
|
||||
confirming.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
let modal = confirming_decline.read().then(|| {
|
||||
decline_modal(confirming_decline, move || {
|
||||
dispatch.decline_tos();
|
||||
let _ = RouterContext::get().replace(Route::Home {});
|
||||
})
|
||||
});
|
||||
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.child(onboarding_page(
|
||||
onboarding_illustration(IconType::File02),
|
||||
content,
|
||||
nav,
|
||||
))
|
||||
.maybe_child(modal)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +258,7 @@ fn external_link_button(text: &'static str, url: String) -> impl IntoElement {
|
|||
.child(Icon::new(IconType::LinkExternal01).size(14.))
|
||||
}
|
||||
|
||||
fn accept_row(accepted: State<bool>) -> impl IntoElement {
|
||||
fn accept_row(accepted: State<bool>) -> Element {
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
|
|
@ -259,7 +283,7 @@ fn accept_row(accepted: State<bool>) -> impl IntoElement {
|
|||
)
|
||||
.child(
|
||||
label()
|
||||
.text("Required to use OneClient.")
|
||||
.text("Required for world hosting, socials, nametag indicator, and more.")
|
||||
.font_size(11.)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
|
|
@ -272,7 +296,7 @@ fn terms_nav(
|
|||
back: Option<Route>,
|
||||
next_enabled: bool,
|
||||
on_next: impl FnMut() + 'static,
|
||||
) -> impl IntoElement {
|
||||
) -> Element {
|
||||
let mut on_next = on_next;
|
||||
rect()
|
||||
.horizontal()
|
||||
|
|
@ -302,3 +326,89 @@ fn terms_nav(
|
|||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn decline_modal(confirming: State<bool>, on_confirm: impl FnMut() + 'static) -> Element {
|
||||
let mut on_confirm = on_confirm;
|
||||
let mut confirming = confirming;
|
||||
|
||||
OverlayPopup::new()
|
||||
.on_close(move |_| confirming.set(false))
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.center()
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(460.))
|
||||
.max_width(Size::window_percent(90.))
|
||||
.spacing(14.)
|
||||
.padding(Gaps::new_all(20.))
|
||||
.corner_radius(CornerRadius::new_all(14.))
|
||||
.background(CARD_BG)
|
||||
.border(border_all_color(1., colors::danger()))
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(10.)
|
||||
.child(
|
||||
Icon::new(IconType::AlertTriangle)
|
||||
.size(20.)
|
||||
.color(colors::code_warn()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text("Continue without accepting?")
|
||||
.font_size(16.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(
|
||||
"This will disable access to world hosting, \
|
||||
all social features, the OneClient nametag indicator, \
|
||||
and more.",
|
||||
)
|
||||
.font_size(12.)
|
||||
.width(Size::fill())
|
||||
.max_lines(4)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(
|
||||
"You can accept later under Settings > Launcher. That takes \
|
||||
effect after a restart.",
|
||||
)
|
||||
.font_size(11.)
|
||||
.width(Size::fill())
|
||||
.max_lines(3)
|
||||
.color(colors::fg_secondary().with_a(180)),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::End)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(move |_| confirming.set(false))
|
||||
.text("Close"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.danger()
|
||||
.on_press(move |_| on_confirm())
|
||||
.text("Decline"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,9 @@ impl Component for Startup {
|
|||
let stale = settings.settings.accepted_tos_version < required_terms
|
||||
|| settings.settings.accepted_privacy_version < required_privacy;
|
||||
|
||||
let destination = if !settings.settings.seen_onboarding {
|
||||
let destination = if settings.settings.declined_tos {
|
||||
Route::Home {}
|
||||
} else if !settings.settings.seen_onboarding {
|
||||
Route::OnboardingWelcome {}
|
||||
} else if stale {
|
||||
Route::OnboardingTerms {}
|
||||
|
|
|
|||
|
|
@ -21,3 +21,5 @@ directories.workspace = true
|
|||
serde.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
url.workspace = true
|
||||
|
|
|
|||
127
packages/oneclient_common/src/consent.rs
Normal file
127
packages/oneclient_common/src/consent.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// Hosts that are out of bounds for someone who didn't accept TOS
|
||||
const GATED_DOMAINS: &[&str] = &["polyfrost.org", "sentry.io"];
|
||||
|
||||
/// Hosts that are allowed even if someone didn't accept TOS
|
||||
const ALLOWED_HOSTS: &[&str] = &[
|
||||
"data-v2.polyfrost.org",
|
||||
"meta.polyfrost.org",
|
||||
"status.polyfrost.org",
|
||||
];
|
||||
|
||||
static DECLINED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn init(declined: bool) {
|
||||
DECLINED.store(declined, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn decline() {
|
||||
DECLINED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn declined() -> bool {
|
||||
DECLINED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn blocks_url(url: &str) -> bool {
|
||||
declined() && is_gated_url(url)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_gated_url(url: &str) -> bool {
|
||||
url::Url::parse(url)
|
||||
.ok()
|
||||
.and_then(|parsed| parsed.host_str().map(is_gated_host))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_gated_host(host: &str) -> bool {
|
||||
let host = host.trim_end_matches('.').to_ascii_lowercase();
|
||||
|
||||
if ALLOWED_HOSTS.contains(&host.as_str()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GATED_DOMAINS
|
||||
.iter()
|
||||
.any(|domain| host == *domain || host.ends_with(&format!(".{domain}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gates_polyfrost_services_and_the_crash_reporter() {
|
||||
for host in [
|
||||
"polyfrost.org",
|
||||
"plus.polyfrost.org",
|
||||
"PLUS.POLYFROST.ORG",
|
||||
"plus.polyfrost.org.",
|
||||
"o4511714343124992.ingest.us.sentry.io",
|
||||
] {
|
||||
assert!(is_gated_host(host), "expected {host} to be gated");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_everything_else_alone() {
|
||||
for host in [
|
||||
"api.modrinth.com",
|
||||
"api.curseforge.com",
|
||||
"sessionserver.mojang.com",
|
||||
"api.minecraftservices.com",
|
||||
"www.gstatic.com",
|
||||
"github.com",
|
||||
"notpolyfrost.org",
|
||||
"polyfrost.org.attacker.com",
|
||||
"evil-sentry.io.attacker.com",
|
||||
] {
|
||||
assert!(!is_gated_host(host), "unexpected gate on {host}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lets_the_exempt_hosts_through() {
|
||||
for url in [
|
||||
"https://data-v2.polyfrost.org/oneclient/bundles/metadata.json",
|
||||
"https://data-v2.polyfrost.org/oneclient/versions/art/Tricky_Trials.jpg",
|
||||
"https://data-v2.polyfrost.org/oneclient/tos.json",
|
||||
"https://meta.polyfrost.org/fabric/v2/manifest.json",
|
||||
"https://status.polyfrost.org/index.json",
|
||||
"https://META.POLYFROST.ORG./minecraft/v1/manifest.json",
|
||||
] {
|
||||
assert!(!is_gated_url(url), "unexpected gate on {url}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_exemption_is_the_exact_host_and_nothing_near_it() {
|
||||
for host in [
|
||||
"plus.polyfrost.org",
|
||||
"evil.data-v2.polyfrost.org",
|
||||
"meta.polyfrost.org.evil.polyfrost.org",
|
||||
"status.polyfrost.org.evil.polyfrost.org",
|
||||
] {
|
||||
assert!(is_gated_host(host), "expected {host} to stay gated");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gates_urls_by_their_host() {
|
||||
assert!(is_gated_url("https://plus.polyfrost.org/account/login"));
|
||||
assert!(is_gated_url("wss://plus.polyfrost.org/websocket"));
|
||||
assert!(!is_gated_url("https://api.modrinth.com/v2/search"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unparseable_urls_fail_closed() {
|
||||
assert!(is_gated_url("not a url"));
|
||||
assert!(is_gated_url(""));
|
||||
assert!(is_gated_url("file:///etc/hosts"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! Leaf crate must never depend on the database network stack or any
|
||||
//! launcher subsystem or the dependency graph cycles back into a monolith
|
||||
|
||||
pub mod consent;
|
||||
pub mod constants;
|
||||
pub mod domain;
|
||||
pub mod os_ext;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ fn drop_opted_out_breadcrumbs(breadcrumb: Breadcrumb) -> Option<Breadcrumb> {
|
|||
}
|
||||
|
||||
pub fn init(enabled: bool) -> Option<ClientInitGuard> {
|
||||
if oneclient_common::consent::declined() {
|
||||
tracing::debug!("crash reporting disabled: terms and privacy policy declined");
|
||||
return None;
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
tracing::debug!("crash reporting disabled by settings");
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub struct LauncherSettings {
|
|||
pub seen_onboarding: bool,
|
||||
pub accepted_tos_version: u32,
|
||||
pub accepted_privacy_version: u32,
|
||||
pub declined_tos: bool,
|
||||
pub seen_versions: Vec<String>,
|
||||
pub seen_changelog_version: Option<String>,
|
||||
pub modrinth_api_key: Option<String>,
|
||||
|
|
@ -74,6 +75,7 @@ impl Default for LauncherSettings {
|
|||
seen_onboarding: false,
|
||||
accepted_tos_version: 0,
|
||||
accepted_privacy_version: 0,
|
||||
declined_tos: false,
|
||||
seen_versions: Vec::new(),
|
||||
seen_changelog_version: None,
|
||||
modrinth_api_key: None,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ impl LauncherState {
|
|||
};
|
||||
|
||||
let settings = store::load_settings(Some(&services.events)).await;
|
||||
|
||||
oneclient_common::consent::init(settings.declined_tos);
|
||||
services
|
||||
.requester
|
||||
.set_config(crate::settings::net_config(&settings));
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ pub enum RequestError {
|
|||
actual: u64,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"OneClient did not contact {url} because the Terms of Service and Privacy Policy were \
|
||||
declined. Accept them in Settings to turn Polyfrost services back on."
|
||||
)]
|
||||
ConsentRequired { url: String },
|
||||
|
||||
#[error("Invalid URL: {0}")]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ use crate::response::{ResponseExt, ResponseOptions};
|
|||
|
||||
const MAX_THROTTLE_RETRIES: u32 = 6;
|
||||
|
||||
/// Backstop against runaway fan-out not the download throttle
|
||||
/// Must stay above the sum of per-phase caller concurrency or it becomes the
|
||||
/// bottleneck
|
||||
const MAX_INFLIGHT_REQUESTS: usize = 64;
|
||||
|
||||
fn retry_after(response: &Response) -> Option<std::time::Duration> {
|
||||
|
|
@ -134,6 +131,12 @@ impl RequestClient {
|
|||
let mut retries = 0;
|
||||
let mut throttle_retries = 0u32;
|
||||
|
||||
if oneclient_common::consent::blocks_url(request.request.url().as_str()) {
|
||||
let url = request.request.url().to_string();
|
||||
tracing::debug!(%url, "request withheld: terms and privacy policy declined");
|
||||
return Err(RequestError::ConsentRequired { url });
|
||||
}
|
||||
|
||||
apply_curseforge_auth(&mut request.request, &self.config.load().curseforge_api_key)?;
|
||||
|
||||
let cloned_backup = request.request.try_clone();
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ pub enum PlusError {
|
|||
body: String,
|
||||
},
|
||||
|
||||
#[error("Poly+ is off because the Terms of Service and Privacy Policy were declined")]
|
||||
ConsentRequired,
|
||||
|
||||
#[error(transparent)]
|
||||
Request(#[from] reqwest::Error),
|
||||
|
||||
|
|
@ -67,6 +70,11 @@ pub fn start(auth: Arc<oneclient_auth::AuthService>) {
|
|||
return;
|
||||
}
|
||||
|
||||
if plus_blocked() {
|
||||
tracing::info!("[plus] playtime reporting stays off: terms and privacy policy declined");
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let client = match build_client() {
|
||||
Ok(client) => client,
|
||||
|
|
@ -79,6 +87,15 @@ pub fn start(auth: Arc<oneclient_auth::AuthService>) {
|
|||
let mut delay = RECONNECT_DELAY;
|
||||
|
||||
loop {
|
||||
// Consent can only be withdrawn while the launcher is up, never
|
||||
// restored, so ending the task is final rather than a pause
|
||||
if plus_blocked() {
|
||||
tracing::info!(
|
||||
"[plus] ending playtime reporting: terms and privacy policy declined"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
delay = match session(&auth, &client).await {
|
||||
Outcome::Connected | Outcome::NoAccount => RECONNECT_DELAY,
|
||||
Outcome::Failed => (delay * 2).min(MAX_RECONNECT_DELAY),
|
||||
|
|
@ -89,6 +106,10 @@ pub fn start(auth: Arc<oneclient_auth::AuthService>) {
|
|||
});
|
||||
}
|
||||
|
||||
fn plus_blocked() -> bool {
|
||||
oneclient_common::consent::blocks_url(base_url())
|
||||
}
|
||||
|
||||
fn build_client() -> Result<reqwest::Client, reqwest::Error> {
|
||||
let builder = reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(Duration::from_secs(15)))
|
||||
|
|
@ -165,8 +186,8 @@ async fn run_session(
|
|||
tracing::info!("[plus] playtime websocket connected for {}", account.id);
|
||||
|
||||
let account_id = account.id;
|
||||
pump(&mut websocket, PING_INTERVAL, account_id, || {
|
||||
account_changed(auth, account_id)
|
||||
pump(&mut websocket, PING_INTERVAL, account_id, || async move {
|
||||
plus_blocked() || account_changed(auth, account_id).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
@ -195,9 +216,7 @@ where
|
|||
match event {
|
||||
Event::Ping => {
|
||||
if should_stop().await {
|
||||
tracing::info!(
|
||||
"[plus] default account changed, ending playtime session for {account_id}"
|
||||
);
|
||||
tracing::info!("[plus] ending playtime session for {account_id}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +238,10 @@ async fn login(
|
|||
client: &reqwest::Client,
|
||||
account: &MinecraftAccount,
|
||||
) -> Result<String, PlusError> {
|
||||
if plus_blocked() {
|
||||
return Err(PlusError::ConsentRequired);
|
||||
}
|
||||
|
||||
let server_id = generate_server_id();
|
||||
|
||||
let join = client
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue