config: accept user_id from file
This commit is contained in:
parent
c1e0481639
commit
08d5e8aa5e
2 changed files with 112 additions and 3 deletions
32
README.md
Normal file
32
README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Read Later Bot
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The bot reads a TOML config passed via `--config`. Most values are standard TOML types. The `user_id` field accepts multiple forms so it can be sourced from secrets managers.
|
||||||
|
|
||||||
|
### `user_id`
|
||||||
|
|
||||||
|
You can provide the Telegram user ID as:
|
||||||
|
|
||||||
|
- A number
|
||||||
|
- A numeric string
|
||||||
|
- A file path containing the numeric ID (useful for age/sops)
|
||||||
|
- An explicit file object
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
user_id = 123456789
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
user_id = "123456789"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
user_id = "/run/agenix/readlater-user-id"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
user_id = { file = "/run/agenix/readlater-user-id" }
|
||||||
|
```
|
||||||
83
src/main.rs
83
src/main.rs
|
|
@ -24,7 +24,7 @@ const DELETE_CONFIRM_TTL_SECS: u64 = 5 * 60;
|
||||||
const RESOURCE_PROMPT_TTL_SECS: u64 = 5 * 60;
|
const RESOURCE_PROMPT_TTL_SECS: u64 = 5 * 60;
|
||||||
const PAGE_SIZE: usize = 3;
|
const PAGE_SIZE: usize = 3;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Config {
|
struct Config {
|
||||||
token: String,
|
token: String,
|
||||||
user_id: u64,
|
user_id: u64,
|
||||||
|
|
@ -36,6 +36,26 @@ struct Config {
|
||||||
sync: Option<SyncConfig>,
|
sync: Option<SyncConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct ConfigFile {
|
||||||
|
token: String,
|
||||||
|
user_id: UserIdInput,
|
||||||
|
read_later_path: PathBuf,
|
||||||
|
finished_path: PathBuf,
|
||||||
|
resources_path: PathBuf,
|
||||||
|
data_dir: PathBuf,
|
||||||
|
retry_interval_seconds: Option<u64>,
|
||||||
|
sync: Option<SyncConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum UserIdInput {
|
||||||
|
Number(u64),
|
||||||
|
String(String),
|
||||||
|
File { file: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
struct SyncConfig {
|
struct SyncConfig {
|
||||||
repo_path: PathBuf,
|
repo_path: PathBuf,
|
||||||
|
|
@ -3059,10 +3079,67 @@ where
|
||||||
Err(last_err.unwrap_or_else(|| anyhow!("retry failed")))
|
Err(last_err.unwrap_or_else(|| anyhow!("retry failed")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_user_id(input: UserIdInput, config_dir: &Path) -> Result<u64> {
|
||||||
|
match input {
|
||||||
|
UserIdInput::Number(value) => Ok(value),
|
||||||
|
UserIdInput::String(raw) => resolve_user_id_string(&raw, config_dir),
|
||||||
|
UserIdInput::File { file } => {
|
||||||
|
let path = resolve_user_id_path(&file, config_dir);
|
||||||
|
read_user_id_file(&path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_user_id_string(raw: &str, config_dir: &Path) -> Result<u64> {
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err(anyhow!("user_id is empty"));
|
||||||
|
}
|
||||||
|
if trimmed.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
return parse_user_id_value(trimmed).context("parse user_id");
|
||||||
|
}
|
||||||
|
let path = resolve_user_id_path(Path::new(trimmed), config_dir);
|
||||||
|
read_user_id_file(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_user_id_path(path: &Path, config_dir: &Path) -> PathBuf {
|
||||||
|
if path.is_relative() {
|
||||||
|
config_dir.join(path)
|
||||||
|
} else {
|
||||||
|
path.to_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_user_id_file(path: &Path) -> Result<u64> {
|
||||||
|
let contents =
|
||||||
|
fs::read_to_string(path).with_context(|| format!("read user_id file {}", path.display()))?;
|
||||||
|
parse_user_id_value(contents.trim())
|
||||||
|
.with_context(|| format!("parse user_id from {}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_user_id_value(raw: &str) -> Result<u64> {
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err(anyhow!("user_id is empty"));
|
||||||
|
}
|
||||||
|
trimmed.parse::<u64>().context("parse user_id")
|
||||||
|
}
|
||||||
|
|
||||||
fn load_config(path: &Path) -> Result<Config> {
|
fn load_config(path: &Path) -> Result<Config> {
|
||||||
let contents = fs::read_to_string(path).with_context(|| format!("read config {}", path.display()))?;
|
let contents = fs::read_to_string(path).with_context(|| format!("read config {}", path.display()))?;
|
||||||
let config: Config = toml::from_str(&contents).context("parse config")?;
|
let config_file: ConfigFile = toml::from_str(&contents).context("parse config")?;
|
||||||
Ok(config)
|
let config_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
|
let user_id = resolve_user_id(config_file.user_id, config_dir)?;
|
||||||
|
Ok(Config {
|
||||||
|
token: config_file.token,
|
||||||
|
user_id,
|
||||||
|
read_later_path: config_file.read_later_path,
|
||||||
|
finished_path: config_file.finished_path,
|
||||||
|
resources_path: config_file.resources_path,
|
||||||
|
data_dir: config_file.data_dir,
|
||||||
|
retry_interval_seconds: config_file.retry_interval_seconds,
|
||||||
|
sync: config_file.sync,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_resource_files(dir: &Path) -> Result<Vec<PathBuf>> {
|
fn list_resource_files(dir: &Path) -> Result<Vec<PathBuf>> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue