frames_core/validators/
frame.rs

1use std::collections::HashMap;
2
3use scraper::{Html, Selector};
4
5use crate::types::{
6    button::FrameButton,
7    errors::{Error, ErrorCode, FrameErrors},
8    frame::Frame,
9    image::AspectRatio,
10};
11
12impl Frame {
13    pub fn validate(&self) -> Result<(), FrameErrors> {
14        let mut errors = FrameErrors::new();
15
16        match self.image.validate() {
17            Ok(_) => (),
18            Err(e) => errors.add_errors(e.errors),
19        }
20
21        for button in &self.buttons {
22            match button.validate() {
23                Ok(_) => (),
24                Err(e) => errors.add_errors(e.errors),
25            }
26        }
27
28        if !errors.is_empty() {
29            return Err(errors);
30        }
31
32        Ok(())
33    }
34
35    pub fn from_url(&mut self, url: &str) -> Result<&mut Self, FrameErrors> {
36        let response = reqwest::blocking::get(url);
37        match response {
38            Ok(body) => {
39                let text = body.text();
40                match text {
41                    Ok(html) => self.from_html(&html),
42                    Err(_) => {
43                        let mut errors = FrameErrors::new();
44                        let error = Error {
45                            description: "Failed to read the response text from the URL provided. This may occur due to network issues, server errors, or the response being in an unexpected format."
46                            .to_string(),
47                            code: ErrorCode::FailedToReadResponse,
48                            key: None,
49                        };
50                        errors.add_error(error);
51                        Err(errors)
52                    }
53                }
54            }
55            Err(_) => {
56                let mut errors = FrameErrors::new();
57                let error = Error {
58                    description: "Failed to fetch frame HTML.".to_string(),
59                    code: ErrorCode::FailedToFetchFrameHTML,
60                    key: None,
61                };
62                errors.add_error(error);
63                Err(errors)
64            }
65        }
66    }
67
68    pub fn from_html(&mut self, html: &str) -> Result<&mut Self, FrameErrors> {
69        let document = Html::parse_document(html);
70        let mut errors = FrameErrors::new();
71
72        let title_selector = Selector::parse("title").unwrap();
73        if let Some(title_element) = document.select(&title_selector).next() {
74            let title_text = title_element.text().collect::<Vec<_>>().join("");
75            self.title = title_text
76        } else {
77            let error = Error {
78                description: "Please ensure a <title> tag is present within the HTML metadata for proper frame functionality..".to_string(),
79                code: ErrorCode::MissingTitle,
80                key: None,
81            };
82            errors.add_error(error);
83        }
84
85        let selector = Selector::parse("meta").unwrap();
86        let mut temp_buttons: HashMap<usize, FrameButton> = HashMap::new();
87        for element in document.select(&selector) {
88            if let Some(name) = element.value().attr("name") {
89                if let Some(_content) = element.value().attr("content") {
90                    let content = _content.to_string();
91                    match name {
92                        "fc:frame" => self.version = content,
93                        "fc:frame:image" => self.image.url = content,
94                        "fc:frame:image:aspect_ratio" => {
95                            self.image.aspect_ratio = match _content {
96                                "1.91:1" => AspectRatio::OnePointNineToOne,
97                                "1:1" => AspectRatio::OneToOne,
98                                _ => AspectRatio::Error,
99                            }
100                        }
101                        "fc:frame:post_url" => self.post_url = Some(content),
102                        "fc:frame:input:text" => self.input_text = Some(content),
103                        name if name.starts_with("fc:frame:button:") => {
104                            let parts: Vec<&str> = name.split(':').collect();
105                            if let Ok(idx) = parts[3].parse::<usize>() {
106                                match parts.get(4) {
107                                    Some(&"action") => {
108                                        if let Some(button) = temp_buttons.get_mut(&idx) {
109                                            button.action = Some(content);
110                                        } else {
111                                            let button = FrameButton {
112                                                id: idx,
113                                                label: content.clone(),
114                                                action: Some(content),
115                                                target: None,
116                                            };
117                                            temp_buttons.insert(idx, button);
118                                        }
119                                    }
120                                    _ => {
121                                        let button = FrameButton {
122                                            id: idx,
123                                            label: content,
124                                            action: Some("post".to_string()),
125                                            target: None,
126                                        };
127                                        temp_buttons.insert(idx, button);
128                                    }
129                                }
130                            }
131                        }
132                        _ => {}
133                    }
134                }
135            }
136        }
137
138        match self.add_buttons_if_apply(temp_buttons) {
139            Ok(buttons) => self.buttons.extend(buttons),
140            Err(errs) => errors.add_errors(errs.errors),
141        };
142
143        match self.validate() {
144            Ok(_) => (),
145            Err(e) => {
146                errors.add_errors(e.errors);
147                return Err(errors);
148            }
149        }
150
151        if !errors.is_empty() {
152            return Err(errors);
153        }
154        Ok(self)
155    }
156
157    fn add_buttons_if_apply(
158        &mut self,
159        temp_buttons: HashMap<usize, FrameButton>,
160    ) -> Result<Vec<FrameButton>, FrameErrors> {
161        let mut errors = FrameErrors::new();
162        let mut buttons: Vec<FrameButton> = Vec::new();
163
164        let mut indices: Vec<usize> = temp_buttons.keys().cloned().collect();
165        indices.sort();
166
167        let valid_sequence = if indices.is_empty() || indices.len() == 1 {
168            true
169        } else {
170            indices[0] == 1 && indices.windows(2).all(|w| w[0] + 1 == w[1])
171        };
172
173        if valid_sequence {
174            buttons.extend(temp_buttons.into_values());
175        } else {
176            let error = Error {
177                description: "Button indices are not in a consecutive sequence starting from 1."
178                    .to_string(),
179                code: ErrorCode::InvalidButtonSequence,
180                key: Some("fc:frame:buttons".to_string()),
181            };
182            errors.add_error(error);
183        }
184
185        if !errors.is_empty() {
186            return Err(errors);
187        }
188
189        Ok(buttons)
190    }
191}