4 Commits

Author SHA1 Message Date
d0746a7af6 update handle
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 11m36s
2025-06-16 20:41:03 +09:00
a4f7f867f5 fix cmd
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 12m45s
2025-06-11 11:49:53 +09:00
81db8cfe29 add 2fa
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run
2025-06-11 11:40:21 +09:00
6513d626de rm shellexpand
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 11m53s
2025-06-09 01:43:05 +09:00
6 changed files with 262 additions and 148 deletions

View File

@ -15,7 +15,8 @@
"Bash(mkdir:*)", "Bash(mkdir:*)",
"Bash(chmod:*)", "Bash(chmod:*)",
"Bash(git checkout:*)", "Bash(git checkout:*)",
"Bash(git add:*)" "Bash(git add:*)",
"Bash(rg:*)"
], ],
"deny": [] "deny": []
} }

View File

@ -17,7 +17,6 @@ path = "src/alias.rs"
seahorse = "*" seahorse = "*"
reqwest = { version = "*", features = ["blocking", "json"] } reqwest = { version = "*", features = ["blocking", "json"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
shellexpand = "*"
config = "*" config = "*"
serde = "*" serde = "*"
serde_json = "*" serde_json = "*"

View File

@ -5,94 +5,70 @@ use std::fs;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::PathBuf;
use std::env;
pub fn data_file(s: &str) -> String { /// 設定ディレクトリのベースパス
// 新しい設定ディレクトリ(優先) const CONFIG_BASE_DIR: &str = "~/.config/syui/ai/bot";
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(); fn expand_home_path(path: &str) -> PathBuf {
old_path.push_str(&old_config_dir); 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..]);
let new_dir = Path::new(&new_path); PathBuf::from(home).join(path_without_tilde)
if !new_dir.is_dir() { } else {
let _ = fs::create_dir_all(new_path.clone()); PathBuf::from(path)
} }
}
let filename = match &*s { /// 設定ディレクトリのベースパスを取得し、必要に応じて作成する
"toml" => "token.toml", fn get_config_base_path() -> PathBuf {
"json" => "token.json", let path = expand_home_path(CONFIG_BASE_DIR);
"refresh" => "refresh.toml", if !path.is_dir() {
_ => &format!(".{}", s), 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; if !path.is_dir() {
let old_file = old_path + filename; let _ = fs::create_dir_all(&path);
// 新しいパスにファイルが存在する場合は新しいパスを使用
if Path::new(&new_file).exists() {
return new_file;
} }
path
}
// 旧パスにファイルが存在し、新しいパスに存在しない場合は移行を試行 pub fn data_file(s: &str) -> String {
if Path::new(&old_file).exists() && !Path::new(&new_file).exists() { let path = get_config_base_path();
if let Ok(_) = fs::copy(&old_file, &new_file) { let path_str = path.to_string_lossy();
eprintln!("Migrated config file: {} -> {}", old_file, new_file);
return new_file;
}
}
// デフォルトは新しいパス match s {
new_file "toml" => format!("{}/token.toml", path_str),
"json" => format!("{}/token.json", path_str),
"refresh" => format!("{}/refresh.toml", path_str),
_ => format!("{}/.{}", path_str, s),
}
} }
pub fn log_file(s: &str) -> String { pub fn log_file(s: &str) -> String {
// 新しい設定ディレクトリ(優先) let path = get_config_path("txt");
let new_log_dir = "/.config/syui/ai/bot/txt/"; let path_str = path.to_string_lossy();
let mut new_path = shellexpand::tilde("~").to_string();
new_path.push_str(&new_log_dir);
// 旧設定ディレクトリ(互換性のため) match s {
let old_log_dir = "/.config/ai/txt/"; "n1" => format!("{}/notify_cid.txt", path_str),
let mut old_path = shellexpand::tilde("~").to_string(); "n2" => format!("{}/notify_cid_run.txt", path_str),
old_path.push_str(&old_log_dir); "c1" => format!("{}/comment_cid.txt", path_str),
_ => format!("{}/{}", path_str, s),
// 新しいディレクトリを作成
let new_dir = Path::new(&new_path);
if !new_dir.is_dir() {
let _ = fs::create_dir_all(new_path.clone());
} }
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 { impl Token {
@ -185,15 +161,14 @@ pub struct BaseUrl {
} }
pub fn url(s: &str) -> String { pub fn url(s: &str) -> String {
let s = String::from(s); let data = match Data::new() {
let data = Data::new().unwrap(); Ok(data) => data,
let data = Data { Err(_) => {
host: data.host, eprintln!("Error: Configuration file not found at {}/token.toml",
password: data.password, get_config_base_path().display());
handle: data.handle, eprintln!("Please run 'aibot login <handle> -p <password>' first to authenticate.");
did: data.did, std::process::exit(1);
access: data.access, }
refresh: data.refresh,
}; };
let t = "https://".to_string() + &data.host.to_string() + &"/xrpc/".to_string(); let t = "https://".to_string() + &data.host.to_string() + &"/xrpc/".to_string();
let baseurl = BaseUrl { let baseurl = BaseUrl {
@ -250,29 +225,29 @@ pub fn url(s: &str) -> String {
"follows" => t.to_string() + &baseurl.follows, "follows" => t.to_string() + &baseurl.follows,
"followers" => t.to_string() + &baseurl.followers, "followers" => t.to_string() + &baseurl.followers,
"feed_get" => t.to_string() + &baseurl.feed_get, "feed_get" => t.to_string() + &baseurl.feed_get,
_ => s, _ => s.to_string(),
} }
} }
pub fn data_toml(s: &str) -> String { pub fn data_toml(s: &str) -> String {
let s = String::from(s); let data = match Data::new() {
let data = Data::new().unwrap(); Ok(data) => data,
let data = Data { Err(_) => {
host: data.host, eprintln!("Error: Configuration file not found at {}/token.toml",
password: data.password, get_config_base_path().display());
handle: data.handle, eprintln!("Please run 'aibot login <handle> -p <password>' first to authenticate.");
did: data.did, std::process::exit(1);
access: data.access, }
refresh: data.refresh,
}; };
match &*s {
match s {
"host" => data.host, "host" => data.host,
"password" => data.password, "password" => data.password,
"handle" => data.handle, "handle" => data.handle,
"did" => data.did, "did" => data.did,
"access" => data.access, "access" => data.access,
"refresh" => data.refresh, "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 { pub fn data_refresh(s: &str) -> String {
let s = String::from(s); let data = match Data::new() {
Ok(data) => data,
let data = Data::new().unwrap(); Err(_) => {
let data = Data { eprintln!("Error: Configuration file not found at {}/token.toml",
host: data.host, get_config_base_path().display());
password: data.password, eprintln!("Please run 'aibot login <handle> -p <password>' first to authenticate.");
handle: data.handle, std::process::exit(1);
did: data.did, }
access: data.access,
refresh: data.refresh,
}; };
let mut _file = match Refresh::new() let mut _file = match Refresh::new() {
{
Err(_why) => c_refresh(&data.access, &data.refresh), Err(_why) => c_refresh(&data.access, &data.refresh),
Ok(_) => println!(""), Ok(_) => println!(""),
}; };
let refresh = Refresh::new().unwrap();
let refresh = Refresh { let refresh = match Refresh::new() {
access: refresh.access, Ok(refresh) => refresh,
refresh: 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, "access" => refresh.access,
"refresh" => refresh.refresh, "refresh" => refresh.refresh,
_ => s, _ => s.to_string(),
} }
} }
pub fn data_scpt(s: &str) -> String { pub fn data_scpt(s: &str) -> String {
let s = String::from(s); let mut path = get_config_path("scpt");
let file = "/.config/ai/scpt/".to_owned() + &s + &".zsh"; path.push(format!("{}.zsh", s));
let mut f = shellexpand::tilde("~").to_string(); path.to_string_lossy().to_string()
f.push_str(&file);
return f;
} }
#[derive(Serialize, Deserialize)] #[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 f = fs::File::create(f.clone()).unwrap();
let mut ff = fs::File::create(ff.clone()).unwrap(); let mut ff = fs::File::create(ff.clone()).unwrap();
f.write_all(&res.as_bytes()).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 { let datas = Data {
host: h.to_string(), host: h.to_string(),
password: password.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() { pub fn c_follow_all() {
let file = "/.config/ai/scpt/follow_all.zsh"; let path = expand_home_path("~/.config/syui/ai/bot/scpt/follow_all.zsh");
let mut f = shellexpand::tilde("~").to_string();
f.push_str(&file);
use std::process::Command; 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 = String::from_utf8_lossy(&output.stdout);
let d = "\n".to_owned() + &d.to_string(); let d = "\n".to_owned() + &d.to_string();
println!("{}", d); println!("{}", d);
@ -673,9 +660,10 @@ pub fn c_openai_key(c: &Context) {
let api = c.args[0].to_string(); let api = c.args[0].to_string();
let o = "api='".to_owned() + &api.to_string() + &"'".to_owned(); let o = "api='".to_owned() + &api.to_string() + &"'".to_owned();
let o = o.to_string(); let o = o.to_string();
let l = shellexpand::tilde("~") + "/.config/ai/openai.toml";
let l = l.to_string(); let path = expand_home_path("~/.config/syui/ai/bot/openai.toml");
let mut l = fs::File::create(l).unwrap();
let mut l = fs::File::create(&path).unwrap();
if o != "" { if o != "" {
l.write_all(&o.as_bytes()).unwrap(); l.write_all(&o.as_bytes()).unwrap();
} }
@ -684,9 +672,10 @@ pub fn c_openai_key(c: &Context) {
impl Open { impl Open {
pub fn new() -> Result<Self, ConfigError> { 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() 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")) .add_source(config::Environment::with_prefix("APP"))
.build()?; .build()?;
s.try_deserialize() s.try_deserialize()

View File

@ -49,6 +49,7 @@ pub mod token;
pub mod feed_get; pub mod feed_get;
pub mod feed_watch; pub mod feed_watch;
pub mod delete_record; pub mod delete_record;
pub mod update_handle;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -105,7 +106,7 @@ fn main() {
.command( .command(
Command::new("login") Command::new("login")
.alias("l") .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) .action(token)
.flag( .flag(
Flag::new("password", FlagType::String) Flag::new("password", FlagType::String)
@ -117,6 +118,11 @@ fn main() {
.description("server flag") .description("server flag")
.alias("s"), .alias("s"),
) )
.flag(
Flag::new("code", FlagType::String)
.description("2FA authentication code")
.alias("c"),
)
) )
.command( .command(
Command::new("refresh") Command::new("refresh")
@ -164,6 +170,11 @@ fn main() {
.alias("c"), .alias("c"),
) )
) )
.command(
Command::new("update-handle")
.description("update-handle <new_handle>")
.action(update_handle)
)
.command( .command(
Command::new("card") Command::new("card")
.description("-v <at://verify> -i <int:id> -p <int:cp> -r <int:rank> -c <collection> -a <author> -img <link> -rare <normal>") .description("-v <at://verify> -i <int:id> -p <int:cp> -r <int:rank> -c <collection> -a <author> -img <link> -rare <normal>")
@ -503,18 +514,19 @@ fn openai_key(c: &Context) {
} }
fn token(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 m = c.args[0].to_string();
let h = async { let h = async {
if let Ok(p) = c.string_flag("password") { if let Ok(p) = c.string_flag("password") {
if let Ok(s) = c.string_flag("server") { let server = c.string_flag("server").unwrap_or_else(|_| "bsky.social".to_string());
let res = token::post_request(m.to_string(), p.to_string(), s.to_string()).await; let code = c.string_flag("code").ok();
w_cfg(&s, &res, &p);
} else { let res = token::post_request(m.to_string(), p.to_string(), server.to_string(), code).await;
let res = w_cfg(&server, &res, &p);
token::post_request(m.to_string(), p.to_string(), "bsky.social".to_string())
.await;
w_cfg(&"bsky.social", &res, &p);
}
} }
}; };
let res = tokio::runtime::Runtime::new().unwrap().block_on(h); let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
@ -530,7 +542,7 @@ fn refresh(_c: &Context) {
let m = data_toml(&"handle"); let m = data_toml(&"handle");
let p = data_toml(&"password"); let p = data_toml(&"password");
let s = data_toml(&"host"); 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); w_cfg(&s, &res, &p);
} else { } else {
w_refresh(&res); w_refresh(&res);
@ -599,6 +611,11 @@ fn timeline(c: &Context) {
fn post(c: &Context) { fn post(c: &Context) {
refresh(c); 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 m = c.args[0].to_string();
let h = async { let h = async {
if let Ok(link) = c.string_flag("link") { if let Ok(link) = c.string_flag("link") {
@ -618,6 +635,11 @@ fn post(c: &Context) {
fn delete(c: &Context) { fn delete(c: &Context) {
refresh(c); 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 m = c.args[0].to_string();
let h = async { let h = async {
if let Ok(col) = c.string_flag("col") { if let Ok(col) = c.string_flag("col") {
@ -629,8 +651,29 @@ fn delete(c: &Context) {
return res; return res;
} }
fn update_handle(c: &Context) {
refresh(c);
if c.args.is_empty() {
eprintln!("Error: New handle is required.");
eprintln!("Usage: aibot update-handle <new_handle>");
std::process::exit(1);
}
let new_handle = c.args[0].to_string();
let h = async {
let str = update_handle::post_request(new_handle);
println!("{}", str.await);
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res;
}
fn like(c: &Context) { fn like(c: &Context) {
refresh(c); 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 m = c.args[0].to_string();
let h = async { let h = async {
if let Ok(uri) = c.string_flag("uri") { if let Ok(uri) = c.string_flag("uri") {
@ -783,6 +826,11 @@ fn game_login(c: &Context) {
fn repost(c: &Context) { fn repost(c: &Context) {
refresh(c); 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 m = c.args[0].to_string();
let h = async { let h = async {
if let Ok(uri) = c.string_flag("uri") { if let Ok(uri) = c.string_flag("uri") {
@ -796,6 +844,11 @@ fn repost(c: &Context) {
fn follow(c: &Context) { fn follow(c: &Context) {
refresh(c); 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 m = c.args[0].to_string();
let h = async { let h = async {
let handle = data_toml(&"handle"); let handle = data_toml(&"handle");
@ -819,10 +872,10 @@ fn profile(c: &Context) {
let h = async { let h = async {
if c.args.len() == 0 { if c.args.len() == 0 {
let j = profile::get_request(data_toml(&"handle")).await; let j = profile::get_request(data_toml(&"handle")).await;
println!("{}", j); print!("{}", j);
} else { } else {
let j = profile::get_request(c.args[0].to_string()).await; 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); let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
@ -831,6 +884,11 @@ fn profile(c: &Context) {
fn mention(c: &Context) { fn mention(c: &Context) {
refresh(c); 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 m = c.args[0].to_string();
let h = async { let h = async {
let str = profile::get_request(m.to_string()).await; let str = profile::get_request(m.to_string()).await;
@ -860,6 +918,11 @@ fn mention(c: &Context) {
fn reply(c: &Context) { fn reply(c: &Context) {
refresh(c); 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 m = c.args[0].to_string();
let h = async { let h = async {
if let Ok(cid) = c.string_flag("cid") { if let Ok(cid) = c.string_flag("cid") {
@ -899,6 +962,11 @@ fn reply(c: &Context) {
#[tokio::main] #[tokio::main]
async fn c_img_upload(c: &Context) -> reqwest::Result<()> { 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 token = data_refresh(&"access");
let atoken = "Authorization: Bearer ".to_owned() + &token; let atoken = "Authorization: Bearer ".to_owned() + &token;
let con = "Content-Type: image/png"; let con = "Content-Type: image/png";
@ -930,6 +998,11 @@ fn img_upload(c: &Context) {
} }
fn img_post(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 m = c.args[0].to_string();
let link = c.string_flag("link").unwrap(); let link = c.string_flag("link").unwrap();
let cid = c.string_flag("cid").unwrap(); let cid = c.string_flag("cid").unwrap();
@ -976,6 +1049,11 @@ fn reply_og(c: &Context) {
} }
fn openai(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 m = c.args[0].to_string();
let h = async { let h = async {
let str = openai::post_request(m.to_string()).await; let str = openai::post_request(m.to_string()).await;

View File

@ -1,12 +1,42 @@
use crate::http_client::HttpClient; use crate::http_client::HttpClient;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{self, Write};
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 {
// First attempt with provided 2FA code (if any)
let response = create_session_request(&handle, &pass, &host, auth_factor_token.as_deref()).await;
// Check if 2FA is required
if response.contains("AuthFactorTokenRequired") {
println!("🔐 2FA authentication required");
println!("📧 A sign-in code has been sent to your email address");
// Prompt for 2FA code
print!("Enter 2FA code: ");
io::stdout().flush().unwrap();
let mut code = String::new();
io::stdin().read_line(&mut code).unwrap();
let code = code.trim();
// Retry with 2FA code
println!("🔄 Retrying authentication with 2FA code...");
return create_session_request(&handle, &pass, &host, Some(code)).await;
}
response
}
async fn create_session_request(handle: &str, pass: &str, host: &str, auth_factor_token: Option<&str>) -> String {
let url = format!("https://{}/xrpc/com.atproto.server.createSession", host); let url = format!("https://{}/xrpc/com.atproto.server.createSession", host);
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("identifier", &handle); map.insert("identifier", handle);
map.insert("password", &pass); map.insert("password", pass);
// Add 2FA code if provided
if let Some(code) = auth_factor_token {
map.insert("authFactorToken", code);
}
let client = HttpClient::new(); let client = HttpClient::new();

17
src/update_handle.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::url;
use crate::http_client::HttpClient;
use serde_json::json;
pub async fn post_request(handle: String) -> String {
let url = url(&"update_handle");
let client = HttpClient::new();
let post = json!({
"handle": handle
});
match client.post_json_with_auth(&url, &post).await {
Ok(response) => response,
Err(e) => format!("Error updating handle: {}", e),
}
}