Skip to main content

tibba_model/
web_page_detector.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{
16    Error, JsonSnafu, Model, ModelListParams, Schema, SchemaAllowCreate, SchemaAllowEdit,
17    SchemaType, SchemaView, SqlxSnafu, format_datetime, new_schema_options,
18};
19use super::{REGION_ALIYUN, REGION_ANY, REGION_GZ, REGION_TX};
20use serde::{Deserialize, Serialize};
21use snafu::ResultExt;
22use sqlx::FromRow;
23use sqlx::types::Json;
24use sqlx::{Pool, Postgres, QueryBuilder};
25use time::PrimitiveDateTime;
26
27type Result<T> = std::result::Result<T, Error>;
28
29#[derive(FromRow)]
30struct WebPageDetectorSchema {
31    id: i64,
32    status: i16,
33    name: String,
34    interval: i16,
35    url: String,
36    width: i32,
37    height: i32,
38    user_agent: String,
39    accept_language: String,
40    platform: String,
41    wait_for_element: String,
42    device_scale_factor: f64,
43    timeout: i32,
44    capture_screenshot: bool,
45    capture_element: String,
46    remark: String,
47    regions: Json<Vec<String>>,
48    created_by: i64,
49    created: PrimitiveDateTime,
50    modified: PrimitiveDateTime,
51}
52
53#[derive(Deserialize, Serialize, Clone, Debug)]
54pub struct WebPageDetector {
55    pub id: i64,
56    pub status: i16,
57    pub name: String,
58    pub interval: i16,
59    pub url: String,
60    pub width: i32,
61    pub height: i32,
62    pub user_agent: String,
63    pub accept_language: String,
64    pub platform: String,
65    pub wait_for_element: String,
66    pub device_scale_factor: f64,
67    pub timeout: i32,
68    pub capture_screenshot: bool,
69    pub capture_element: String,
70    pub remark: String,
71    pub regions: Vec<String>,
72    pub created_by: i64,
73    pub created: String,
74    pub modified: String,
75}
76
77impl From<WebPageDetectorSchema> for WebPageDetector {
78    fn from(schema: WebPageDetectorSchema) -> Self {
79        Self {
80            id: schema.id,
81            status: schema.status,
82            name: schema.name,
83            interval: schema.interval,
84            url: schema.url,
85            width: schema.width,
86            height: schema.height,
87            user_agent: schema.user_agent,
88            accept_language: schema.accept_language,
89            platform: schema.platform,
90            wait_for_element: schema.wait_for_element,
91            device_scale_factor: schema.device_scale_factor,
92            timeout: schema.timeout,
93            capture_screenshot: schema.capture_screenshot,
94            capture_element: schema.capture_element,
95            remark: schema.remark,
96            regions: schema.regions.0,
97            created_by: schema.created_by,
98            created: format_datetime(schema.created),
99            modified: format_datetime(schema.modified),
100        }
101    }
102}
103
104#[derive(Deserialize, Serialize, Clone, Debug)]
105pub struct WebPageDetectorInsertParams {
106    pub name: String,
107    pub interval: u16,
108    pub url: String,
109    pub width: u32,
110    pub height: u32,
111    pub user_agent: Option<String>,
112    pub accept_language: Option<String>,
113    pub platform: Option<String>,
114    pub wait_for_element: Option<String>,
115    pub device_scale_factor: Option<f64>,
116    pub timeout: Option<u32>,
117    pub capture_screenshot: Option<bool>,
118    pub capture_element: Option<String>,
119    pub regions: Vec<String>,
120    pub remark: String,
121    pub created_by: u64,
122}
123
124pub struct WebPageDetectorModel {}
125
126impl Model for WebPageDetectorModel {
127    type Output = WebPageDetector;
128    fn new() -> Self {
129        Self {}
130    }
131    fn keyword(&self) -> String {
132        "name".to_string()
133    }
134    async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView {
135        SchemaView {
136            schemas: vec![
137                Schema::new_id(),
138                Schema::new_name(),
139                Schema {
140                    name: "interval".to_string(),
141                    category: SchemaType::Number,
142                    default_value: Some(serde_json::json!(1)),
143                    ..Default::default()
144                },
145                Schema {
146                    name: "url".to_string(),
147                    category: SchemaType::String,
148                    required: true,
149                    ..Default::default()
150                },
151                Schema {
152                    name: "regions".to_string(),
153                    category: SchemaType::Strings,
154                    options: Some(new_schema_options(&[
155                        REGION_ANY,
156                        REGION_TX,
157                        REGION_GZ,
158                        REGION_ALIYUN,
159                    ])),
160                    ..Default::default()
161                },
162                Schema {
163                    name: "width".to_string(),
164                    category: SchemaType::Number,
165                    ..Default::default()
166                },
167                Schema {
168                    name: "height".to_string(),
169                    category: SchemaType::Number,
170                    ..Default::default()
171                },
172                Schema {
173                    name: "user_agent".to_string(),
174                    category: SchemaType::String,
175                    ..Default::default()
176                },
177                Schema {
178                    name: "accept_language".to_string(),
179                    category: SchemaType::String,
180                    ..Default::default()
181                },
182                Schema {
183                    name: "platform".to_string(),
184                    category: SchemaType::String,
185                    ..Default::default()
186                },
187                Schema {
188                    name: "wait_for_element".to_string(),
189                    category: SchemaType::String,
190                    ..Default::default()
191                },
192                Schema {
193                    name: "device_scale_factor".to_string(),
194                    category: SchemaType::Number,
195                    ..Default::default()
196                },
197                Schema {
198                    name: "timeout".to_string(),
199                    category: SchemaType::Number,
200                    ..Default::default()
201                },
202                Schema {
203                    name: "capture_screenshot".to_string(),
204                    category: SchemaType::Boolean,
205                    ..Default::default()
206                },
207                Schema {
208                    name: "capture_element".to_string(),
209                    category: SchemaType::String,
210                    ..Default::default()
211                },
212                Schema::new_status(),
213                Schema::new_remark(),
214                Schema::new_created(),
215                Schema::new_modified(),
216            ],
217            allow_edit: SchemaAllowEdit {
218                owner: true,
219                roles: vec!["*".to_string()],
220                ..Default::default()
221            },
222            allow_create: SchemaAllowCreate {
223                roles: vec!["*".to_string()],
224                ..Default::default()
225            },
226        }
227    }
228    async fn insert(&self, pool: &Pool<Postgres>, params: serde_json::Value) -> Result<u64> {
229        let params: WebPageDetectorInsertParams =
230            serde_json::from_value(params).context(JsonSnafu)?;
231        let row: (i64,) = sqlx::query_as(
232            r#"INSERT INTO web_page_detectors (name, "interval", url, width, height, user_agent, accept_language, platform, wait_for_element, device_scale_factor, timeout, capture_screenshot, capture_element, remark, regions, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id"#,
233        )
234        .bind(params.name)
235        .bind(params.interval as i16)
236        .bind(params.url)
237        .bind(params.width as i32)
238        .bind(params.height as i32)
239        .bind(params.user_agent.unwrap_or_default())
240        .bind(params.accept_language.unwrap_or_default())
241        .bind(params.platform.unwrap_or_default())
242        .bind(params.wait_for_element.unwrap_or_default())
243        .bind(params.device_scale_factor.unwrap_or_default())
244        .bind(params.timeout.unwrap_or_default() as i32)
245        .bind(params.capture_screenshot.unwrap_or_default())
246        .bind(params.capture_element.unwrap_or_default())
247        .bind(params.remark)
248        .bind(Json(params.regions))
249        .bind(params.created_by as i64)
250        .fetch_one(pool)
251        .await
252        .context(SqlxSnafu)?;
253
254        Ok(row.0 as u64)
255    }
256    async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
257        let mut qb = QueryBuilder::new("SELECT COUNT(*) FROM web_page_detectors");
258        self.push_conditions(&mut qb, params)?;
259        let count = qb
260            .build_query_scalar::<i64>()
261            .fetch_one(pool)
262            .await
263            .context(SqlxSnafu)?;
264        Ok(count)
265    }
266    async fn list(
267        &self,
268        pool: &Pool<Postgres>,
269        params: &ModelListParams,
270    ) -> Result<Vec<Self::Output>> {
271        let mut qb = QueryBuilder::new("SELECT * FROM web_page_detectors");
272        self.push_conditions(&mut qb, params)?;
273        params.push_pagination(&mut qb);
274        let detectors = qb
275            .build_query_as::<WebPageDetectorSchema>()
276            .fetch_all(pool)
277            .await
278            .context(SqlxSnafu)?;
279        Ok(detectors.into_iter().map(|s| s.into()).collect())
280    }
281}
282
283impl WebPageDetectorModel {
284    pub async fn list_enabled_by_region(
285        &self,
286        pool: &Pool<Postgres>,
287        region: Option<String>,
288        limit: u64,
289        offset: u64,
290    ) -> Result<Vec<WebPageDetector>> {
291        let region = region.unwrap_or(REGION_ANY.to_string());
292        let detectors = sqlx::query_as::<_, WebPageDetectorSchema>(
293            r#"SELECT * FROM web_page_detectors WHERE deleted_at IS NULL AND status = 1 AND (jsonb_array_length(regions) = 0 OR regions @> $1::jsonb OR regions @> $2::jsonb) ORDER BY id ASC LIMIT $3 OFFSET $4"#,
294        )
295        .bind(format!("[{:?}]", region))
296        .bind(format!("[{:?}]", REGION_ANY))
297        .bind(limit as i64)
298        .bind(offset as i64)
299        .fetch_all(pool)
300        .await
301        .context(SqlxSnafu)?;
302
303        Ok(detectors.into_iter().map(|schema| schema.into()).collect())
304    }
305}