Skip to main content

night_fury_core/domains/
emulation.rs

1use chromiumoxide_cdp::cdp::browser_protocol::emulation::{
2    MediaFeature, SetEmulatedMediaParams, SetGeolocationOverrideParams, SetLocaleOverrideParams,
3    SetTimezoneOverrideParams,
4};
5use tokio::sync::oneshot;
6
7use crate::error::NightFuryError;
8use crate::session::BrowserSession;
9use crate::worker::WorkerState;
10
11// ---------------------------------------------------------------------------
12// Command enum
13// ---------------------------------------------------------------------------
14
15/// Commands for the emulation domain.
16#[non_exhaustive]
17#[allow(clippy::enum_variant_names)]
18pub enum EmulationCmd {
19    SetGeolocation {
20        latitude: f64,
21        longitude: f64,
22        reply: oneshot::Sender<Result<String, String>>,
23    },
24    SetMediaFeature {
25        name: String,
26        value: String,
27        reply: oneshot::Sender<Result<String, String>>,
28    },
29    SetMediaType {
30        media: String,
31        reply: oneshot::Sender<Result<String, String>>,
32    },
33    SetTimezone {
34        timezone_id: String,
35        reply: oneshot::Sender<Result<String, String>>,
36    },
37    SetLocale {
38        locale: String,
39        reply: oneshot::Sender<Result<String, String>>,
40    },
41}
42
43// ---------------------------------------------------------------------------
44// Dispatch
45// ---------------------------------------------------------------------------
46
47impl EmulationCmd {
48    pub(crate) async fn dispatch(self, state: &mut WorkerState) {
49        match self {
50            EmulationCmd::SetGeolocation {
51                latitude,
52                longitude,
53                reply,
54            } => handle_set_geolocation(state, latitude, longitude, reply).await,
55            EmulationCmd::SetMediaFeature { name, value, reply } => {
56                handle_emulate_media_feature(state, name, value, reply).await
57            }
58            EmulationCmd::SetMediaType { media, reply } => {
59                handle_emulate_media_type(state, media, reply).await
60            }
61            EmulationCmd::SetTimezone { timezone_id, reply } => {
62                handle_emulate_timezone(state, timezone_id, reply).await
63            }
64            EmulationCmd::SetLocale { locale, reply } => {
65                handle_emulate_locale(state, locale, reply).await
66            }
67        }
68    }
69}
70
71// ---------------------------------------------------------------------------
72// Handlers
73// ---------------------------------------------------------------------------
74
75async fn handle_set_geolocation(
76    state: &mut WorkerState,
77    latitude: f64,
78    longitude: f64,
79    reply: oneshot::Sender<Result<String, String>>,
80) {
81    let result: Result<String, String> = async {
82        let page = &state.tabs[state.active_tab].page;
83        let params = SetGeolocationOverrideParams::builder()
84            .latitude(latitude)
85            .longitude(longitude)
86            .accuracy(1.0)
87            .build();
88        page.raw_page()
89            .execute(params)
90            .await
91            .map_err(|e| format!("Emulation.setGeolocationOverride failed: {e}"))?;
92        Ok(format!("Geolocation set to ({latitude}, {longitude})"))
93    }
94    .await;
95    let _ = reply.send(result);
96}
97
98async fn handle_emulate_media_feature(
99    state: &mut WorkerState,
100    name: String,
101    value: String,
102    reply: oneshot::Sender<Result<String, String>>,
103) {
104    let result: Result<String, String> = async {
105        let page = &state.tabs[state.active_tab].page;
106        let params = SetEmulatedMediaParams::builder()
107            .feature(MediaFeature::new(name.as_str(), value.as_str()))
108            .build();
109        page.raw_page()
110            .execute(params)
111            .await
112            .map_err(|e| format!("Emulation.setEmulatedMedia failed: {e}"))?;
113        Ok(format!("Media feature {name} set to {value}"))
114    }
115    .await;
116    let _ = reply.send(result);
117}
118
119async fn handle_emulate_media_type(
120    state: &mut WorkerState,
121    media: String,
122    reply: oneshot::Sender<Result<String, String>>,
123) {
124    let result: Result<String, String> = async {
125        let page = &state.tabs[state.active_tab].page;
126        let params = SetEmulatedMediaParams::builder()
127            .media(media.as_str())
128            .build();
129        page.raw_page()
130            .execute(params)
131            .await
132            .map_err(|e| format!("Emulation.setEmulatedMedia failed: {e}"))?;
133        if media.is_empty() {
134            Ok("Media type override disabled".to_string())
135        } else {
136            Ok(format!("Media type set to {media}"))
137        }
138    }
139    .await;
140    let _ = reply.send(result);
141}
142
143async fn handle_emulate_timezone(
144    state: &mut WorkerState,
145    timezone_id: String,
146    reply: oneshot::Sender<Result<String, String>>,
147) {
148    let result: Result<String, String> = async {
149        let page = &state.tabs[state.active_tab].page;
150        let params = SetTimezoneOverrideParams::new(timezone_id.as_str());
151        page.raw_page()
152            .execute(params)
153            .await
154            .map_err(|e| format!("Emulation.setTimezoneOverride failed: {e}"))?;
155        if timezone_id.is_empty() {
156            Ok("Timezone override disabled".to_string())
157        } else {
158            Ok(format!("Timezone set to {timezone_id}"))
159        }
160    }
161    .await;
162    let _ = reply.send(result);
163}
164
165async fn handle_emulate_locale(
166    state: &mut WorkerState,
167    locale: String,
168    reply: oneshot::Sender<Result<String, String>>,
169) {
170    let result: Result<String, String> = async {
171        let page = &state.tabs[state.active_tab].page;
172        let params = SetLocaleOverrideParams::builder()
173            .locale(locale.as_str())
174            .build();
175        page.raw_page()
176            .execute(params)
177            .await
178            .map_err(|e| format!("Emulation.setLocaleOverride failed: {e}"))?;
179        if locale.is_empty() {
180            Ok("Locale override disabled".to_string())
181        } else {
182            Ok(format!("Locale set to {locale}"))
183        }
184    }
185    .await;
186    let _ = reply.send(result);
187}
188
189// ---------------------------------------------------------------------------
190// Session API
191// ---------------------------------------------------------------------------
192
193impl BrowserSession {
194    /// Override the browser's reported geolocation.
195    pub async fn set_geolocation(
196        &self,
197        latitude: f64,
198        longitude: f64,
199    ) -> Result<String, NightFuryError> {
200        send_cmd!(
201            self,
202            |tx| crate::cmd::BrowserCmd::Emulation(EmulationCmd::SetGeolocation {
203                latitude,
204                longitude,
205                reply: tx,
206            }),
207            NightFuryError::OperationFailed
208        )
209    }
210
211    /// Override a CSS media feature for media queries.
212    ///
213    /// Common features: `prefers-color-scheme` (`dark` / `light`),
214    /// `prefers-reduced-motion` (`reduce` / `no-preference`).
215    pub async fn emulate_media_feature(
216        &self,
217        name: &str,
218        value: &str,
219    ) -> Result<String, NightFuryError> {
220        send_cmd!(
221            self,
222            |tx| crate::cmd::BrowserCmd::Emulation(EmulationCmd::SetMediaFeature {
223                name: name.to_string(),
224                value: value.to_string(),
225                reply: tx,
226            }),
227            NightFuryError::OperationFailed
228        )
229    }
230
231    /// Override the CSS media type (e.g. `screen`, `print`).
232    /// Pass an empty string to disable the override.
233    pub async fn emulate_media_type(&self, media: &str) -> Result<String, NightFuryError> {
234        send_cmd!(
235            self,
236            |tx| crate::cmd::BrowserCmd::Emulation(EmulationCmd::SetMediaType {
237                media: media.to_string(),
238                reply: tx,
239            }),
240            NightFuryError::OperationFailed
241        )
242    }
243
244    /// Override the browser's timezone.
245    /// Pass a valid IANA timezone ID (e.g. `Asia/Tokyo`, `America/New_York`).
246    /// Pass an empty string to restore the default.
247    pub async fn emulate_timezone(&self, timezone_id: &str) -> Result<String, NightFuryError> {
248        send_cmd!(
249            self,
250            |tx| crate::cmd::BrowserCmd::Emulation(EmulationCmd::SetTimezone {
251                timezone_id: timezone_id.to_string(),
252                reply: tx,
253            }),
254            NightFuryError::OperationFailed
255        )
256    }
257
258    /// Override the browser's locale (e.g. `ja-JP`, `en-US`).
259    /// Pass an empty string to restore the default.
260    pub async fn emulate_locale(&self, locale: &str) -> Result<String, NightFuryError> {
261        send_cmd!(
262            self,
263            |tx| crate::cmd::BrowserCmd::Emulation(EmulationCmd::SetLocale {
264                locale: locale.to_string(),
265                reply: tx,
266            }),
267            NightFuryError::OperationFailed
268        )
269    }
270}