1use std::path::PathBuf;
12
13use tail_fin_common::TailFinError;
14
15pub fn no_mode_error(service: &str, cmd: &str) -> TailFinError {
17 TailFinError::Api(format!(
18 "No connection mode specified for {service}.\n\
19 \x20 Use --connect to use browser mode:\n\
20 \x20 tail-fin --connect 127.0.0.1:9222 {service} {cmd}\n\
21 \x20 Or --cookies to use saved cookies:\n\
22 \x20 tail-fin --cookies {service} {cmd}\n\
23 \x20 Some adapters (e.g. spotify) auto-launch a stealth browser when no mode is given."
24 ))
25}
26
27pub struct Ctx {
29 pub connect: Option<String>,
30 pub cookies: Option<String>,
31 pub headed: bool,
32}
33
34pub fn default_cookies_path(site: &str) -> PathBuf {
36 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
37 PathBuf::from(home)
38 .join(".tail-fin")
39 .join(format!("{}-cookies.txt", site))
40}
41
42pub fn resolve_cookies_path(cookies_flag: &str, site: &str) -> PathBuf {
45 if cookies_flag == "auto" {
46 default_cookies_path(site)
47 } else {
48 PathBuf::from(cookies_flag)
49 }
50}
51
52pub async fn browser_session(
54 host: &str,
55 headed: bool,
56) -> Result<night_fury_core::BrowserSession, TailFinError> {
57 Ok(night_fury_core::BrowserSession::builder()
58 .connect_to(format!("ws://{}", host))
59 .headed(headed)
60 .build()
61 .await?)
62}
63
64pub async fn launch_browser(headed: bool) -> Result<night_fury_core::BrowserSession, TailFinError> {
66 Ok(night_fury_core::BrowserSession::builder()
67 .headed(headed)
68 .build()
69 .await?)
70}
71
72pub async fn auto_launch_stealth(
76 url: &str,
77 headed: bool,
78) -> Result<night_fury_core::BrowserSession, TailFinError> {
79 eprintln!("No connection mode specified. Launching stealth browser...");
80 Ok(night_fury_core::BrowserSession::builder()
81 .headed(headed)
82 .cloudflare_timeout(std::time::Duration::from_secs(30))
83 .launch_stealth(url)
84 .await?)
85}
86
87pub async fn launch_stealth_session(
89 url: &str,
90 headed: bool,
91) -> Result<night_fury_core::BrowserSession, TailFinError> {
92 Ok(night_fury_core::BrowserSession::builder()
93 .headed(headed)
94 .launch_stealth(url)
95 .await?)
96}
97
98pub fn require_browser(
101 connect: &Option<String>,
102 service: &str,
103 action_name: &str,
104) -> Result<String, TailFinError> {
105 match connect {
106 Some(host) => Ok(host.clone()),
107 None => Err(TailFinError::Api(format!(
108 "`{service} {action_name}` requires browser mode (--connect).\n\
109 \x20 Use: tail-fin --connect 127.0.0.1:9222 {service} {action_name} ..."
110 ))),
111 }
112}
113
114pub async fn require_browser_session(
119 ctx: &Ctx,
120 service: &str,
121) -> Result<night_fury_core::BrowserSession, TailFinError> {
122 if ctx.cookies.is_some() {
123 return Err(TailFinError::Api(format!(
124 "{service} cookie mode is not supported.\n\
125 \x20 Use --connect for browser mode."
126 )));
127 }
128 let host = match ctx.connect.as_deref() {
129 Some(h) => h,
130 None => {
131 return Err(TailFinError::Api(format!(
132 "{service} requires --connect.\n\
133 \x20 Example: tail-fin --connect 127.0.0.1:9222 {service} ..."
134 )));
135 }
136 };
137 browser_session(host, ctx.headed).await
138}
139
140pub fn print_json(value: &(impl serde::Serialize + ?Sized)) -> Result<(), TailFinError> {
142 println!("{}", serde_json::to_string_pretty(value)?);
143 Ok(())
144}
145
146pub fn print_list(
148 key: &str,
149 items: &impl serde::Serialize,
150 count: usize,
151) -> Result<(), TailFinError> {
152 println!(
153 "{}",
154 serde_json::to_string_pretty(&serde_json::json!({
155 key: items,
156 "count": count,
157 }))?
158 );
159 Ok(())
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn resolve_cookies_path_auto_ends_with_site_cookies_txt() {
168 let p = resolve_cookies_path("auto", "twitter");
169
170 assert_eq!(
171 p.file_name().and_then(|n| n.to_str()),
172 Some("twitter-cookies.txt"),
173 "unexpected filename in: {}",
174 p.display()
175 );
176
177 assert_eq!(
178 p.parent()
179 .and_then(|pp| pp.file_name())
180 .and_then(|n| n.to_str()),
181 Some(".tail-fin"),
182 "unexpected parent directory in: {}",
183 p.display()
184 );
185 }
186
187 #[test]
188 fn resolve_cookies_path_explicit_is_verbatim() {
189 let p = resolve_cookies_path("/explicit/cookies.txt", "twitter");
190 assert_eq!(p.to_string_lossy(), "/explicit/cookies.txt");
191 }
192
193 #[test]
194 fn require_browser_errors_when_connect_missing() {
195 let err = require_browser(&None, "twitter", "timeline").unwrap_err();
196 let msg = err.to_string();
197 assert!(
198 msg.contains("--connect"),
199 "error should mention --connect; got: {msg}"
200 );
201 assert!(
202 msg.contains("twitter timeline"),
203 "error should mention the service/action; got: {msg}"
204 );
205 }
206
207 #[test]
208 fn require_browser_returns_host_when_present() {
209 let host = require_browser(&Some("127.0.0.1:9222".to_string()), "twitter", "timeline")
210 .expect("should succeed when --connect is provided");
211 assert_eq!(host, "127.0.0.1:9222");
212 }
213}