1use std::collections::HashMap;
2
3use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
4use url::Url;
5
6use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "ts_rs")]
9use ts_rs::TS;
10
11use crate::prelude::*;
12
13#[derive(Deserialize, Serialize)]
14#[cfg_attr(feature = "ts_rs", derive(TS))]
15pub struct OEmbedResponse {
16 pub author_name: String,
17 pub author_url: String,
18 pub html: String,
19 pub provider_name: String,
20 pub provider_url: String,
21 pub title: String,
22 pub version: String,
23}
24
25#[derive(Deserialize, Debug)]
26pub struct OEmbedRequest {
27 pub url: String,
28}
29
30pub fn url_to_oembed_endpoint(url: String, base_url: Option<String>) -> UtilResult<Url> {
32 let parsed_url = Url::parse(url.as_str())?;
33 if let Some(host) = parsed_url.host_str() {
34 if host.ends_with("youtu.be") || host.ends_with("youtube.com") {
35 return oembed_url_builder(
36 "https://www.youtube.com/oembed",
37 &format!("url={}&format=json&maxwidth=780&maxheight=440", url),
38 );
39 }
40 if host.ends_with("twitter.com") {
41 return oembed_url_builder(
42 "https://publish.twitter.com/oembed",
43 &format!("url={}&maxwidth=780&maxheight=440", url),
44 );
45 }
46 if host.ends_with("soundcloud.com") {
47 return oembed_url_builder(
48 "https://soundcloud.com/oembed",
49 &format!("url={}&format=json&maxwidth=780&maxheight=440", url),
50 );
51 }
52 if host.ends_with("open.spotify.com") || host.ends_with("play.spotify.com") {
53 return oembed_url_builder(
54 "https://embed.spotify.com/oembed",
55 &format!("url={}&format=json&height=335&width=780", url),
56 );
57 }
58 if host.ends_with("flickr.com") || host.ends_with("flic.kr") {
59 return oembed_url_builder(
60 "https://www.flickr.com/services/oembed",
61 &format!("url={}&format=json&maxwidth=780&maxheight=780", url),
62 );
63 }
64 if host.ends_with("vimeo.com") {
65 return oembed_url_builder(
66 &format!("{}/api/v0/cms/gutenberg/oembed/vimeo", base_url.unwrap()),
67 &format!(
68 "url={}",
69 utf8_percent_encode(url.as_str(), NON_ALPHANUMERIC)
70 ),
71 );
72 }
73 if host.ends_with("menti.com") || host.ends_with("mentimeter.com") {
74 return oembed_url_builder(
75 &format!(
76 "{}/api/v0/cms/gutenberg/oembed/mentimeter",
77 base_url.unwrap()
78 ),
79 &format!(
80 "url={}",
81 utf8_percent_encode(url.as_str(), NON_ALPHANUMERIC)
82 ),
83 );
84 }
85 if host.ends_with("thinglink.com") {
86 return oembed_url_builder(
87 &format!(
88 "{}/api/v0/cms/gutenberg/oembed/thinglink",
89 base_url.unwrap()
90 ),
91 &format!(
92 "url={}",
93 utf8_percent_encode(url.as_str(), NON_ALPHANUMERIC)
94 ),
95 );
96 }
97 if host.ends_with("imgur.com") {
98 return oembed_url_builder(
99 "https://api.imgur.com/oembed",
100 &format!("url={}&maxwidth=780", url),
101 );
102 }
103 if host.ends_with("reddit.com") {
104 return oembed_url_builder(
105 "https://www.reddit.com/oembed",
106 &format!("url={}&format=json", url),
107 );
108 }
109 if host.ends_with("slideshare.net") {
110 return oembed_url_builder(
111 "https://www.slideshare.net/api/oembed/2",
112 &format!("url={}&format=json", url),
113 );
114 }
115 if host.ends_with("ted.com") {
116 return oembed_url_builder(
117 "https://www.ted.com/services/v1/oembed.json",
118 &format!("url={}", url),
119 );
120 }
121 if host.ends_with("tumblr.com") {
122 return oembed_url_builder(
124 "https://www.tumblr.com/oembed/1.0",
125 &format!("url={}&format=json", url),
126 );
127 }
128 Err(UtilError::new(
129 UtilErrorType::Other,
130 "Link not supported for embedding.".to_string(),
131 None,
132 ))
133 } else {
134 Err(UtilError::new(
135 UtilErrorType::Other,
136 "Failed to parse host from URL.".to_string(),
137 None,
138 ))
139 }
140}
141
142pub fn mentimeter_oembed_response_builder(
143 url: String,
144 base_url: String,
145) -> UtilResult<OEmbedResponse> {
146 let mut parsed_url = Url::parse(url.as_str()).map_err(|e| {
147 UtilError::new(
148 UtilErrorType::UrlParse,
149 "Failed to parse url".to_string(),
150 Some(e.into()),
151 )
152 })?;
153 let params: HashMap<_, _> = parsed_url.query_pairs().into_owned().collect();
155 parsed_url.set_query(None);
157 let decoded_title = percent_decode_str(
158 params
159 .get("title")
160 .unwrap_or(&"Mentimeter%20embed".to_string()),
161 )
162 .decode_utf8()
163 .expect("Decoding title or default value for menti embed failed")
164 .to_string();
165
166 let response = OEmbedResponse {
167 author_name: "Mooc.fi".to_string(),
168 author_url: base_url,
169 html: format!(
170 "<iframe src={} style='width: 99%;' height={:?} title={:?}></iframe>",
171 parsed_url,
172 params.get("height").unwrap_or(&"500".to_string()),
173 decoded_title
174 ),
175 provider_name: "mentimeter".to_string(),
176 provider_url: parsed_url
177 .host_str()
178 .unwrap_or("https://www.mentimeter.com")
179 .to_string(),
180 title: decoded_title,
181 version: "1.0".to_string(),
182 };
183 Ok(response)
184}
185
186pub fn thinglink_oembed_response_builder(
187 url: String,
188 base_url: String,
189) -> UtilResult<OEmbedResponse> {
190 let mut parsed_url = Url::parse(url.as_str()).map_err(|e| {
191 UtilError::new(
192 UtilErrorType::UrlParse,
193 "Failed to parse url".to_string(),
194 Some(e.into()),
195 )
196 })?;
197 let params: HashMap<_, _> = parsed_url.query_pairs().into_owned().collect();
199 parsed_url.set_query(None);
201 let decoded_title = percent_decode_str(
202 params
203 .get("title")
204 .unwrap_or(&"Thinlink%20embed".to_string()),
205 )
206 .decode_utf8()
207 .expect("Decoding title or default value for thinglink embed failed")
208 .to_string();
209
210 let response = OEmbedResponse {
211 author_name: "Mooc.fi".to_string(),
212 author_url: base_url,
213 html: format!(
214 "<iframe sandbox=\"allow-scripts allow-same-origin\" src=\"{}\" style=\"width: 99%;\" height={:?} title=\"{:?}\"></iframe>",
215 parsed_url,
216 params.get("height").unwrap_or(&"500".to_string()),
217 decoded_title
218 ),
219 provider_name: "thinglink".to_string(),
220 provider_url: parsed_url
221 .host_str()
222 .unwrap_or("https://www.thinglink.com/")
223 .to_string(),
224 title: decoded_title,
225 version: "1.0".to_string(),
226 };
227 Ok(response)
228}
229
230pub fn vimeo_oembed_response_builder(url: String, base_url: String) -> UtilResult<OEmbedResponse> {
231 let mut parsed_url = Url::parse(url.as_str()).map_err(|e| {
232 UtilError::new(
233 UtilErrorType::UrlParse,
234 "Failed to parse url".to_string(),
235 Some(e.into()),
236 )
237 })?;
238 let params: HashMap<_, _> = parsed_url.query_pairs().into_owned().collect();
240 parsed_url.set_query(None);
242 let decoded_title =
243 percent_decode_str(params.get("title").unwrap_or(&"Vimeo%20embed".to_string()))
244 .decode_utf8()
245 .expect("Decoding title or default value for vimeo embed failed")
246 .to_string();
247
248 let path = parsed_url.path();
249 let video_id = extract_numerical_id(path).ok_or_else(|| {
250 UtilError::new(
251 UtilErrorType::Other,
252 "Could not find video id from url".to_string(),
253 None,
254 )
255 })?;
256
257 let mut iframe_url = Url::parse("https://player.vimeo.com").map_err(|e| {
258 UtilError::new(
259 UtilErrorType::UrlParse,
260 "Failed to parse url".to_string(),
261 Some(e.into()),
262 )
263 })?;
264 iframe_url.set_path(&format!("/video/{}", video_id));
265 iframe_url.set_query(Some("app_id=122963"));
266
267 let response = OEmbedResponse {
268 author_name: "Mooc.fi".to_string(),
269 author_url: base_url,
270 html: format!(
271 "<iframe sandbox=\"allow-scripts allow-same-origin\" frameborder=\"0\" src=\"{}\" style=\"width: 99%;\" height={:?} title=\"{:?}\"></iframe>",
272 iframe_url,
273 params.get("height").unwrap_or(&"500".to_string()),
274 decoded_title
275 ),
276 provider_name: "vimeo".to_string(),
277 provider_url: parsed_url
278 .host_str()
279 .unwrap_or("https://www.vimeo.com/")
280 .to_string(),
281 title: decoded_title,
282 version: "1.0".to_string(),
283 };
284 Ok(response)
285}
286
287fn oembed_url_builder(url: &str, query_params: &str) -> UtilResult<Url> {
288 let mut endpoint_url = Url::parse(url)?;
289 endpoint_url.set_query(Some(query_params));
290 Ok(endpoint_url)
291}
292
293fn extract_numerical_id(path: &str) -> Option<String> {
294 path.split('/')
295 .map(|o| o.trim())
296 .find(|segment| !segment.is_empty() && segment.chars().all(|c| c.is_ascii_digit()))
297 .map(|s| s.to_string())
298}
299
300#[cfg(test)]
301mod tests {
302 use url::Url;
303
304 use super::*;
305 #[test]
306 fn works_with_youtu_be() {
307 assert_eq!(
308 url_to_oembed_endpoint("https://youtu.be/dQw4w9WgXcQ".to_string(), None).unwrap(),
309 Url::parse(
310 "https://www.youtube.com/oembed?url=https://youtu.be/dQw4w9WgXcQ&format=json&maxwidth=780&maxheight=440"
311 )
312 .unwrap()
313 )
314 }
315 #[test]
316 fn works_with_youtube_com() {
317 assert_eq!(
318 url_to_oembed_endpoint("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string(), None)
319 .unwrap(),
320 Url::parse("https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&format=json&maxwidth=780&maxheight=440").unwrap()
321 )
322 }
323 #[test]
324 fn works_with_youtube_com_playlist() {
325 assert_eq!(
326 url_to_oembed_endpoint(
327 "https://www.youtube.com/playlist?list=gYLqDMMh1GEbHf-Q1".to_string(), None
328 )
329 .unwrap(),
330 Url::parse("https://www.youtube.com/oembed?url=https://www.youtube.com/playlist?list=gYLqDMMh1GEbHf-Q1&format=json&maxwidth=780&maxheight=440").unwrap()
331 )
332 }
333 #[test]
334 fn works_with_open_spotify_com() {
335 assert_eq!(url_to_oembed_endpoint(
336 "http://open.spotify.com/track/298gs9ATwKlQR".to_string(), None
337 )
338 .unwrap(),
339 Url::parse("https://embed.spotify.com/oembed?url=http://open.spotify.com/track/298gs9ATwKlQR&format=json&height=335&width=780").unwrap())
340 }
341 #[test]
342 fn works_with_flic_kr_com() {
343 assert_eq!(
344 url_to_oembed_endpoint("https://flic.kr/p/2jJ".to_string(), None).unwrap(),
345 Url::parse(
346 "https://www.flickr.com/services/oembed?url=https://flic.kr/p/2jJ&format=json&maxwidth=780&maxheight=780"
347 ).unwrap()
348 )
349 }
350
351 #[test]
352 fn works_with_thinglink() {
353 assert_eq!(
354 url_to_oembed_endpoint(
355 "https://www.thinglink.com/card/1205257932048957445".to_string(),
356 Some("http://project-331.local".to_string())
357 )
358 .unwrap()
359 .to_string(),
360 "http://project-331.local/api/v0/cms/gutenberg/oembed/thinglink?url=https%3A%2F%2Fwww%2Ethinglink%2Ecom%2Fcard%2F1205257932048957445"
361 )
362 }
363
364 #[test]
365 fn works_with_vimeo() {
366 assert_eq!(
367 url_to_oembed_endpoint(
368 "https://vimeo.com/275255674".to_string(),
369 Some("http://project-331.local".to_string())
370 )
371 .unwrap()
372 .to_string(),
373 "http://project-331.local/api/v0/cms/gutenberg/oembed/vimeo?url=https%3A%2F%2Fvimeo%2Ecom%2F275255674"
374 )
375 }
376}