Skip to content

Commit 8c7988b

Browse files
committed
Retry guest login when throttled
1 parent 268a9a7 commit 8c7988b

4 files changed

Lines changed: 106 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ crossfire = { version = "2.1.6", optional = true }
1010
futures-util = "0.3"
1111
html_parser = "0.7"
1212
log = "0.4"
13+
regex = "1.12"
1314
reqwest = { version = "0.12", default-features = false, features = ["json"] }
1415
rust_socketio = { version = "0.6", features = ["async"] }
1516
serde = { version = "1.0", features = ["derive"] }

src/main.rs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ use rust_socketio::asynchronous::{Client, ClientBuilder};
99
use rust_socketio::{Payload, TransportType};
1010
use serde_json::{Value, json};
1111
use simple_logger::SimpleLogger;
12+
use std::sync::Arc;
1213
use tokio::fs::File;
1314
use tokio::io::{AsyncWriteExt, BufWriter};
1415
use tokio::select;
1516
use tokio::signal;
16-
use tokio::time::Duration;
17+
use tokio::time::{Duration, sleep};
1718
use tokio_util::sync::CancellationToken;
1819

1920
const WRITE_BUFFER_SIZE: usize = 8 * 1024; // 8 KiB
@@ -54,6 +55,11 @@ enum Event {
5455
Terminate,
5556
}
5657

58+
enum LoginError {
59+
Throttled(u64),
60+
Unknown(Option<String>),
61+
}
62+
5763
enum SocketAddressError {
5864
NotFound,
5965
Parse(serde_json::Error),
@@ -73,7 +79,7 @@ async fn create_chat_log_file(channel: &str) -> File {
7379
file
7480
}
7581

76-
fn handle_login_event(values: Vec<Value>) {
82+
fn handle_login_event(values: Vec<Value>) -> Result<(), LoginError> {
7783
for value in values {
7884
let login: data::Login = match serde_json::from_value(value) {
7985
Ok(v) => v,
@@ -86,15 +92,21 @@ fn handle_login_event(values: Vec<Value>) {
8692
if login.success {
8793
log::info!(
8894
"Logged in as guest {}",
89-
login.name.unwrap_or("Unknown".into())
95+
login.name.unwrap_or("<UNKNOWN>".into())
9096
);
97+
return Ok(());
9198
} else {
92-
log::warn!(
93-
"Login failed: {}",
94-
login.error.unwrap_or("Unknown error".into())
95-
);
99+
if let Some(error_message) = login.error {
100+
if let Some(duration) = utils::check_throttling_error(&error_message) {
101+
return Err(LoginError::Throttled(duration));
102+
} else {
103+
return Err(LoginError::Unknown(Some(error_message)));
104+
}
105+
}
106+
return Err(LoginError::Unknown(None));
96107
}
97108
}
109+
Err(LoginError::Unknown(None))
98110
}
99111

100112
/// Join a channel on the Cytube server.
@@ -211,16 +223,18 @@ async fn main() {
211223
};
212224

213225
let channel_name = args.channel.clone();
226+
let guest_login = Arc::new(args.guest_login);
227+
let connect_guest_login = guest_login.clone();
214228
let socket = ClientBuilder::new(socket_address)
215229
.transport_type(TransportType::Any)
216230
.on(rust_socketio::Event::Connect, move |_, client| {
217231
let channel_name = channel_name.clone();
218-
let guest_login = args.guest_login.clone();
232+
let username = connect_guest_login.clone();
219233
async move {
220234
log::info!("Connected to server");
221235
join_channel(&client, &channel_name).await;
222-
if let Some(username) = guest_login {
223-
login_as_guest(&client, &username).await;
236+
if let Some(username) = &*username {
237+
login_as_guest(&client, username).await;
224238
}
225239
}
226240
.boxed()
@@ -286,6 +300,7 @@ async fn main() {
286300
.expect("Connection failed");
287301

288302
let channel_name = args.channel.clone();
303+
let manager_socket = socket.clone();
289304
let manager = tokio::spawn(async move {
290305
let mut last_timestamp: u64 = 0;
291306
while let Some(event) = channel::read_event(&mut rx).await {
@@ -327,7 +342,24 @@ async fn main() {
327342
Event::Disconnect => {
328343
log::warn!("Client disconnected from server");
329344
}
330-
Event::Login(values) => handle_login_event(values),
345+
Event::Login(values) => match handle_login_event(values) {
346+
Ok(_) => {}
347+
Err(LoginError::Throttled(seconds)) => {
348+
log::warn!("Guest login throttled; retrying in {} seconds", seconds);
349+
let username = guest_login.clone();
350+
let socket = manager_socket.clone();
351+
let _ = tokio::spawn(async move {
352+
sleep(Duration::from_secs(seconds)).await;
353+
if let Some(username) = &*username {
354+
login_as_guest(&socket, username).await;
355+
}
356+
});
357+
}
358+
Err(LoginError::Unknown(Some(message))) => {
359+
log::warn!("Login failed: {}", message)
360+
}
361+
Err(LoginError::Unknown(None)) => log::warn!("Login failed"),
362+
},
331363
Event::RotateLog => {
332364
log::info!("Rotating log file...");
333365
match file_buffer.flush().await {

src/utils.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
use regex::Regex;
2+
use std::sync::LazyLock;
3+
4+
/// Determine if the error message indicates a throttling error.
5+
pub fn check_throttling_error(string: &str) -> Option<u64> {
6+
static RE: LazyLock<Regex> = LazyLock::new(|| {
7+
Regex::new(r"Guest logins are restricted to one per IP address per (\d+) seconds").unwrap()
8+
});
9+
if let Some(captures) = RE.captures(string) {
10+
captures.get(1).unwrap().as_str().parse::<u64>().ok()
11+
} else {
12+
None
13+
}
14+
}
15+
116
/// Parse host from plain domain name or URL.
217
pub fn parse_domain(s: &str) -> Result<url::Host, String> {
318
if let Ok(host) = url::Host::parse(s) {
@@ -15,6 +30,14 @@ pub fn parse_domain(s: &str) -> Result<url::Host, String> {
1530
mod tests {
1631
use test_case::test_case;
1732

33+
#[test_case("Guest logins are restricted to one per IP address per 60 seconds.", Some(60); "normal error")]
34+
#[test_case("Guest logins are restricted to one per IP address per 9999 seconds.", Some(9999); "longer error")]
35+
#[test_case("Guest logins are restricted to one per IP address per N seconds.", None; "non-numerical error")]
36+
#[test_case("Something bad happened", None; "unknown error")]
37+
fn check_throttling_error(input: &str, expected: Option<u64>) {
38+
assert_eq!(super::check_throttling_error(input), expected);
39+
}
40+
1841
#[test_case("cytu.be", Some("cytu.be"); "plain domain")]
1942
#[test_case("https://cytu.be", Some("cytu.be"); "URL")]
2043
#[test_case("@t!", None; "invalid characters")]

0 commit comments

Comments
 (0)