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)
|
# 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=""
|
||||||
|
|
|
||||||
37
README.md
37
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 = ""
|
||||||
|
|
|
||||||
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.
|
# 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 = ""
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
34
src/main.rs
34
src/main.rs
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue