some stuff IDK how to do ts

This commit is contained in:
lda committed 2026-05-27 03:08:54 +07:00 Verified
1 parent 6a547bceb9
commit ff4a93d967
16 files changed
+1107 -220

No files matched your search

+47 -1
View File
@@ -1 +1,47 @@
//! this deals with authentication. validate, refresh, and quota check.
//! this deals with authentication. validate, refresh, and quota check.
use crate::store::Account;
use crate::store::AccountStore;
pub async fn validate_account<S: AccountStore>(store: &S, id: &str) -> anyhow::Result<()> {
let account = store.load(id)?;
let (auth_manager, _) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
auth_manager
.auth()
.await
.ok_or_else(|| anyhow::anyhow!("not logged in!"))?;
Ok(())
}
// auth manager hides path
// path gets auth json
// auth json plus path gets auth manager
// so you lowk only need path and auth json
pub async fn refresh_account<S: AccountStore>(store: &mut S, id: &str) -> anyhow::Result<()> {
let account = store.load(id)?;
let (auth_manager, path) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
codexport::renew(&auth_manager).await?;
let new_auth_json = codexport::ephemeral_auth_json(&path)?;
let new_account = Account {
id: account.id,
nickname: account.nickname,
auth_json: new_auth_json,
};
store.upsert(new_account)?;
Ok(())
}
pub async fn delete_account<S: AccountStore>(store: &mut S, id: &str) -> anyhow::Result<()> {
let account = store.load(id)?;
let (auth_manager, _path) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
auth_manager.logout_with_revoke().await?;
store.delete(id)?;
Ok(())
}
pub async fn check_quota<S: AccountStore>(store: &S, id: &str) -> anyhow::Result<codexport::RateLimit> {
let account = store.load(id)?;
let (auth_manager, _path) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
let client = codexport::backend_client_from_auth_mgr(&auth_manager).await?;
let quota = codexport::quota(&client).await?;
Ok(quota)
}
+38 -57
View File
@@ -1,9 +1,5 @@
#[derive(clap::Parser, Debug)]
#[clap(
name = "csa",
version,
about = "A CLI tool to manage multiple Codex accounts"
)]
#[clap(version, about = "A CLI tool to manage multiple Codex accounts")]
pub struct Cli {
#[clap(subcommand)]
command: Commands,
@@ -14,7 +10,28 @@ pub enum Commands {
Show(ShowCommand),
Add(AddCommand),
Swap(SwapCommand),
Account(AccountCommand),
Refresh {
/// specify to refresh the account. Else refresh all accounts.
// this and AccountCommand.account at the same time is an error.
// chat how do i enforce above rule?
acc: Option<String>,
/// specify to only refresh the account if it's expired. Else refresh anyway.
#[clap(long)]
expired: bool,
},
// also logout
Remove {
/// specify to remove the account
// do NOT support remove all accounts.
acc: String,
},
Rename {
/// specify to rename the account
acc: String,
/// the new name for the account
new_name: String,
},
}
#[derive(Clone, Debug, clap::Parser)]
@@ -34,7 +51,7 @@ pub struct AddCommand {
pub name: Option<String>,
}
/// `csa swap acc1` would swap to acc1 if it exists. Else error out. `csa swap` would swap to the most capable account, which is determined by the internal logic of csa, and is usually the best valid account with the best availability.
///
///
/// to use something right after add/swap, use your favourite shell feature, the semicolon `;`.
// that looks like an emoji yo (`;`) wth
#[derive(Clone, Debug, clap::Parser)]
@@ -43,41 +60,6 @@ pub struct SwapCommand {
pub acc: Option<String>,
}
#[derive(Clone, Debug, clap::Parser)]
pub struct AccountCommand {
#[clap(subcommand)]
subcommand: AccountSubCommand,
}
#[derive(Clone, Debug, clap::Subcommand)]
pub enum AccountSubCommand {
Refresh {
/// specify to refresh the account. Else refresh all accounts.
// this and AccountCommand.account at the same time is an error.
// chat how do i enforce above rule?
acc: Option<String>,
/// specify to only refresh the account if it's expired. Else refresh anyway.
expired: bool,
},
// also logout
Remove {
/// specify to remove the account
// do NOT support remove all accounts.
acc: String,
},
Rename {
/// specify to rename the account
acc: String,
/// the new name for the account
new_name: String,
},
// same thing as Show above
Show {
/// specify to show the account. Else show all accounts.
acc: Option<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
@@ -114,8 +96,8 @@ mod tests {
}
#[test]
fn add_use_name() {
let cli = Cli::try_parse_from(["test", "add", "--use", "--name", "myacc"])
.expect("csa add --use --name myacc");
let cli =
Cli::try_parse_from(["test", "add", "--use", "myacc"]).expect("csa add --use myacc");
println!("{cli:#?}");
assert!(
matches!(cli.command, Commands::Add(AddCommand { use_rn: true, name: Some(name) }) if name == "myacc")
@@ -123,8 +105,7 @@ mod tests {
}
#[test]
fn add_name() {
let cli =
Cli::try_parse_from(["test", "add", "--name", "myacc"]).expect("csa add --name myacc");
let cli = Cli::try_parse_from(["test", "add", "myacc"]).expect("csa add myacc");
println!("{cli:#?}");
assert!(
matches!(cli.command, Commands::Add(AddCommand { use_rn: false, name: Some(name) }) if name == "myacc")
@@ -153,20 +134,20 @@ mod tests {
#[test]
fn refresh_acc() {
let cli = Cli::try_parse_from(["test", "refresh", "--acc", "acc1"])
.expect("csa refresh --acc acc1");
let cli = Cli::try_parse_from(["test", "refresh", "acc1"])
.expect("csa refresh acc1");
println!("{cli:#?}");
assert!(
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Refresh { acc: Some(acc), expired }, .. }) if acc == "acc1" && !expired)
matches!(cli.command, Commands::Refresh { acc: Some(acc), expired } if acc == "acc1" && !expired)
);
}
#[test]
fn refresh_acc_expired() {
let cli = Cli::try_parse_from(["test", "refresh", "--acc", "acc1", "--expired"])
.expect("csa refresh --acc acc1 --expired");
let cli = Cli::try_parse_from(["test", "refresh", "acc1", "--expired"])
.expect("csa refresh acc1 --expired");
println!("{cli:#?}");
assert!(
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Refresh { acc: Some(acc), expired }, .. }) if acc == "acc1" && expired)
matches!(cli.command, Commands::Refresh { acc: Some(acc), expired }if acc == "acc1" && expired)
);
}
#[test]
@@ -175,17 +156,17 @@ mod tests {
Cli::try_parse_from(["test", "refresh", "--expired"]).expect("csa refresh --expired");
println!("{cli:#?}");
assert!(
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Refresh { acc: None, expired }, .. }) if expired)
matches!(cli.command, Commands::Refresh { acc: None, expired } if expired)
);
}
#[test]
fn remove() {
let cli = Cli::try_parse_from(["test", "remove", "--acc", "acc1"])
.expect("csa remove --acc acc1");
let cli = Cli::try_parse_from(["test", "remove", "acc1"])
.expect("csa remove acc1");
println!("{cli:#?}");
assert!(
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Remove { acc }, .. }) if acc == "acc1")
matches!(cli.command, Commands::Remove { acc } if acc == "acc1")
);
}
@@ -202,7 +183,7 @@ mod tests {
.expect("csa rename acc1 acc_new");
println!("{cli:#?}");
assert!(
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Rename { acc, new_name }, .. }) if acc == "acc1" && new_name == "acc_new")
matches!(cli.command, Commands::Rename { acc, new_name } if acc == "acc1" && new_name == "acc_new")
);
}
+3 -1
View File
@@ -1,4 +1,6 @@
pub mod auth;
pub mod cli;
pub mod store;
pub mod util;
pub mod auth;
pub mod logic;
+374
View File
@@ -0,0 +1,374 @@
use std::time::Duration;
use crate::store::Account;
use anyhow::Context;
use codexport::AccountPlanType;
use codexport::RateLimit;
use codexport::RateLimitWindow;
/// what is the best account?
pub fn best_account(accounts: &[Account]) -> anyhow::Result<&Account> {
// Placeholder implementation - replace with actual logic to determine the best account
accounts
.first()
.ok_or_else(|| anyhow::anyhow!("No accounts available"))
}
/// plan weight.
/// this is going to be the weight per week. i just made ts up.
/// To get weight per session, divide by 6 (or 100/16)
///
/// this may align with my idea of if there is an account with api key, DON'T switch to it.
// that might mean we need to have a priority system.
pub fn plan_weight(plan: &AccountPlanType) -> anyhow::Result<u32> {
// Placeholder implementation - replace with actual logic to determine the weight of a plan
match plan {
// weekly faster than pluses session for some reason, so this may get LESS weight, idk
AccountPlanType::Free | AccountPlanType::Go => Ok(1),
// i think these are the same
AccountPlanType::Plus | AccountPlanType::Team | AccountPlanType::Business => Ok(10),
// pro 5x
AccountPlanType::ProLite => Ok(50),
// OG pro, pro 20x
AccountPlanType::Pro => Ok(200),
// idk below
AccountPlanType::Enterprise => anyhow::bail!("Enterprise plan is not supported"),
AccountPlanType::SelfServeBusinessUsageBased => {
anyhow::bail!("Self-Serve Business Usage-Based plan is not supported")
}
AccountPlanType::EnterpriseCbpUsageBased => {
anyhow::bail!("Enterprise CBP Usage-Based plan is not supported")
}
AccountPlanType::Edu => anyhow::bail!("Edu plan is not supported"),
AccountPlanType::Unknown => anyhow::bail!("Unknown plan type"),
}
}
fn now() -> std::time::SystemTime {
std::time::SystemTime::now()
}
fn now_u64() -> u64 {
now()
.duration_since(std::time::UNIX_EPOCH)
.expect("failed to calculate duration since UNIX epoch")
.as_secs()
}
fn stale_window(window: &RateLimitWindow, now: u64) -> Option<bool> {
window.resets_at.map(|resets_at| (resets_at as u64) < now)
}
/// how to interpret the rate limit window:
///
/// ```rust
/// # fn main() -> anyhow::Result<()> {
/// # use lda_codex_account::logic::window_weight;
/// # use codexport::RateLimitWindow;
/// # fn now_u64() -> u64 {
/// # std::time::SystemTime::now()
/// # .duration_since(std::time::UNIX_EPOCH)
/// # .expect("failed to calculate duration since UNIX epoch")
/// # .as_secs()
/// # }
/// # let window = RateLimitWindow {
/// # window_minutes: Some(60),
/// # used_percent: 50.0,
/// # resets_at: Some(now_u64() as i64 + 30 * 60),
/// # };
/// let (window_duration_minutes, window_weight) = window_weight(window)?;
/// # Ok(())
/// # }
/// ```
///
/// the weight is determined by the percentage of the window used and the time left until it resets.
/// the less of the window that is used and the less time left until it resets, the higher the weight.
///
/// this only runs if all the Options in the struct are Some.
/// If not, i advise not ranking the window at all.
///
/// Match the window with certain constants (300, 10080) and use that in tandem with plan weight
/// to get the final weight for the window.
/// [try_match_window]
pub fn window_weight(window: &RateLimitWindow) -> anyhow::Result<(u64, f64)> {
let now = now();
let window_duration = window
.window_minutes
.context("missing field 'window_minutes'")? as u64;
let used = if (0.0..=100.0).contains(&window.used_percent) {
window.used_percent
} else {
anyhow::bail!("invalid 'used_percent' value: {}", window.used_percent);
};
let left = 100.0 - used;
let resets_at = window.resets_at.context("missing field 'resets_at'")?;
let resets_at = std::time::UNIX_EPOCH + Duration::from_secs(resets_at as u64);
let time_left = resets_at
.duration_since(now)
.context("stale window")?
.as_secs();
Ok((window_duration, (left / time_left as f64)))
}
/// small function to match the window with certain constants (300, 10080) and use that in tandem with plan weight
pub fn try_match_window(window: &RateLimitWindow) -> anyhow::Result<Option<f64>> {
let (window_duration, weight) = window_weight(window)?;
match window_duration {
300 => Ok(Some(weight / 6.25)), // 5 hours
10080 => Ok(Some(weight)), // 1 week
_ => Ok(None), // unknown window duration, can not determine weight
}
}
pub fn account_weight(quota: &RateLimit) -> anyhow::Result<f64> {
let plan = quota.plan_type.context("missing field 'plan_type'")?;
let plan_weight = plan_weight(&plan)?;
let mut total_weight: f64 = 1.0;
if let Some(ref primary_window) = quota.primary
&& let Some(window_weight) = try_match_window(primary_window)?
{
total_weight *= window_weight;
}
if let Some(ref secondary_window) = quota.secondary
&& let Some(window_weight) = try_match_window(secondary_window)?
{
total_weight *= window_weight;
}
total_weight *= plan_weight as f64;
Ok(total_weight)
}
// lets step back and define how the BEST account is determined. we want to consider the following factors:
// 1. plan type (pro > plus > free)
// 2. rate limit window (how much of the window is used and how much time is left until it resets)
// - the primary window vs the secondary window (if the primary window is close to being used up,
// we might want to switch to an account with a less used primary window, even if it has a worse plan type)
// - a window weight is based on window duration. If it is equal to 5 hours, the weight is 1/6 of the plan weight,
// and if its 1 week, the weight is 1 of the plan weight.
// - the window weight is determined by the percentage of the window used and the time left until it resets.
// the logic could complicate if this account is at the end of a window, which means it will reset soon,
// so we might want to switch to it even if it has a worse plan type, because it will reset soon and we can use it for a long time after that.
// but if its window is just reset, we might want to switch away from it, because it will be used up soon.
// so we need to consider the time left until the window resets, and the percentage of the window that is used.
// the logic could complicate further if the secondary window is also close to being used up, or just started.
// imagine big Lda using this thing in an hour. An hour often depletes the plus 5h window. Because openai is just so bad.
// an account that has 50% of its primary window used, but it resets in 30 minutes.
// its going to be better than a fresh one but resets in 5 hours.
// so we need to consider the time left until the window resets, and the percentage of the window that is used.
/// standard tests
#[cfg(test)]
mod tests {
use super::*;
/// i lowk need this function outside
fn w(window_minutes: u64, used_percent: f64, resets_at: i64) -> codexport::RateLimitWindow {
codexport::RateLimitWindow {
window_minutes: Some(window_minutes as i64),
used_percent,
resets_at: Some(resets_at),
}
}
fn rl(
plan_type: AccountPlanType,
primary: Option<RateLimitWindow>,
secondary: Option<RateLimitWindow>,
) -> codexport::RateLimit {
codexport::RateLimit {
plan_type: Some(plan_type),
primary,
secondary,
limit_id: None,
limit_name: None,
rate_limit_reached_type: None,
credits: None,
}
}
/// these are gonna test some of my theories
mod lda_tests {
use super::*;
#[test]
fn weekly_better_than_five_hours() {
let five_hours_window = w(300, 50.0, now_u64() as i64 + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
let weekly_window = w(10080, 50.0, now_u64() as i64 + 30 * 60); // 1 week window, 50% used, resets in 30 minutes
let five_hours_weight = crate::logic::try_match_window(&five_hours_window)
.unwrap()
.unwrap();
let weekly_weight = crate::logic::try_match_window(&weekly_window)
.unwrap()
.unwrap();
println!(
"Five hours weight: {}, Weekly weight: {}",
five_hours_weight, weekly_weight
);
assert!(weekly_weight > five_hours_weight);
}
#[test]
fn five_hours_worse_than_30_minutes() {
let window = 300; // 5 hours window
let five_hours_window = w(window, 0.0, now_u64() as i64 + 300 * 60); // 5 hours window, 0% used, resets in 5 hours
let thirty_minutes_window = w(window, 0.0, now_u64() as i64 + 30 * 60); // 5 hours window, 0% used, resets in 30 minutes
let five_hours_weight = crate::logic::try_match_window(&five_hours_window)
.unwrap()
.unwrap();
let thirty_minutes_weight = crate::logic::try_match_window(&thirty_minutes_window)
.unwrap()
.unwrap();
println!(
"Five hours weight: {}, Thirty minutes weight: {}",
five_hours_weight, thirty_minutes_weight
);
assert!(five_hours_weight < thirty_minutes_weight);
}
#[test]
fn pro_20x_better_than_plus() {
let pro_window = w(300, 0.0, now_u64() as i64 + 30 * 60); // 5 hours window, 0% used, resets in 30 minutes
let plus_window = w(300, 0.0, now_u64() as i64 + 30 * 60); // 5 hours window, 0% used, resets in 30 minutes
let pro_quota = rl(AccountPlanType::Pro, Some(pro_window), None);
let plus_quota = rl(AccountPlanType::Plus, Some(plus_window), None);
let pro_weight = crate::logic::account_weight(&pro_quota).unwrap();
let plus_weight = crate::logic::account_weight(&plus_quota).unwrap();
println!("Pro weight: {}, Plus weight: {}", pro_weight, plus_weight);
assert!(pro_weight > plus_weight);
assert!(pro_weight > 19.9 * plus_weight);
assert!(pro_weight < 20.1 * plus_weight); // lmao
}
#[test]
fn weekly_0_left_is_dead() {
let now = now_u64() as i64;
let five_hours_window = w(300, 30.0, now + 30 * 60); // 5 hours window, 30% used, resets in 30 minutes
let weekly_window = w(10080, 100.0, now + 2 * 24 * 60); // 1 week window, 100% used, resets in 2 days
let mut rate_limit = rl(
AccountPlanType::Pro,
Some(five_hours_window),
Some(weekly_window),
);
rate_limit.rate_limit_reached_type = // this simulates the case where the account has hit the weekly limit
Some(codexport::RateLimitReachedType::RateLimitReached);
let total_weight = crate::logic::account_weight(&rate_limit).unwrap();
println!("Total weight: {}", total_weight);
assert!(total_weight <= 0.0); // its dead
}
#[test]
fn weekly_low_doesnt_care_about_5h() {
let now = now_u64() as i64;
let fh_left_1 = 90.0; // 90% left in 5 hours window
let fh_left_2 = 50.0; // 50% left in 5 hours window
let w_left = 4.0; // 4% left in weekly window
assert!(w_left * 6.25 < fh_left_1);
assert!(w_left * 6.25 < fh_left_2);
// so the weekly window is going to go out before the 5 hours window
let five_hours_window = w(300, fh_left_1, now + 30 * 60); // 5 hours window, 90% used, resets in 30 minutes
let five_hours_window_2 = w(300, fh_left_2, now + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
let weekly_window = w(10080, w_left, now + 2 * 24 * 60); // 1 week window, 4% used, resets in 2 days
let rate_limit = rl(
AccountPlanType::Pro,
Some(five_hours_window),
Some(weekly_window.clone()),
);
let rate_limit_2 = rl(
AccountPlanType::Pro,
Some(five_hours_window_2),
Some(weekly_window),
);
let total_weight = crate::logic::account_weight(&rate_limit).unwrap();
let total_weight_2 = crate::logic::account_weight(&rate_limit_2).unwrap();
println!("Total weight: {}", total_weight);
println!("Total weight 2: {}", total_weight_2);
assert!(total_weight > 0.0); // its not dead
assert!(total_weight_2 > 0.0); // its not dead either
// assert!(total_weight == total_weight_2); // its just as dead tho
assert!(total_weight >= total_weight_2);
// should i go through with that comparison?
// the 5 hours window is not going to go out before the weekly
// window, so it doesn't matter how much is left in the 5 hours
// window, because the weekly window is going to go out first
// however, for comparisons like this, should one still be lower than the other?
// in this case, they are the same, because the weekly window is the bottleneck.
// The API generously lets us finish turns even if there is 0% left.
}
}
#[test]
fn test_window_weight() {
let now = now_u64() as i64;
let window = w(300, 50.0, now + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
let (duration, weight) = crate::logic::window_weight(&window).unwrap();
println!("Duration: {} minutes, Weight: {}", duration, weight);
assert_eq!(duration, 300);
assert!(weight > 0.0);
}
#[test]
fn test_try_match_window() {
let now = now_u64() as i64;
let window = w(300, 50.0, now + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
let weight = crate::logic::try_match_window(&window).unwrap();
println!("Weight: {:?}", weight);
assert!(weight.is_some());
}
#[test]
fn test_try_match_window_unknown() {
let now = now_u64() as i64;
let window = w(12345, 50.0, now + 30 * 60); // unknown window duration
let weight = crate::logic::try_match_window(&window).unwrap();
println!("Weight: {:?}", weight);
assert!(weight.is_none());
}
#[test]
fn test_plan_weight() {
use codexport::AccountPlanType::*;
let plans = [Free, Go, Plus, Team, Business, ProLite, Pro]; // supported plans
for plan in plans {
let weight = crate::logic::plan_weight(&plan).unwrap();
println!("Plan: {:?}, Weight: {}", plan, weight);
}
}
#[test]
fn test_window_weight_invalid_used_percent() {
let now = now_u64() as i64;
// SUPA EDGES
for i in [150.0, 0.0_f64.next_down(), 100.0_f64.next_up()] {
let window = w(300, i, now + 30 * 60);
let result = crate::logic::window_weight(&window);
println!("Result for used_percent {}: {:?}", i, result);
assert!(result.is_err());
}
}
#[test]
fn test_window_weight_valid_used_percent() {
let now = now_u64() as i64;
for i in [0.0, 50.0, 100.0] {
let window = w(300, i, now + 30 * 60);
let result = crate::logic::window_weight(&window);
println!("Result for used_percent {}: {:?}", i, result);
assert!(result.is_ok());
if i == 100.0 {
assert_eq!(result.unwrap().1, 0.0); // if 100% used, weight should be 0
} else if i < 100.0 {
assert!(result.unwrap().1 > 0.0); // if not fully used, weight should be greater than 0
}
}
}
#[test]
fn test_window_weight_stale() {
let now = now_u64() as i64;
let window = w(300, 50.0, now - 30 * 60); // stale window, reset time is in the past
let result = crate::logic::window_weight(&window);
println!("Result: {:?}", result);
assert!(result.is_err());
}
}
+69 -1
View File
@@ -1,3 +1,71 @@
//! deals with storage of accounts.
//! updates, pull out, standard crud... all here.
//! 0 logic to do with the actual accounts, just storage and retrieval.
//! 0 logic to do with the actual accounts, just storage and retrieval.
use codexport::AuthDotJson;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Account {
pub id: String,
pub nickname: String,
pub auth_json: AuthDotJson,
}
/// this is the interface for storing accounts. We can have different
/// implementations, such as in-memory, file-based, or even a database. The key
/// is that the rest of the code should not care about how the accounts are
/// stored, as long as it can save, load, delete, list, and rename accounts.
pub trait AccountStore {
fn upsert(&mut self, account: Account) -> anyhow::Result<()>;
fn load(&self, id: &str) -> anyhow::Result<Account>;
fn delete(&mut self, id: &str) -> anyhow::Result<()>;
fn list(&self) -> anyhow::Result<Vec<Account>>;
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()>;
fn has(&self, id: &str) -> anyhow::Result<bool> {
Ok(self.load(id).is_ok())
}
fn get_all(&self) -> anyhow::Result<Vec<Account>> {
self.list()
}
fn clear(&mut self) -> anyhow::Result<()> {
for account in self.list()? {
self.delete(&account.id)?;
}
Ok(())
}
fn count(&self) -> anyhow::Result<usize> {
Ok(self.list()?.len())
}
fn is_empty(&self) -> anyhow::Result<bool> {
Ok(self.count()? == 0)
}
fn load_by_nickname(&self, nickname: &str) -> anyhow::Result<Account> {
let accounts = self.list()?;
for account in accounts {
if account.nickname == nickname {
return Ok(account);
}
}
Err(anyhow::anyhow!(
"Account with nickname '{}' not found",
nickname
))
}
fn delete_by_nickname(&mut self, nickname: &str) -> anyhow::Result<()> {
let account = self.load_by_nickname(nickname)?;
self.delete(&account.id)
}
fn rename_by_nickname(&mut self, old_nickname: &str, new_nickname: &str) -> anyhow::Result<()> {
let account = self.load_by_nickname(old_nickname)?;
self.rename(&account.id, new_nickname)
}
}
pub mod file;
pub mod in_memory;
pub mod file2;
// ooh! account strore has a lil PROBLEM. we should not let name dictate the key, of the inmem store. the key, is the account ID!
// the name is a nickname, which can be changed however. And it is not, the key!
// but nicknames are useful. we should support these.
+79
View File
@@ -0,0 +1,79 @@
//! file store!
use crate::store::Account;
use super::AccountStore;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Clone, Debug)]
pub struct FileStore {
dir: PathBuf,
}
impl FileStore {
pub fn new<P: AsRef<Path>>(dir: P) -> Self {
Self {
dir: dir.as_ref().to_path_buf(),
}
}
fn account_path(&self, id: &str) -> PathBuf {
self.dir.join(format!("{}.json", id))
}
pub fn ensure_dir(&self) -> anyhow::Result<()> {
if !self.dir.exists() {
fs::create_dir_all(&self.dir)?;
}
Ok(())
}
pub fn dir(&self) -> &Path {
&self.dir
}
}
impl AccountStore for FileStore {
fn upsert(&mut self, account: Account) -> anyhow::Result<()> {
let path = self.account_path(&account.id);
let data = serde_json::to_string_pretty(&account)?;
fs::write(path, data)?;
Ok(())
}
fn load(&self, id: &str) -> anyhow::Result<Account> {
let path = self.account_path(id);
let data = fs::read_to_string(path)?;
let account: Account = serde_json::from_str(&data)?;
Ok(account)
}
fn delete(&mut self, id: &str) -> anyhow::Result<()> {
let path = self.account_path(id);
fs::remove_file(path)?;
Ok(())
}
fn list(&self) -> anyhow::Result<Vec<Account>> {
let mut accounts = Vec::new();
for entry in fs::read_dir(&self.dir)? {
let entry = entry?;
if entry.path().is_file()
&& let Some(ext) = entry.path().extension()
&& ext == "json"
{
let data = fs::read_to_string(entry.path())?;
let account: Account = serde_json::from_str(&data)?;
accounts.push(account);
}
}
Ok(accounts)
}
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()> {
let mut account = self.load(id)?;
account.nickname = new_nickname.to_string();
self.upsert(account)
}
}
+123
View File
@@ -0,0 +1,123 @@
//! this setup gives you a codex home if you want to also
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::store::{Account, AccountStore};
/// this is a file-based implementation of the AccountStore trait. it stores the accounts in a json file, and the auth.json files in the codex home directories.
pub struct FileAccountStore {
pub db: Db,
pub homes: Homes,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Db {
#[serde(skip)]
pub path: std::path::PathBuf,
/// this is id: nickname. search for nicknames is O(n) but whatever, we are not expecting a huge number of accounts. and this is just a demo.
#[serde(flatten)]
store: HashMap<String, String>,
}
impl Db {
fn new(path: std::path::PathBuf) -> Self {
Self {
path,
store: HashMap::new(),
}
}
pub fn load(path: std::path::PathBuf) -> anyhow::Result<Self> {
if !path.exists() {
return Ok(Self::new(path));
}
let data = std::fs::read_to_string(&path)?;
let store: HashMap<_, _> = serde_json::from_str(&data)?;
let db = Self { path, store };
Ok(db)
}
/// call this after you make changes to the store, to save it to disk. this is separate from the upsert/delete functions, so you can make multiple changes and then save once.
pub fn save(&self) -> anyhow::Result<()> {
let data = serde_json::to_string_pretty(&self)?;
std::fs::write(&self.path, data)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Homes {
path: std::path::PathBuf,
}
impl Homes {
/// CODEX_HOME, not auth.json
pub fn get_home(&self, id: &str) -> std::path::PathBuf {
self.path.join(id)
}
/// the file that will deserialize into [`AuthDotJson`](codexport::AuthDotJson)
pub fn get_auth_json_path(&self, id: &str) -> std::path::PathBuf {
self.get_home(id).join("auth.json") // codex expects this file name
}
}
impl AccountStore for FileAccountStore {
fn upsert(&mut self, account: Account) -> anyhow::Result<()> {
{
let codex_home = self.homes.get_home(&account.id);
std::fs::create_dir_all(&codex_home)?;
codexport::save_auth_json_file(&codex_home, account.auth_json)?;
}
{
self.db.store.insert(account.id, account.nickname);
self.db.save()?;
}
Ok(())
}
fn load(&self, id: &str) -> anyhow::Result<Account> {
let nickname = self
.db
.store
.get(id)
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))?
.clone();
let auth_json = codexport::file_auth_json(&self.homes.get_auth_json_path(id))?;
Ok(Account {
id: id.to_string(),
nickname,
auth_json,
})
}
/// this will not delete the directory of the account, if in case you want to login again
fn delete(&mut self, id: &str) -> anyhow::Result<()> {
self.db.store.remove(id);
self.db.save()?;
Ok(())
}
fn list(&self) -> anyhow::Result<Vec<Account>> {
let mut accounts = Vec::new();
for (id, nickname) in &self.db.store {
let auth_json = codexport::file_auth_json(&self.homes.get_auth_json_path(id))?;
accounts.push(Account {
id: id.clone(),
nickname: nickname.clone(),
auth_json,
});
}
Ok(accounts)
}
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()> {
if let Some(nickname) = self.db.store.get_mut(id) {
*nickname = new_nickname.to_string();
self.db.save()?;
Ok(())
} else {
Err(anyhow::anyhow!("Account with id '{}' not found", id))
}
}
}
+46
View File
@@ -0,0 +1,46 @@
//! in memory store!
use crate::store::Account;
use super::AccountStore;
#[derive(Clone, Debug, Default)]
pub struct InMemoryStore {
/// id to account map!
accounts: std::collections::HashMap<String, Account>,
}
impl AccountStore for InMemoryStore {
fn upsert(&mut self, account: Account) -> anyhow::Result<()> {
self.accounts.insert(account.id.clone(), account);
Ok(())
}
fn load(&self, id: &str) -> anyhow::Result<Account> {
self.accounts
.get(id)
.cloned()
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))
}
fn delete(&mut self, id: &str) -> anyhow::Result<()> {
self.accounts
.remove(id)
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))?;
Ok(())
}
fn list(&self) -> anyhow::Result<Vec<Account>> {
Ok(self.accounts.values().cloned().collect())
}
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()> {
let account = self
.accounts
.get_mut(id)
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))?;
account.nickname = new_nickname.to_string();
Ok(())
}
}
+1 -1
View File
@@ -1 +1 @@
//! whatever looks like an general utility goes here.
//! whatever looks like an general utility goes here.