headless_lms_server/controllers/cms/
gutenberg.rs

1use std::time::Duration;
2
3use headless_lms_utils::url_to_oembed_endpoint::{
4    OEmbedRequest, OEmbedResponse, mentimeter_oembed_response_builder,
5    thinglink_oembed_response_builder, url_to_oembed_endpoint, vimeo_oembed_response_builder,
6};
7use serde::{Deserialize, Serialize};
8
9use crate::prelude::*;
10
11#[derive(Deserialize, Serialize)]
12#[serde(rename_all = "kebab-case")]
13pub struct ThemeSupports {
14    pub responsive_embeds: bool,
15}
16
17#[derive(Deserialize, Serialize)]
18pub struct ThemeResponse {
19    pub theme_supports: ThemeSupports,
20}
21
22// Needed for Spotify oembed, should be fetched from env?
23static APP_USER_AGENT: &str = concat!("moocfi", "/", "0.1.0",);
24
25/**
26GET `/api/v0/cms/gutenberg/oembed/preview?url=https%3A%2F%2Fyoutube.com%2Fwatch%3Fv%3D123123123` - Fetch oembed response from correct provider.
27Endpoint for proxying oembed requests to correct provider using url query param.
28
29# Example
30
31Request:
32```http
33GET /api/v0/cms/gutenberg/oembed/preview?url=https%3A%2F%2Fyoutube.com%2Fwatch%3Fv%3D123123123 HTTP/1.1
34Content-Type: application/json
35
36```
37
38Response:
39```json
40{
41    "title":"AUTHOR - Title (OFFICIAL)",
42    "author_name":"Author Name",
43    "author_url":"https://www.youtube.com/author",
44    "type":"video",
45    "height":439,
46    "width":780,
47    "version":"1.0",
48    "provider_name":"YouTube",
49    "provider_url":"https://www.youtube.com/",
50    "thumbnail_height":360,"thumbnail_width":480,
51    "thumbnail_url":"https://i.ytimg.com/vi/JWBo/hqdefault.jpg",
52    "html":"<iframe width=\"780\" height=\"439\" src=\"https://www.youtube.com/embed/JYjVo?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>"}
53}
54
55```
56*/
57#[instrument(skip(pool, app_conf))]
58async fn get_oembed_data_from_provider(
59    query_params: web::Query<OEmbedRequest>,
60    pool: web::Data<PgPool>,
61    user: AuthUser,
62    app_conf: web::Data<ApplicationConfiguration>,
63) -> ControllerResult<web::Json<serde_json::Value>> {
64    let mut conn = pool.acquire().await?;
65    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::AnyCourse).await?;
66    let endpoint = url_to_oembed_endpoint(
67        query_params.url.to_string(),
68        Some(app_conf.base_url.to_string()),
69    )?;
70    let client = reqwest::Client::builder()
71        .user_agent(APP_USER_AGENT)
72        .build()
73        .map_err(|oe| anyhow::anyhow!(oe.to_string()))?;
74    let res = client
75        .get(endpoint)
76        .timeout(Duration::from_secs(120))
77        .send()
78        .await
79        .map_err(|oe| {
80            ControllerError::new(
81                ControllerErrorType::BadRequest,
82                oe.to_string(),
83                Some(oe.into()),
84            )
85        })?;
86    let status = res.status();
87    if !status.is_success() {
88        let response_url = res.url().to_string();
89        let body = res.text().await.map_err(|oe| {
90            ControllerError::new(
91                ControllerErrorType::BadRequest,
92                oe.to_string(),
93                Some(oe.into()),
94            )
95        })?;
96        warn!(url=?response_url, status=?status, body=?body, "Could not fetch oembed data from provider");
97        return Err(ControllerError::new(
98            ControllerErrorType::BadRequest,
99            "Could not fetch oembed data from provider".to_string(),
100            None,
101        ));
102    }
103    let res = res.json::<serde_json::Value>().await.map_err(|oe| {
104        ControllerError::new(
105            ControllerErrorType::BadRequest,
106            oe.to_string(),
107            Some(oe.into()),
108        )
109    })?;
110    token.authorized_ok(web::Json(res))
111}
112
113/**
114GET `/api/v0/cms/gutenberg/themes?context=edit&status=active&_locale=user` - Mock themes response
115Endpoint for proxying themes requests.
116<https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/embed/test/index.native.js#L128>
117
118# Example
119
120Request:
121```http
122GET /api/v0/cms/gutenberg/themes?context=edit&status=active&_locale=user HTTP/1.1
123Content-Type: application/json
124
125```
126
127Response:
128```json
129{
130    {
131        "theme_supports": {
132                "responsive-embeds": true
133            }
134        }
135}
136
137```
138*/
139#[instrument(skip(pool))]
140async fn get_theme_settings(
141    pool: web::Data<PgPool>,
142    user: AuthUser,
143) -> ControllerResult<web::Json<ThemeResponse>> {
144    let mut conn = pool.acquire().await?;
145    let token = authorize(&mut conn, Act::Teach, Some(user.id), Res::AnyCourse).await?;
146    let response = ThemeResponse {
147        theme_supports: ThemeSupports {
148            responsive_embeds: true,
149        },
150    };
151    token.authorized_ok(web::Json(response))
152}
153
154#[instrument(skip(app_conf))]
155async fn get_mentimeter_oembed_data(
156    query_params: web::Query<OEmbedRequest>,
157    app_conf: web::Data<ApplicationConfiguration>,
158    pool: web::Data<PgPool>,
159) -> ControllerResult<web::Json<OEmbedResponse>> {
160    let token = skip_authorize();
161    let url = query_params.url.to_string();
162    let response = mentimeter_oembed_response_builder(url, app_conf.base_url.to_string())?;
163    token.authorized_ok(web::Json(response))
164}
165
166#[instrument(skip(app_conf))]
167async fn get_thinglink_oembed_data(
168    query_params: web::Query<OEmbedRequest>,
169    app_conf: web::Data<ApplicationConfiguration>,
170    pool: web::Data<PgPool>,
171) -> ControllerResult<web::Json<OEmbedResponse>> {
172    let token = skip_authorize();
173    let url = query_params.url.to_string();
174    let response = thinglink_oembed_response_builder(url, app_conf.base_url.to_string())?;
175    token.authorized_ok(web::Json(response))
176}
177
178#[instrument(skip(app_conf))]
179async fn get_vimeo_oembed_data(
180    query_params: web::Query<OEmbedRequest>,
181    app_conf: web::Data<ApplicationConfiguration>,
182    pool: web::Data<PgPool>,
183) -> ControllerResult<web::Json<OEmbedResponse>> {
184    let token = skip_authorize();
185    let url = query_params.url.to_string();
186    let response = vimeo_oembed_response_builder(url, app_conf.base_url.to_string())?;
187    token.authorized_ok(web::Json(response))
188}
189
190/**
191Add a route for each controller in this module.
192
193The name starts with an underline in order to appear before other functions in the module documentation.
194
195We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
196*/
197pub fn _add_routes(cfg: &mut ServiceConfig) {
198    cfg.route(
199        "/oembed/preview",
200        web::get().to(get_oembed_data_from_provider),
201    )
202    .route("/themes", web::get().to(get_theme_settings))
203    .route(
204        "/oembed/mentimeter",
205        web::get().to(get_mentimeter_oembed_data),
206    )
207    .route(
208        "/oembed/thinglink",
209        web::get().to(get_thinglink_oembed_data),
210    )
211    .route("/oembed/vimeo", web::get().to(get_vimeo_oembed_data));
212}