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)
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=""

View file

@ -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.

View file

@ -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 = ""

View file

@ -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 = ""

View file

@ -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());

View file

@ -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<String>,
#[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,
);