From fb54e25ee2a6b1eee221c67f8d92b50684be1dfe Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Sat, 26 Sep 2026 22:03:39 +0530 Subject: [PATCH] Add cookie tutorial and configurable 12h/24h time format --- .env.example | 7 +++-- README.md | 37 +++++++++++++++++++--- config.example.toml | 11 ++++--- config.toml | 11 ++++--- src/config.rs | 76 +++++++++++++++++++++++++++++++++++++++------ src/main.rs | 34 +++++++++++++++----- 6 files changed, 143 insertions(+), 33 deletions(-) diff --git a/.env.example b/.env.example index 3df0fbf..3e261dc 100644 --- a/.env.example +++ b/.env.example @@ -21,8 +21,11 @@ INSTA_INSTAGRAM__TIMEZONE=Asia/Kolkata # Note update interval in seconds (SAFETY: Minimum 120s recommended to prevent bans) INSTA_NOTE__INTERVAL_SECONDS=120 -# Time format (strftime syntax, e.g. "%H:%M" for 24h, "%I:%M %p" for 12h) -INSTA_NOTE__TIME_FORMAT=%H:%M +# Time display mode: "12h" (e.g. 02:30 PM) or "24h" (e.g. 14:30) +INSTA_NOTE__MODE=12h + +# Optional custom strftime format (leave empty to use mode) +INSTA_NOTE__TIME_FORMAT="" # Note Prefix and Suffix (Max note length on Instagram is 60 characters) INSTA_NOTE__PREFIX="" diff --git a/README.md b/README.md index d7b2ff7..6fd19c5 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ cargo build --release # Option A: Direct session cookie import (recommended) cargo run -- login --session-id "YOUR_SESSION_ID" --user-id "YOUR_DS_USER_ID" -# Option B: Interactive login (supports 2FA) +# Option B: Interactive login (supports 2FA or guided cookie entry) cargo run -- login ``` -Session data will be stored securely in `session.json`. +Session data is saved securely to `session.json` with restricted `0600` permissions. ### 3. Run ```bash @@ -37,6 +37,27 @@ cargo run -- run --- +## How to Get Your ds_user_id & sessionid Cookies + +Using your browser cookies is the safest authentication method because it bypasses Instagram's mobile app bot detection algorithms. + +1. Open your web browser (Chrome, Firefox, Edge, or Brave). +2. Go to [instagram.com](https://www.instagram.com) and log in to your account. +3. Press **F12** (or right-click anywhere and choose **Inspect**) to open Developer Tools. +4. Go to the storage tab: + - In **Chrome / Edge / Brave**: Click the **Application** tab at the top. + - In **Firefox**: Click the **Storage** tab at the top. +5. In the left sidebar, expand **Cookies** and select `https://www.instagram.com`. +6. Find these two cookie names in the table: + - `ds_user_id`: Your numeric user ID (e.g. `6891234567`). + - `sessionid`: Your private session authentication token. +7. Copy both values and pass them to the login command: + ```bash + cargo run -- login --session-id "YOUR_SESSION_ID" --user-id "YOUR_DS_USER_ID" + ``` + +--- + ## Configuration (`config.toml`) ```toml @@ -46,10 +67,18 @@ timezone = "Asia/Kolkata" [note] interval_seconds = 120 # Minimum recommended: 120s -time_format = "%H:%M" # strftime syntax + +# Time display mode: choose "12h" or "24h" +# "12h" -> 02:30 PM +# "24h" -> 14:30 +mode = "12h" + +# Optional custom strftime format (leave empty to use `mode` setting) +time_format = "" + prefix = "" suffix = "" max_requests_per_hour = 30 ``` -Environment variables with prefix `INSTA_` (e.g. `INSTA_NOTE__INTERVAL_SECONDS=120`) are also supported. +Environment variables prefixed with `INSTA_` (e.g. `INSTA_NOTE__MODE=24h`) are also supported. diff --git a/config.example.toml b/config.example.toml index 674f95e..5c8e934 100644 --- a/config.example.toml +++ b/config.example.toml @@ -25,11 +25,12 @@ timezone = "Asia/Kolkata" # Setting this below 120s carries extreme risk of temporary or permanent account suspension. interval_seconds = 120 -# Time formatting using standard chrono strftime specifiers: -# "%H:%M" -> "14:30" (24-hour) -# "%I:%M %p" -> "02:30 PM" (12-hour) -# "%a %H:%M" -> "Sat 14:30" -time_format = "%H:%M" +# Time display mode: "12h" (e.g. 02:30 PM) or "24h" (e.g. 14:30) +mode = "12h" + +# Optional custom strftime format (leave empty to use `mode` setting) +# Examples: "%I:%M %p", "%H:%M", "%a %I:%M %p" +time_format = "" # Prefix placed before the formatted time prefix = "" diff --git a/config.toml b/config.toml index 674f95e..5c8e934 100644 --- a/config.toml +++ b/config.toml @@ -25,11 +25,12 @@ timezone = "Asia/Kolkata" # Setting this below 120s carries extreme risk of temporary or permanent account suspension. interval_seconds = 120 -# Time formatting using standard chrono strftime specifiers: -# "%H:%M" -> "14:30" (24-hour) -# "%I:%M %p" -> "02:30 PM" (12-hour) -# "%a %H:%M" -> "Sat 14:30" -time_format = "%H:%M" +# Time display mode: "12h" (e.g. 02:30 PM) or "24h" (e.g. 14:30) +mode = "12h" + +# Optional custom strftime format (leave empty to use `mode` setting) +# Examples: "%I:%M %p", "%H:%M", "%a %I:%M %p" +time_format = "" # Prefix placed before the formatted time prefix = "" diff --git a/src/config.rs b/src/config.rs index c765838..13e0ac3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,7 +50,11 @@ fn default_timezone() -> String { pub struct NoteConfig { #[serde(default = "default_interval")] pub interval_seconds: u64, - #[serde(default = "default_time_format")] + /// Time display mode: "12h" or "24h" + #[serde(default = "default_mode")] + pub mode: String, + /// Optional strftime format (e.g. "%I:%M %p", "%H:%M", or shorthand "12h" / "24h") + #[serde(default)] pub time_format: String, #[serde(default = "default_prefix")] pub prefix: String, @@ -60,11 +64,34 @@ pub struct NoteConfig { pub max_requests_per_hour: u32, } +impl NoteConfig { + /// Resolve the active strftime time format from either `time_format` or `mode` ("12h" vs "24h") + pub fn resolved_time_format(&self) -> &str { + let tf = self.time_format.trim(); + if tf.eq_ignore_ascii_case("12h") { + return "%I:%M %p"; + } + if tf.eq_ignore_ascii_case("24h") { + return "%H:%M"; + } + if !tf.is_empty() { + return tf; + } + + if self.mode.eq_ignore_ascii_case("24h") { + "%H:%M" + } else { + "%I:%M %p" // Default to 12h format + } + } +} + impl Default for NoteConfig { fn default() -> Self { Self { interval_seconds: default_interval(), - time_format: default_time_format(), + mode: default_mode(), + time_format: String::new(), prefix: default_prefix(), suffix: String::new(), max_requests_per_hour: default_max_requests_per_hour(), @@ -76,8 +103,8 @@ fn default_interval() -> u64 { 120 } -fn default_time_format() -> String { - "%H:%M".to_string() +fn default_mode() -> String { + "12h".to_string() } fn default_prefix() -> String { @@ -110,7 +137,8 @@ impl AppConfig { .set_default("instagram.session_file", "./session.json")? .set_default("instagram.timezone", "Asia/Kolkata")? .set_default("note.interval_seconds", 120)? - .set_default("note.time_format", "%H:%M")? + .set_default("note.mode", "12h")? + .set_default("note.time_format", "")? .set_default("note.prefix", "")? .set_default("note.suffix", "")? .set_default("note.max_requests_per_hour", 30)?; @@ -140,12 +168,22 @@ impl AppConfig { .try_parsing(true), ); - let config: AppConfig = builder + let mut config: AppConfig = builder .build() .context("Failed to build configuration")? .try_deserialize() .context("Failed to deserialize configuration")?; + if let Ok(val) = std::env::var("INSTA_NOTE_MODE").or_else(|_| std::env::var("INSTA_NOTE__MODE")) { + config.note.mode = val; + } + if let Ok(val) = std::env::var("INSTA_NOTE_TIME_FORMAT").or_else(|_| std::env::var("INSTA_NOTE__TIME_FORMAT")) { + config.note.time_format = val; + } + if let Ok(val) = std::env::var("INSTA_TIMEZONE").or_else(|_| std::env::var("INSTA_INSTAGRAM__TIMEZONE")) { + config.instagram.timezone = val; + } + config.validate()?; Ok(config) } @@ -184,13 +222,32 @@ mod tests { fn test_default_config() { let config = AppConfig::default(); assert_eq!(config.note.interval_seconds, 120); - assert_eq!(config.note.time_format, "%H:%M"); + assert_eq!(config.note.mode, "12h"); + assert_eq!(config.note.resolved_time_format(), "%I:%M %p"); assert_eq!(config.note.prefix, ""); assert_eq!(config.note.max_requests_per_hour, 30); assert_eq!(config.instagram.timezone, "Asia/Kolkata"); assert!(config.validate().is_ok()); } + #[test] + fn test_time_format_resolution() { + let mut note = NoteConfig::default(); + assert_eq!(note.resolved_time_format(), "%I:%M %p"); + + note.mode = "24h".to_string(); + assert_eq!(note.resolved_time_format(), "%H:%M"); + + note.mode = "12h".to_string(); + assert_eq!(note.resolved_time_format(), "%I:%M %p"); + + note.time_format = "24h".to_string(); + assert_eq!(note.resolved_time_format(), "%H:%M"); + + note.time_format = "%H:%M:%S".to_string(); + assert_eq!(note.resolved_time_format(), "%H:%M:%S"); + } + #[test] fn test_parse_toml_string() { let toml_str = r#" @@ -201,7 +258,7 @@ mod tests { [note] interval_seconds = 180 - time_format = "%I:%M %p" + mode = "24h" prefix = "Current Time: " max_requests_per_hour = 20 "#; @@ -217,7 +274,8 @@ mod tests { assert_eq!(cfg.instagram.password, "testpassword"); assert_eq!(cfg.instagram.timezone, "Europe/Paris"); assert_eq!(cfg.note.interval_seconds, 180); - assert_eq!(cfg.note.time_format, "%I:%M %p"); + assert_eq!(cfg.note.mode, "24h"); + assert_eq!(cfg.note.resolved_time_format(), "%H:%M"); assert_eq!(cfg.note.prefix, "Current Time: "); assert_eq!(cfg.note.max_requests_per_hour, 20); assert!(cfg.validate().is_ok()); diff --git a/src/main.rs b/src/main.rs index 20b31d0..e150782 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,10 @@ struct Cli { #[arg(long, global = true)] dry_run: bool, + /// Time display mode override ("12h" or "24h") + #[arg(short, long, global = true)] + mode: Option, + #[command(subcommand)] command: Commands, } @@ -60,9 +64,13 @@ async fn main() -> Result<()> { .init(); let cli = Cli::parse(); - let config = AppConfig::load(cli.config.as_deref()) + let mut config = AppConfig::load(cli.config.as_deref()) .context("Failed to initialize configuration")?; + if let Some(ref m) = cli.mode { + config.note.mode = m.clone(); + } + match cli.command { Commands::Login { session_id, user_id } => { handle_login(&config, session_id, user_id).await?; @@ -191,10 +199,17 @@ async fn handle_login( let choice = choice.trim(); if choice == "2" { - println!("\nTo obtain your session cookie:"); - println!("1. Log in to instagram.com in any web browser."); - println!("2. Open DevTools (F12) -> Application/Storage -> Cookies -> instagram.com"); - println!("3. Copy the values of 'sessionid' and 'ds_user_id'.\n"); + println!("\n--- Tutorial: How to get your ds_user_id and sessionid cookies ---"); + println!("1. Open your browser (Chrome, Firefox, Edge, or Brave)."); + println!("2. Go to https://www.instagram.com and log in to your account."); + println!("3. Press F12 (or right-click the page and select 'Inspect') to open Developer Tools."); + println!("4. In the top tabs, select 'Application' (in Firefox, select 'Storage')."); + println!("5. In the left panel, expand 'Cookies' and click 'https://www.instagram.com'."); + println!("6. In the cookie list, locate:"); + println!(" - 'ds_user_id' : Your numeric account ID (e.g., 6891234567)"); + println!(" - 'sessionid' : Your authentication token string"); + println!(" - 'csrftoken' : Optional CSRF token"); + println!("7. Double-click the 'Value' column to copy each, then enter below:\n"); let sid = read_line("Enter 'sessionid' cookie value: ")?; let uid = read_line("Enter 'ds_user_id' cookie value: ")?; @@ -256,10 +271,11 @@ async fn handle_login( async fn update_clock_note(client: &InstagramClient, config: &AppConfig) -> Result<()> { let tz = config.parse_timezone()?; let now = chrono::Utc::now(); + let time_format = config.note.resolved_time_format(); let note_text = format_note_time( now, tz, - &config.note.time_format, + time_format, &config.note.prefix, &config.note.suffix, ); @@ -278,12 +294,13 @@ async fn update_clock_note(client: &InstagramClient, config: &AppConfig) -> Resu async fn run_clock_loop(client: &InstagramClient, config: &AppConfig) -> Result<()> { let tz = config.parse_timezone()?; let interval = Duration::from_secs(config.note.interval_seconds); + let time_format = config.note.resolved_time_format(); info!("=================================================="); info!("Instagram Note Clock Started"); info!("Timezone: {}", config.instagram.timezone); info!("Refresh Interval: {} seconds", config.note.interval_seconds); - info!("Format: \"{}{}{}\"", config.note.prefix, config.note.time_format, config.note.suffix); + info!("Mode: {} (Format: \"{}{}{}\")", config.note.mode, config.note.prefix, time_format, config.note.suffix); info!("Max Requests / Hour: {}", config.note.max_requests_per_hour); if client.dry_run { info!("MODE: DRY-RUN (No requests will be sent to Instagram)"); @@ -300,10 +317,11 @@ async fn run_clock_loop(client: &InstagramClient, config: &AppConfig) -> Result< tokio::select! { _ = tokio::time::sleep(interval) => { let now = chrono::Utc::now(); + let time_format = config.note.resolved_time_format(); let note_text = format_note_time( now, tz, - &config.note.time_format, + time_format, &config.note.prefix, &config.note.suffix, );