Compare commits
3 Commits
claude-cod
...
claude
Author | SHA1 | Date | |
---|---|---|---|
a4f7f867f5
|
|||
81db8cfe29
|
|||
6513d626de
|
@ -15,7 +15,8 @@
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git add:*)"
|
||||
"Bash(git add:*)",
|
||||
"Bash(rg:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ path = "src/alias.rs"
|
||||
seahorse = "*"
|
||||
reqwest = { version = "*", features = ["blocking", "json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
shellexpand = "*"
|
||||
config = "*"
|
||||
serde = "*"
|
||||
serde_json = "*"
|
||||
|
249
src/data.rs
249
src/data.rs
@ -5,94 +5,70 @@ use std::fs;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::env;
|
||||
|
||||
pub fn data_file(s: &str) -> String {
|
||||
// 新しい設定ディレクトリ(優先)
|
||||
let new_config_dir = "/.config/syui/ai/bot/";
|
||||
let mut new_path = shellexpand::tilde("~").to_string();
|
||||
new_path.push_str(&new_config_dir);
|
||||
|
||||
// 旧設定ディレクトリ(互換性のため)
|
||||
let old_config_dir = "/.config/ai/";
|
||||
let mut old_path = shellexpand::tilde("~").to_string();
|
||||
old_path.push_str(&old_config_dir);
|
||||
|
||||
// 新しいディレクトリを作成
|
||||
let new_dir = Path::new(&new_path);
|
||||
if !new_dir.is_dir() {
|
||||
let _ = fs::create_dir_all(new_path.clone());
|
||||
/// 設定ディレクトリのベースパス
|
||||
const CONFIG_BASE_DIR: &str = "~/.config/syui/ai/bot";
|
||||
|
||||
/// ホームディレクトリパスを展開するユーティリティ関数
|
||||
/// "~"で始まるパスをユーザーのホームディレクトリに展開します
|
||||
fn expand_home_path(path: &str) -> PathBuf {
|
||||
if path.starts_with("~") {
|
||||
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||
let path_without_tilde = path.strip_prefix("~/").unwrap_or(&path[1..]);
|
||||
PathBuf::from(home).join(path_without_tilde)
|
||||
} else {
|
||||
PathBuf::from(path)
|
||||
}
|
||||
|
||||
let filename = match &*s {
|
||||
"toml" => "token.toml",
|
||||
"json" => "token.json",
|
||||
"refresh" => "refresh.toml",
|
||||
_ => &format!(".{}", s),
|
||||
}
|
||||
|
||||
/// 設定ディレクトリのベースパスを取得し、必要に応じて作成する
|
||||
fn get_config_base_path() -> PathBuf {
|
||||
let path = expand_home_path(CONFIG_BASE_DIR);
|
||||
if !path.is_dir() {
|
||||
let _ = fs::create_dir_all(&path);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// サブディレクトリを含む設定パスを取得し、必要に応じて作成する
|
||||
fn get_config_path(subdir: &str) -> PathBuf {
|
||||
let base_path = get_config_base_path();
|
||||
let path = if subdir.is_empty() {
|
||||
base_path
|
||||
} else {
|
||||
base_path.join(subdir)
|
||||
};
|
||||
|
||||
let new_file = new_path.clone() + filename;
|
||||
let old_file = old_path + filename;
|
||||
|
||||
// 新しいパスにファイルが存在する場合は新しいパスを使用
|
||||
if Path::new(&new_file).exists() {
|
||||
return new_file;
|
||||
if !path.is_dir() {
|
||||
let _ = fs::create_dir_all(&path);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
pub fn data_file(s: &str) -> String {
|
||||
let path = get_config_base_path();
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
// 旧パスにファイルが存在し、新しいパスに存在しない場合は移行を試行
|
||||
if Path::new(&old_file).exists() && !Path::new(&new_file).exists() {
|
||||
if let Ok(_) = fs::copy(&old_file, &new_file) {
|
||||
eprintln!("Migrated config file: {} -> {}", old_file, new_file);
|
||||
return new_file;
|
||||
}
|
||||
match s {
|
||||
"toml" => format!("{}/token.toml", path_str),
|
||||
"json" => format!("{}/token.json", path_str),
|
||||
"refresh" => format!("{}/refresh.toml", path_str),
|
||||
_ => format!("{}/.{}", path_str, s),
|
||||
}
|
||||
|
||||
// デフォルトは新しいパス
|
||||
new_file
|
||||
}
|
||||
|
||||
pub fn log_file(s: &str) -> String {
|
||||
// 新しい設定ディレクトリ(優先)
|
||||
let new_log_dir = "/.config/syui/ai/bot/txt/";
|
||||
let mut new_path = shellexpand::tilde("~").to_string();
|
||||
new_path.push_str(&new_log_dir);
|
||||
let path = get_config_path("txt");
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
// 旧設定ディレクトリ(互換性のため)
|
||||
let old_log_dir = "/.config/ai/txt/";
|
||||
let mut old_path = shellexpand::tilde("~").to_string();
|
||||
old_path.push_str(&old_log_dir);
|
||||
|
||||
// 新しいディレクトリを作成
|
||||
let new_dir = Path::new(&new_path);
|
||||
if !new_dir.is_dir() {
|
||||
let _ = fs::create_dir_all(new_path.clone());
|
||||
match s {
|
||||
"n1" => format!("{}/notify_cid.txt", path_str),
|
||||
"n2" => format!("{}/notify_cid_run.txt", path_str),
|
||||
"c1" => format!("{}/comment_cid.txt", path_str),
|
||||
_ => format!("{}/{}", path_str, s),
|
||||
}
|
||||
|
||||
let filename = match &*s {
|
||||
"n1" => "notify_cid.txt",
|
||||
"n2" => "notify_cid_run.txt",
|
||||
"c1" => "comment_cid.txt",
|
||||
_ => s,
|
||||
};
|
||||
|
||||
let new_file = new_path.clone() + filename;
|
||||
let old_file = old_path + filename;
|
||||
|
||||
// 新しいパスにファイルが存在する場合は新しいパスを使用
|
||||
if Path::new(&new_file).exists() {
|
||||
return new_file;
|
||||
}
|
||||
|
||||
// 旧パスにファイルが存在し、新しいパスに存在しない場合は移行を試行
|
||||
if Path::new(&old_file).exists() && !Path::new(&new_file).exists() {
|
||||
if let Ok(_) = fs::copy(&old_file, &new_file) {
|
||||
eprintln!("Migrated log file: {} -> {}", old_file, new_file);
|
||||
return new_file;
|
||||
}
|
||||
}
|
||||
|
||||
// デフォルトは新しいパス
|
||||
new_file
|
||||
}
|
||||
|
||||
impl Token {
|
||||
@ -185,15 +161,14 @@ pub struct BaseUrl {
|
||||
}
|
||||
|
||||
pub fn url(s: &str) -> String {
|
||||
let s = String::from(s);
|
||||
let data = Data::new().unwrap();
|
||||
let data = Data {
|
||||
host: data.host,
|
||||
password: data.password,
|
||||
handle: data.handle,
|
||||
did: data.did,
|
||||
access: data.access,
|
||||
refresh: data.refresh,
|
||||
let data = match Data::new() {
|
||||
Ok(data) => data,
|
||||
Err(_) => {
|
||||
eprintln!("Error: Configuration file not found at {}/token.toml",
|
||||
get_config_base_path().display());
|
||||
eprintln!("Please run 'aibot login <handle> -p <password>' first to authenticate.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let t = "https://".to_string() + &data.host.to_string() + &"/xrpc/".to_string();
|
||||
let baseurl = BaseUrl {
|
||||
@ -250,29 +225,29 @@ pub fn url(s: &str) -> String {
|
||||
"follows" => t.to_string() + &baseurl.follows,
|
||||
"followers" => t.to_string() + &baseurl.followers,
|
||||
"feed_get" => t.to_string() + &baseurl.feed_get,
|
||||
_ => s,
|
||||
_ => s.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_toml(s: &str) -> String {
|
||||
let s = String::from(s);
|
||||
let data = Data::new().unwrap();
|
||||
let data = Data {
|
||||
host: data.host,
|
||||
password: data.password,
|
||||
handle: data.handle,
|
||||
did: data.did,
|
||||
access: data.access,
|
||||
refresh: data.refresh,
|
||||
let data = match Data::new() {
|
||||
Ok(data) => data,
|
||||
Err(_) => {
|
||||
eprintln!("Error: Configuration file not found at {}/token.toml",
|
||||
get_config_base_path().display());
|
||||
eprintln!("Please run 'aibot login <handle> -p <password>' first to authenticate.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
match &*s {
|
||||
|
||||
match s {
|
||||
"host" => data.host,
|
||||
"password" => data.password,
|
||||
"handle" => data.handle,
|
||||
"did" => data.did,
|
||||
"access" => data.access,
|
||||
"refresh" => data.refresh,
|
||||
_ => s,
|
||||
_ => s.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,41 +263,41 @@ pub fn c_refresh(access: &str, refresh: &str) {
|
||||
}
|
||||
|
||||
pub fn data_refresh(s: &str) -> String {
|
||||
let s = String::from(s);
|
||||
|
||||
let data = Data::new().unwrap();
|
||||
let data = Data {
|
||||
host: data.host,
|
||||
password: data.password,
|
||||
handle: data.handle,
|
||||
did: data.did,
|
||||
access: data.access,
|
||||
refresh: data.refresh,
|
||||
let data = match Data::new() {
|
||||
Ok(data) => data,
|
||||
Err(_) => {
|
||||
eprintln!("Error: Configuration file not found at {}/token.toml",
|
||||
get_config_base_path().display());
|
||||
eprintln!("Please run 'aibot login <handle> -p <password>' first to authenticate.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut _file = match Refresh::new()
|
||||
{
|
||||
let mut _file = match Refresh::new() {
|
||||
Err(_why) => c_refresh(&data.access, &data.refresh),
|
||||
Ok(_) => println!(""),
|
||||
};
|
||||
let refresh = Refresh::new().unwrap();
|
||||
let refresh = Refresh {
|
||||
access: refresh.access,
|
||||
refresh: refresh.refresh,
|
||||
|
||||
let refresh = match Refresh::new() {
|
||||
Ok(refresh) => refresh,
|
||||
Err(_) => {
|
||||
eprintln!("Error: Refresh token file not found.");
|
||||
eprintln!("Please run 'aibot login <handle> -p <password>' to re-authenticate.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
match &*s {
|
||||
|
||||
match s {
|
||||
"access" => refresh.access,
|
||||
"refresh" => refresh.refresh,
|
||||
_ => s,
|
||||
_ => s.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_scpt(s: &str) -> String {
|
||||
let s = String::from(s);
|
||||
let file = "/.config/ai/scpt/".to_owned() + &s + &".zsh";
|
||||
let mut f = shellexpand::tilde("~").to_string();
|
||||
f.push_str(&file);
|
||||
return f;
|
||||
let mut path = get_config_path("scpt");
|
||||
path.push(format!("{}.zsh", s));
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -590,7 +565,20 @@ pub fn w_cfg(h: &str, res: &str, password: &str) {
|
||||
let mut f = fs::File::create(f.clone()).unwrap();
|
||||
let mut ff = fs::File::create(ff.clone()).unwrap();
|
||||
f.write_all(&res.as_bytes()).unwrap();
|
||||
let json: Token = serde_json::from_str(&res).unwrap();
|
||||
// Check if response contains an error
|
||||
if res.contains("\"error\"") {
|
||||
eprintln!("Authentication error: {}", res);
|
||||
return;
|
||||
}
|
||||
|
||||
let json: Token = match serde_json::from_str(&res) {
|
||||
Ok(token) => token,
|
||||
Err(e) => {
|
||||
eprintln!("JSON parse error: {}", e);
|
||||
eprintln!("Response: {}", res);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let datas = Data {
|
||||
host: h.to_string(),
|
||||
password: password.to_string(),
|
||||
@ -659,11 +647,10 @@ pub fn w_cid(cid: String, file: String, t: bool) -> bool {
|
||||
}
|
||||
|
||||
pub fn c_follow_all() {
|
||||
let file = "/.config/ai/scpt/follow_all.zsh";
|
||||
let mut f = shellexpand::tilde("~").to_string();
|
||||
f.push_str(&file);
|
||||
let path = expand_home_path("~/.config/syui/ai/bot/scpt/follow_all.zsh");
|
||||
|
||||
use std::process::Command;
|
||||
let output = Command::new(&f).output().expect("zsh");
|
||||
let output = Command::new(path.to_str().unwrap()).output().expect("zsh");
|
||||
let d = String::from_utf8_lossy(&output.stdout);
|
||||
let d = "\n".to_owned() + &d.to_string();
|
||||
println!("{}", d);
|
||||
@ -673,9 +660,10 @@ pub fn c_openai_key(c: &Context) {
|
||||
let api = c.args[0].to_string();
|
||||
let o = "api='".to_owned() + &api.to_string() + &"'".to_owned();
|
||||
let o = o.to_string();
|
||||
let l = shellexpand::tilde("~") + "/.config/ai/openai.toml";
|
||||
let l = l.to_string();
|
||||
let mut l = fs::File::create(l).unwrap();
|
||||
|
||||
let path = expand_home_path("~/.config/syui/ai/bot/openai.toml");
|
||||
|
||||
let mut l = fs::File::create(&path).unwrap();
|
||||
if o != "" {
|
||||
l.write_all(&o.as_bytes()).unwrap();
|
||||
}
|
||||
@ -684,9 +672,10 @@ pub fn c_openai_key(c: &Context) {
|
||||
|
||||
impl Open {
|
||||
pub fn new() -> Result<Self, ConfigError> {
|
||||
let d = shellexpand::tilde("~") + "/.config/ai/openai.toml";
|
||||
let path = expand_home_path("~/.config/syui/ai/bot/openai.toml");
|
||||
|
||||
let s = Config::builder()
|
||||
.add_source(File::with_name(&d))
|
||||
.add_source(File::with_name(path.to_str().unwrap()))
|
||||
.add_source(config::Environment::with_prefix("APP"))
|
||||
.build()?;
|
||||
s.try_deserialize()
|
||||
|
82
src/main.rs
82
src/main.rs
@ -105,7 +105,7 @@ fn main() {
|
||||
.command(
|
||||
Command::new("login")
|
||||
.alias("l")
|
||||
.description("l <handle> -p <password>\n\t\t\tl <handle> -p <password> -s <server>")
|
||||
.description("l <handle> -p <password>\n\t\t\tl <handle> -p <password> -s <server>\n\t\t\tl <handle> -p <password> -c <2fa_code>")
|
||||
.action(token)
|
||||
.flag(
|
||||
Flag::new("password", FlagType::String)
|
||||
@ -117,6 +117,11 @@ fn main() {
|
||||
.description("server flag")
|
||||
.alias("s"),
|
||||
)
|
||||
.flag(
|
||||
Flag::new("code", FlagType::String)
|
||||
.description("2FA authentication code")
|
||||
.alias("c"),
|
||||
)
|
||||
)
|
||||
.command(
|
||||
Command::new("refresh")
|
||||
@ -503,18 +508,19 @@ fn openai_key(c: &Context) {
|
||||
}
|
||||
|
||||
fn token(c: &Context) {
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Handle is required.");
|
||||
eprintln!("Usage: aibot login <handle> -p <password>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
if let Ok(p) = c.string_flag("password") {
|
||||
if let Ok(s) = c.string_flag("server") {
|
||||
let res = token::post_request(m.to_string(), p.to_string(), s.to_string()).await;
|
||||
w_cfg(&s, &res, &p);
|
||||
} else {
|
||||
let res =
|
||||
token::post_request(m.to_string(), p.to_string(), "bsky.social".to_string())
|
||||
.await;
|
||||
w_cfg(&"bsky.social", &res, &p);
|
||||
}
|
||||
let server = c.string_flag("server").unwrap_or_else(|_| "bsky.social".to_string());
|
||||
let code = c.string_flag("code").ok();
|
||||
|
||||
let res = token::post_request(m.to_string(), p.to_string(), server.to_string(), code).await;
|
||||
w_cfg(&server, &res, &p);
|
||||
}
|
||||
};
|
||||
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
|
||||
@ -530,7 +536,7 @@ fn refresh(_c: &Context) {
|
||||
let m = data_toml(&"handle");
|
||||
let p = data_toml(&"password");
|
||||
let s = data_toml(&"host");
|
||||
let res = token::post_request(m.to_string(), p.to_string(), s.to_string()).await;
|
||||
let res = token::post_request(m.to_string(), p.to_string(), s.to_string(), None).await;
|
||||
w_cfg(&s, &res, &p);
|
||||
} else {
|
||||
w_refresh(&res);
|
||||
@ -599,6 +605,11 @@ fn timeline(c: &Context) {
|
||||
|
||||
fn post(c: &Context) {
|
||||
refresh(c);
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Post text is required.");
|
||||
eprintln!("Usage: aibot post <text>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
if let Ok(link) = c.string_flag("link") {
|
||||
@ -618,6 +629,11 @@ fn post(c: &Context) {
|
||||
|
||||
fn delete(c: &Context) {
|
||||
refresh(c);
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Record key is required.");
|
||||
eprintln!("Usage: aibot delete <rkey> --col <collection>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
if let Ok(col) = c.string_flag("col") {
|
||||
@ -631,6 +647,11 @@ fn delete(c: &Context) {
|
||||
|
||||
fn like(c: &Context) {
|
||||
refresh(c);
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: CID is required.");
|
||||
eprintln!("Usage: aibot like <cid> --uri <uri>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
if let Ok(uri) = c.string_flag("uri") {
|
||||
@ -783,6 +804,11 @@ fn game_login(c: &Context) {
|
||||
|
||||
fn repost(c: &Context) {
|
||||
refresh(c);
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: CID is required.");
|
||||
eprintln!("Usage: aibot repost <cid> --uri <uri>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
if let Ok(uri) = c.string_flag("uri") {
|
||||
@ -796,6 +822,11 @@ fn repost(c: &Context) {
|
||||
|
||||
fn follow(c: &Context) {
|
||||
refresh(c);
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Handle is required.");
|
||||
eprintln!("Usage: aibot follow <handle>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
let handle = data_toml(&"handle");
|
||||
@ -819,10 +850,10 @@ fn profile(c: &Context) {
|
||||
let h = async {
|
||||
if c.args.len() == 0 {
|
||||
let j = profile::get_request(data_toml(&"handle")).await;
|
||||
println!("{}", j);
|
||||
print!("{}", j);
|
||||
} else {
|
||||
let j = profile::get_request(c.args[0].to_string()).await;
|
||||
println!("{}", j);
|
||||
print!("{}", j);
|
||||
}
|
||||
};
|
||||
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
|
||||
@ -831,6 +862,11 @@ fn profile(c: &Context) {
|
||||
|
||||
fn mention(c: &Context) {
|
||||
refresh(c);
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Handle is required.");
|
||||
eprintln!("Usage: aibot mention <handle>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
let str = profile::get_request(m.to_string()).await;
|
||||
@ -860,6 +896,11 @@ fn mention(c: &Context) {
|
||||
|
||||
fn reply(c: &Context) {
|
||||
refresh(c);
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Reply text is required.");
|
||||
eprintln!("Usage: aibot reply <text> --cid <cid> --uri <uri>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
if let Ok(cid) = c.string_flag("cid") {
|
||||
@ -899,6 +940,11 @@ fn reply(c: &Context) {
|
||||
|
||||
#[tokio::main]
|
||||
async fn c_img_upload(c: &Context) -> reqwest::Result<()> {
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Image file path is required.");
|
||||
eprintln!("Usage: aibot img_upload <image_file>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let token = data_refresh(&"access");
|
||||
let atoken = "Authorization: Bearer ".to_owned() + &token;
|
||||
let con = "Content-Type: image/png";
|
||||
@ -930,6 +976,11 @@ fn img_upload(c: &Context) {
|
||||
}
|
||||
|
||||
fn img_post(c: &Context) {
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Text is required.");
|
||||
eprintln!("Usage: aibot img_post <text> --link <link> --cid <cid> --uri <uri>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let link = c.string_flag("link").unwrap();
|
||||
let cid = c.string_flag("cid").unwrap();
|
||||
@ -976,6 +1027,11 @@ fn reply_og(c: &Context) {
|
||||
}
|
||||
|
||||
fn openai(c: &Context) {
|
||||
if c.args.is_empty() {
|
||||
eprintln!("Error: Message is required.");
|
||||
eprintln!("Usage: aibot openai <message>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let m = c.args[0].to_string();
|
||||
let h = async {
|
||||
let str = openai::post_request(m.to_string()).await;
|
||||
|
@ -1,12 +1,17 @@
|
||||
use crate::http_client::HttpClient;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub async fn post_request(handle: String, pass: String, host: String) -> String {
|
||||
pub async fn post_request(handle: String, pass: String, host: String, auth_factor_token: Option<String>) -> String {
|
||||
let url = format!("https://{}/xrpc/com.atproto.server.createSession", host);
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("identifier", &handle);
|
||||
map.insert("password", &pass);
|
||||
|
||||
// Add 2FA code if provided
|
||||
if let Some(code) = &auth_factor_token {
|
||||
map.insert("authFactorToken", code);
|
||||
}
|
||||
|
||||
let client = HttpClient::new();
|
||||
|
||||
|
Reference in New Issue
Block a user