diff --git a/crates/services/gifbox/src/api.rs b/crates/services/gifbox/src/api.rs index 07883652..36a29757 100644 --- a/crates/services/gifbox/src/api.rs +++ b/crates/services/gifbox/src/api.rs @@ -1,12 +1,18 @@ -use std::sync::Arc; - -use axum::{extract::{Query, State}, routing::get, Json, Router}; +use axum::{ + extract::{Query, State}, + routing::get, + Json, Router, +}; +use revolt_database::User; use revolt_result::{create_error, Result}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use revolt_database::User; -use crate::{AppState, tenor::{Tenor, types}}; +use crate::{ + tenor, + types, + AppState, +}; pub async fn router() -> Router { Router::new() @@ -43,7 +49,7 @@ async fn root() -> Json { struct SearchQueryParams { pub query: String, pub locale: String, - pub position: Option + pub position: Option, } #[utoipa::path( @@ -56,11 +62,13 @@ struct SearchQueryParams { async fn search( _user: User, Query(params): Query, - State(tenor): State, -) -> Result>> { + State(tenor): State, +) -> Result> { // Todo - tenor.search(¶ms.query, ¶ms.locale, params.position.as_deref()) + tenor + .search(¶ms.query, ¶ms.locale, params.position.as_deref()) .await .map_err(|_| create_error!(InternalError)) + .map(|results| results.as_ref().clone().into()) .map(Json) -} \ No newline at end of file +} diff --git a/crates/services/gifbox/src/main.rs b/crates/services/gifbox/src/main.rs index 3cfb2e95..a3339078 100644 --- a/crates/services/gifbox/src/main.rs +++ b/crates/services/gifbox/src/main.rs @@ -15,11 +15,12 @@ use crate::tenor::Tenor; mod api; mod tenor; +mod types; #[derive(Clone)] struct AppState { pub database: Database, - pub tenor: Tenor + pub tenor: Tenor, } impl FromRef for Database { @@ -45,11 +46,15 @@ async fn main() -> Result<(), std::io::Error> { modifiers(&SecurityAddon), paths( api::root, + api::search, ), components( schemas( revolt_result::Error, revolt_result::ErrorType, + types::SearchResponse, + types::MediaObject, + types::MediaResult, ) ) )] @@ -70,8 +75,11 @@ async fn main() -> Result<(), std::io::Error> { let config = config().await; let state = AppState { - database: DatabaseInfo::Auto.connect().await.expect("Unable to connect to database"), - tenor: tenor::Tenor::new(&config.api.security.tenor_key) + database: DatabaseInfo::Auto + .connect() + .await + .expect("Unable to connect to database"), + tenor: tenor::Tenor::new(&config.api.security.tenor_key), }; // Configure Axum and router diff --git a/crates/services/gifbox/src/tenor/mod.rs b/crates/services/gifbox/src/tenor/mod.rs index 56db6269..2ff3785c 100644 --- a/crates/services/gifbox/src/tenor/mod.rs +++ b/crates/services/gifbox/src/tenor/mod.rs @@ -1,10 +1,10 @@ use std::{sync::Arc, time::Duration}; -use reqwest::Client; -use tokio::sync::RwLock; use lru_time_cache::LruCache; -use urlencoding::encode as url_encode; +use reqwest::Client; use revolt_coalesced::{CoalescionService, CoalescionServiceConfig}; +use tokio::sync::RwLock; +use urlencoding::encode as url_encode; pub mod types; @@ -20,13 +20,13 @@ pub struct Tenor { pub key: Arc, pub client: Client, pub coalescion: CoalescionService, TenorError>>, - pub cache: Arc>>> + pub cache: Arc>>>, } impl Tenor { pub fn new(key: &str) -> Self { // 1 hour, 1k queries - let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60*60), 1000); + let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60 * 60), 1000); Self { key: Arc::from(key), @@ -34,18 +34,23 @@ impl Tenor { coalescion: CoalescionService::from_config(CoalescionServiceConfig { max_concurrent: Some(100), queue_requests: true, - max_queue: None + max_queue: None, }), cache: Arc::new(RwLock::new(cache)), } } - pub async fn search(&self, query: &str, locale: &str, position: Option<&str>) -> Result, TenorError> { + pub async fn search( + &self, + query: &str, + locale: &str, + position: Option<&str>, + ) -> Result, TenorError> { let unique_key = format!("{query}:{locale}:{position:?}"); if self.cache.read().await.contains_key(&unique_key) { if let Some(response) = self.cache.write().await.get(&unique_key) { - return Ok(response.clone()) + return Ok(response.clone()); } } @@ -54,7 +59,7 @@ impl Tenor { async move { let mut url = format!( - "{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&locale={}&contentfilter=medium&limit=1", + "{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=medium&limit=1", &self.key, url_encode(query), url_encode(locale), @@ -86,44 +91,3 @@ impl Tenor { (*res).clone() } } - -#[cfg(test)] -mod tests { - use super::*; - use revolt_config::config; - - #[tokio::test(flavor = "current_thread")] - async fn test() { - let config = config().await; - - let tenor = Tenor::new(&config.api.security.tenor_key); - - let results =tenor.search("amog", "en_US", None).await.unwrap(); - - let result = &results.results[0]; - - println!("{:?}", result.media_formats.iter().next().unwrap()); - } - - #[tokio::test(flavor = "current_thread")] - async fn test_2() { - let config = config().await; - let tenor = Tenor::new(&config.api.security.tenor_key); - - let mut tasks = Vec::new(); - - for i in 1..1001 { - let tenor = tenor.clone(); - println!("creating search task {i}"); - - tasks.push((i, tokio::spawn(async move { - tenor.search(&format!("amog-{i}"), "en_US", None).await - }))); - }; - - for (i, task) in tasks { - task.await.unwrap().unwrap(); - println!("Got result for {i}"); - }; - } -} \ No newline at end of file diff --git a/crates/services/gifbox/src/tenor/types.rs b/crates/services/gifbox/src/tenor/types.rs index a712fcb7..b4ad4a08 100644 --- a/crates/services/gifbox/src/tenor/types.rs +++ b/crates/services/gifbox/src/tenor/types.rs @@ -1,12 +1,11 @@ use std::collections::HashMap; -use serde::{Serialize, Deserialize}; - +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct SearchResponse { - pub results: Vec, - pub next: String + pub results: Vec, + pub next: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -14,11 +13,11 @@ pub struct MediaObject { pub url: String, pub dims: Vec, pub duration: f64, - pub size: f64 + pub size: f64, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct TenorResult { +pub struct MediaResult { pub created: f64, #[serde(default)] pub hasaudio: bool, @@ -32,5 +31,5 @@ pub struct TenorResult { pub hascaption: bool, pub flags: Vec, pub bg_color: Option, - pub url: String -} \ No newline at end of file + pub url: String, +} diff --git a/crates/services/gifbox/src/types.rs b/crates/services/gifbox/src/types.rs new file mode 100644 index 00000000..d4267602 --- /dev/null +++ b/crates/services/gifbox/src/types.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use utoipa::ToSchema; +use serde::{Serialize, Deserialize}; + +use crate::tenor::types; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +/// Response containing the current results and the id of the next result for pagination. +pub struct SearchResponse { + /// Current gif results. + pub results: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + /// Id of the next result. + pub next: Option, +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +/// Indivual gif result. +pub struct MediaResult { + /// Unique Tenor id. + pub id: String, + /// Mapping of each file format and url of the file. + pub media_formats: HashMap, + /// Public Tenor web url for the gif. + pub url: String +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +/// Represents the gif in a certain file format. +pub struct MediaObject { + /// File url of the gif in a certain format. + pub url: String, + /// Width and height of the file in px. + pub dimensions: Vec, +} + + +impl From for SearchResponse { + fn from(value: types::SearchResponse) -> Self { + Self { + results: value.results.into_iter().map(|result| result.into()).collect(), + next: if value.next.is_empty() { + None + } else { + Some(value.next) + } + } + } +} + +impl From for MediaResult { + fn from(value: types::MediaResult) -> Self { + Self { + id: value.id, + media_formats: value.media_formats.into_iter().map(|(k, v)| (k, v.into())).collect(), + url: value.url + } + } +} + +impl From for MediaObject { + fn from(value: types::MediaObject) -> Self { + Self { + url: value.url, + dimensions: value.dims + } + } +} \ No newline at end of file