frames_core/validators/
frame.rs1use 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}