Add cookie tutorial and configurable 12h/24h time format
This commit is contained in:
parent
be32704914
commit
fb54e25ee2
6 changed files with 143 additions and 33 deletions
|
|
@ -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=""
|
||||
|
|
|
|||
37
README.md
37
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.
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
11
config.toml
11
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 = ""
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
34
src/main.rs
34
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<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,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue