Add cookie tutorial and configurable 12h/24h time format

This commit is contained in:
katR 2026-09-26 22:03:39 +05:30
parent be32704914
commit fb54e25ee2
6 changed files with 143 additions and 33 deletions

View file

@ -21,8 +21,11 @@ INSTA_INSTAGRAM__TIMEZONE=Asia/Kolkata
# Note update interval in seconds (SAFETY: Minimum 120s recommended to prevent bans) # Note update interval in seconds (SAFETY: Minimum 120s recommended to prevent bans)
INSTA_NOTE__INTERVAL_SECONDS=120 INSTA_NOTE__INTERVAL_SECONDS=120
# Time format (strftime syntax, e.g. "%H:%M" for 24h, "%I:%M %p" for 12h) # Time display mode: "12h" (e.g. 02:30 PM) or "24h" (e.g. 14:30)
INSTA_NOTE__TIME_FORMAT=%H:%M 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) # Note Prefix and Suffix (Max note length on Instagram is 60 characters)
INSTA_NOTE__PREFIX="" INSTA_NOTE__PREFIX=""

View file

@ -18,10 +18,10 @@ cargo build --release
# Option A: Direct session cookie import (recommended) # Option A: Direct session cookie import (recommended)
cargo run -- login --session-id "YOUR_SESSION_ID" --user-id "YOUR_DS_USER_ID" 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 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 ### 3. Run
```bash ```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`) ## Configuration (`config.toml`)
```toml ```toml
@ -46,10 +67,18 @@ timezone = "Asia/Kolkata"
[note] [note]
interval_seconds = 120 # Minimum recommended: 120s 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 = "" prefix = ""
suffix = "" suffix = ""
max_requests_per_hour = 30 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.

View file

@ -25,11 +25,12 @@ timezone = "Asia/Kolkata"
# Setting this below 120s carries extreme risk of temporary or permanent account suspension. # Setting this below 120s carries extreme risk of temporary or permanent account suspension.
interval_seconds = 120 interval_seconds = 120
# Time formatting using standard chrono strftime specifiers: # Time display mode: "12h" (e.g. 02:30 PM) or "24h" (e.g. 14:30)
# "%H:%M" -> "14:30" (24-hour) mode = "12h"
# "%I:%M %p" -> "02:30 PM" (12-hour)
# "%a %H:%M" -> "Sat 14:30" # Optional custom strftime format (leave empty to use `mode` setting)
time_format = "%H:%M" # Examples: "%I:%M %p", "%H:%M", "%a %I:%M %p"
time_format = ""
# Prefix placed before the formatted time # Prefix placed before the formatted time
prefix = "" prefix = ""

View file

@ -25,11 +25,12 @@ timezone = "Asia/Kolkata"
# Setting this below 120s carries extreme risk of temporary or permanent account suspension. # Setting this below 120s carries extreme risk of temporary or permanent account suspension.
interval_seconds = 120 interval_seconds = 120
# Time formatting using standard chrono strftime specifiers: # Time display mode: "12h" (e.g. 02:30 PM) or "24h" (e.g. 14:30)
# "%H:%M" -> "14:30" (24-hour) mode = "12h"
# "%I:%M %p" -> "02:30 PM" (12-hour)
# "%a %H:%M" -> "Sat 14:30" # Optional custom strftime format (leave empty to use `mode` setting)
time_format = "%H:%M" # Examples: "%I:%M %p", "%H:%M", "%a %I:%M %p"
time_format = ""
# Prefix placed before the formatted time # Prefix placed before the formatted time
prefix = "" prefix = ""

View file

@ -50,7 +50,11 @@ fn default_timezone() -> String {
pub struct NoteConfig { pub struct NoteConfig {
#[serde(default = "default_interval")] #[serde(default = "default_interval")]
pub interval_seconds: u64, 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, pub time_format: String,
#[serde(default = "default_prefix")] #[serde(default = "default_prefix")]
pub prefix: String, pub prefix: String,
@ -60,11 +64,34 @@ pub struct NoteConfig {
pub max_requests_per_hour: u32, 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 { impl Default for NoteConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
interval_seconds: default_interval(), interval_seconds: default_interval(),
time_format: default_time_format(), mode: default_mode(),
time_format: String::new(),
prefix: default_prefix(), prefix: default_prefix(),
suffix: String::new(), suffix: String::new(),
max_requests_per_hour: default_max_requests_per_hour(), max_requests_per_hour: default_max_requests_per_hour(),
@ -76,8 +103,8 @@ fn default_interval() -> u64 {
120 120
} }
fn default_time_format() -> String { fn default_mode() -> String {
"%H:%M".to_string() "12h".to_string()
} }
fn default_prefix() -> String { fn default_prefix() -> String {
@ -110,7 +137,8 @@ impl AppConfig {
.set_default("instagram.session_file", "./session.json")? .set_default("instagram.session_file", "./session.json")?
.set_default("instagram.timezone", "Asia/Kolkata")? .set_default("instagram.timezone", "Asia/Kolkata")?
.set_default("note.interval_seconds", 120)? .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.prefix", "")?
.set_default("note.suffix", "")? .set_default("note.suffix", "")?
.set_default("note.max_requests_per_hour", 30)?; .set_default("note.max_requests_per_hour", 30)?;
@ -140,12 +168,22 @@ impl AppConfig {
.try_parsing(true), .try_parsing(true),
); );
let config: AppConfig = builder let mut config: AppConfig = builder
.build() .build()
.context("Failed to build configuration")? .context("Failed to build configuration")?
.try_deserialize() .try_deserialize()
.context("Failed to deserialize configuration")?; .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()?; config.validate()?;
Ok(config) Ok(config)
} }
@ -184,13 +222,32 @@ mod tests {
fn test_default_config() { fn test_default_config() {
let config = AppConfig::default(); let config = AppConfig::default();
assert_eq!(config.note.interval_seconds, 120); 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.prefix, "");
assert_eq!(config.note.max_requests_per_hour, 30); assert_eq!(config.note.max_requests_per_hour, 30);
assert_eq!(config.instagram.timezone, "Asia/Kolkata"); assert_eq!(config.instagram.timezone, "Asia/Kolkata");
assert!(config.validate().is_ok()); 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] #[test]
fn test_parse_toml_string() { fn test_parse_toml_string() {
let toml_str = r#" let toml_str = r#"
@ -201,7 +258,7 @@ mod tests {
[note] [note]
interval_seconds = 180 interval_seconds = 180
time_format = "%I:%M %p" mode = "24h"
prefix = "Current Time: " prefix = "Current Time: "
max_requests_per_hour = 20 max_requests_per_hour = 20
"#; "#;
@ -217,7 +274,8 @@ mod tests {
assert_eq!(cfg.instagram.password, "testpassword"); assert_eq!(cfg.instagram.password, "testpassword");
assert_eq!(cfg.instagram.timezone, "Europe/Paris"); assert_eq!(cfg.instagram.timezone, "Europe/Paris");
assert_eq!(cfg.note.interval_seconds, 180); 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.prefix, "Current Time: ");
assert_eq!(cfg.note.max_requests_per_hour, 20); assert_eq!(cfg.note.max_requests_per_hour, 20);
assert!(cfg.validate().is_ok()); assert!(cfg.validate().is_ok());

View file

@ -24,6 +24,10 @@ struct Cli {
#[arg(long, global = true)] #[arg(long, global = true)]
dry_run: bool, dry_run: bool,
/// Time display mode override ("12h" or "24h")
#[arg(short, long, global = true)]
mode: Option<String>,
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
} }
@ -60,9 +64,13 @@ async fn main() -> Result<()> {
.init(); .init();
let cli = Cli::parse(); 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")?; .context("Failed to initialize configuration")?;
if let Some(ref m) = cli.mode {
config.note.mode = m.clone();
}
match cli.command { match cli.command {
Commands::Login { session_id, user_id } => { Commands::Login { session_id, user_id } => {
handle_login(&config, session_id, user_id).await?; handle_login(&config, session_id, user_id).await?;
@ -191,10 +199,17 @@ async fn handle_login(
let choice = choice.trim(); let choice = choice.trim();
if choice == "2" { if choice == "2" {
println!("\nTo obtain your session cookie:"); println!("\n--- Tutorial: How to get your ds_user_id and sessionid cookies ---");
println!("1. Log in to instagram.com in any web browser."); println!("1. Open your browser (Chrome, Firefox, Edge, or Brave).");
println!("2. Open DevTools (F12) -> Application/Storage -> Cookies -> instagram.com"); println!("2. Go to https://www.instagram.com and log in to your account.");
println!("3. Copy the values of 'sessionid' and 'ds_user_id'.\n"); 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 sid = read_line("Enter 'sessionid' cookie value: ")?;
let uid = read_line("Enter 'ds_user_id' 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<()> { async fn update_clock_note(client: &InstagramClient, config: &AppConfig) -> Result<()> {
let tz = config.parse_timezone()?; let tz = config.parse_timezone()?;
let now = chrono::Utc::now(); let now = chrono::Utc::now();
let time_format = config.note.resolved_time_format();
let note_text = format_note_time( let note_text = format_note_time(
now, now,
tz, tz,
&config.note.time_format, time_format,
&config.note.prefix, &config.note.prefix,
&config.note.suffix, &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<()> { async fn run_clock_loop(client: &InstagramClient, config: &AppConfig) -> Result<()> {
let tz = config.parse_timezone()?; let tz = config.parse_timezone()?;
let interval = Duration::from_secs(config.note.interval_seconds); let interval = Duration::from_secs(config.note.interval_seconds);
let time_format = config.note.resolved_time_format();
info!("=================================================="); info!("==================================================");
info!("Instagram Note Clock Started"); info!("Instagram Note Clock Started");
info!("Timezone: {}", config.instagram.timezone); info!("Timezone: {}", config.instagram.timezone);
info!("Refresh Interval: {} seconds", config.note.interval_seconds); 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); info!("Max Requests / Hour: {}", config.note.max_requests_per_hour);
if client.dry_run { if client.dry_run {
info!("MODE: DRY-RUN (No requests will be sent to Instagram)"); 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::select! {
_ = tokio::time::sleep(interval) => { _ = tokio::time::sleep(interval) => {
let now = chrono::Utc::now(); let now = chrono::Utc::now();
let time_format = config.note.resolved_time_format();
let note_text = format_note_time( let note_text = format_note_time(
now, now,
tz, tz,
&config.note.time_format, time_format,
&config.note.prefix, &config.note.prefix,
&config.note.suffix, &config.note.suffix,
); );