forked from jmug/stoatchat
feat: trending and categories routes
This commit is contained in:
committed by
Angelo Kontaxis
parent
a92152d86d
commit
5885e067a6
@@ -1,74 +0,0 @@
|
|||||||
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 crate::{
|
|
||||||
tenor,
|
|
||||||
types,
|
|
||||||
AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn router() -> Router<AppState> {
|
|
||||||
Router::new()
|
|
||||||
.route("/", get(root))
|
|
||||||
.route("/search", get(search))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Successful root response
|
|
||||||
#[derive(Serialize, Debug, ToSchema)]
|
|
||||||
pub struct RootResponse {
|
|
||||||
message: &'static str,
|
|
||||||
version: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Capture crate version from Cargo
|
|
||||||
static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
/// Root response from service
|
|
||||||
#[utoipa::path(
|
|
||||||
get,
|
|
||||||
path = "/",
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Echo response", body = RootResponse)
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
async fn root() -> Json<RootResponse> {
|
|
||||||
Json(RootResponse {
|
|
||||||
message: "Gifbox lives on!",
|
|
||||||
version: CRATE_VERSION,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct SearchQueryParams {
|
|
||||||
pub query: String,
|
|
||||||
pub locale: String,
|
|
||||||
pub position: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[utoipa::path(
|
|
||||||
get,
|
|
||||||
path = "/search",
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Search results", body = SearchResponse)
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
async fn search(
|
|
||||||
_user: User,
|
|
||||||
Query(params): Query<SearchQueryParams>,
|
|
||||||
State(tenor): State<tenor::Tenor>,
|
|
||||||
) -> Result<Json<types::SearchResponse>> {
|
|
||||||
// Todo
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,14 @@ use revolt_config::config;
|
|||||||
use revolt_database::{Database, DatabaseInfo};
|
use revolt_database::{Database, DatabaseInfo};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
|
||||||
Modify, OpenApi,
|
Modify, OpenApi,
|
||||||
};
|
};
|
||||||
use utoipa_scalar::{Scalar, Servable as ScalarServable};
|
use utoipa_scalar::{Scalar, Servable as ScalarServable};
|
||||||
|
|
||||||
use crate::tenor::Tenor;
|
use crate::tenor::Tenor;
|
||||||
|
|
||||||
mod api;
|
mod routes;
|
||||||
mod tenor;
|
mod tenor;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
@@ -35,6 +35,26 @@ impl FromRef<AppState> for Tenor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TokenAddon;
|
||||||
|
|
||||||
|
impl Modify for TokenAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
let components = openapi.components.get_or_insert_default();
|
||||||
|
|
||||||
|
components.add_security_scheme(
|
||||||
|
"User Token",
|
||||||
|
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new(
|
||||||
|
"X-Session-Token".to_string(),
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
|
||||||
|
components.add_security_scheme(
|
||||||
|
"Bot Token",
|
||||||
|
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Bot-Token".to_string()))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), std::io::Error> {
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
// Configure logging and environment
|
// Configure logging and environment
|
||||||
@@ -43,36 +63,28 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
// Configure API schema
|
// Configure API schema
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
modifiers(&SecurityAddon),
|
modifiers(&TokenAddon),
|
||||||
paths(
|
paths(
|
||||||
api::root,
|
routes::categories::categories,
|
||||||
api::search,
|
routes::root::root,
|
||||||
|
routes::search::search,
|
||||||
|
routes::trending::trending,
|
||||||
|
),
|
||||||
|
tags(
|
||||||
|
(name = "Misc", description = "Misc routes for microservice."),
|
||||||
|
(name = "GIFs", description = "All routes for requesting GIFs from tenor.")
|
||||||
),
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
revolt_result::Error,
|
revolt_result::Error,
|
||||||
revolt_result::ErrorType,
|
revolt_result::ErrorType,
|
||||||
types::SearchResponse,
|
|
||||||
types::MediaObject,
|
|
||||||
types::MediaResult,
|
types::MediaResult,
|
||||||
|
types::MediaObject,
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
|
|
||||||
struct SecurityAddon;
|
|
||||||
|
|
||||||
impl Modify for SecurityAddon {
|
|
||||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
|
||||||
if let Some(components) = openapi.components.as_mut() {
|
|
||||||
components.add_security_scheme(
|
|
||||||
"api_key",
|
|
||||||
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
database: DatabaseInfo::Auto
|
database: DatabaseInfo::Auto
|
||||||
@@ -85,7 +97,7 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
// Configure Axum and router
|
// Configure Axum and router
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
|
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
|
||||||
.nest("/", api::router().await)
|
.nest("/", routes::router())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
// Configure TCP listener and bind
|
// Configure TCP listener and bind
|
||||||
|
|||||||
47
crates/services/gifbox/src/routes/categories.rs
Normal file
47
crates/services/gifbox/src/routes/categories.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use revolt_database::User;
|
||||||
|
use revolt_result::{create_error, Result};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::{tenor, types};
|
||||||
|
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
pub struct CategoriesQueryParams {
|
||||||
|
#[param(example = "en_US")]
|
||||||
|
pub locale: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trending GIF categories
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/categories",
|
||||||
|
tag = "GIFs",
|
||||||
|
security(("User Token" = []), ("Bot Token" = [])),
|
||||||
|
params(CategoriesQueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Categories results", body = inline(Vec<types::CategoryResponse>))
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn categories(
|
||||||
|
_user: User,
|
||||||
|
Query(params): Query<CategoriesQueryParams>,
|
||||||
|
State(tenor): State<tenor::Tenor>,
|
||||||
|
) -> Result<Json<Vec<types::CategoryResponse>>> {
|
||||||
|
tenor
|
||||||
|
.categories(¶ms.locale)
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_error!(InternalError))
|
||||||
|
.map(|results| {
|
||||||
|
(*results)
|
||||||
|
.clone()
|
||||||
|
.tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|cat| cat.into())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.map(Json)
|
||||||
|
}
|
||||||
15
crates/services/gifbox/src/routes/mod.rs
Normal file
15
crates/services/gifbox/src/routes/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use axum::routing::{get, Router};
|
||||||
|
|
||||||
|
pub mod categories;
|
||||||
|
pub mod root;
|
||||||
|
pub mod search;
|
||||||
|
pub mod trending;
|
||||||
|
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(root::root))
|
||||||
|
.route("/categories", get(categories::categories))
|
||||||
|
.route("/search", get(search::search))
|
||||||
|
.route("/trending", get(trending::trending))
|
||||||
|
}
|
||||||
22
crates/services/gifbox/src/routes/root.rs
Normal file
22
crates/services/gifbox/src/routes/root.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use axum::Json;
|
||||||
|
|
||||||
|
use crate::types;
|
||||||
|
|
||||||
|
/// Capture crate version from Cargo
|
||||||
|
static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// Root response from service
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/",
|
||||||
|
tag = "Misc",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Root response", body = inline(types::RootResponse))
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn root() -> Json<types::RootResponse<'static>> {
|
||||||
|
Json(types::RootResponse {
|
||||||
|
message: "Gifbox lives on!",
|
||||||
|
version: CRATE_VERSION,
|
||||||
|
})
|
||||||
|
}
|
||||||
51
crates/services/gifbox/src/routes/search.rs
Normal file
51
crates/services/gifbox/src/routes/search.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use revolt_database::User;
|
||||||
|
use revolt_result::{create_error, Result};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::{tenor, types};
|
||||||
|
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
pub struct SearchQueryParams {
|
||||||
|
#[param(example = "Wave")]
|
||||||
|
pub query: String,
|
||||||
|
#[param(example = "en_US")]
|
||||||
|
pub locale: String,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub is_category: Option<bool>,
|
||||||
|
pub position: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Searches for GIFs with a query
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/search",
|
||||||
|
tag = "GIFs",
|
||||||
|
security(("User Token" = []), ("Bot Token" = [])),
|
||||||
|
params(SearchQueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Search results", body = inline(types::PaginatedMediaResponse))
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn search(
|
||||||
|
_user: User,
|
||||||
|
Query(params): Query<SearchQueryParams>,
|
||||||
|
State(tenor): State<tenor::Tenor>,
|
||||||
|
) -> Result<Json<types::PaginatedMediaResponse>> {
|
||||||
|
tenor
|
||||||
|
.search(
|
||||||
|
¶ms.query,
|
||||||
|
¶ms.locale,
|
||||||
|
params.limit.unwrap_or(50),
|
||||||
|
params.is_category.unwrap_or_default(),
|
||||||
|
params.position.as_deref().unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_error!(InternalError))
|
||||||
|
.map(|results| results.as_ref().clone().into())
|
||||||
|
.map(Json)
|
||||||
|
}
|
||||||
45
crates/services/gifbox/src/routes/trending.rs
Normal file
45
crates/services/gifbox/src/routes/trending.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use revolt_database::User;
|
||||||
|
use revolt_result::{create_error, Result};
|
||||||
|
use serde::{Deserialize};
|
||||||
|
use utoipa::{IntoParams};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
tenor,
|
||||||
|
types,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
pub struct TrendingQueryParams {
|
||||||
|
#[param(example = "en_US")]
|
||||||
|
pub locale: String,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub position: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trending GIFs
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/featured",
|
||||||
|
tag = "GIFs",
|
||||||
|
security(("User Token" = []), ("Bot Token" = [])),
|
||||||
|
params(TrendingQueryParams),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Trending results", body = inline(types::PaginatedMediaResponse))
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn trending(
|
||||||
|
_user: User,
|
||||||
|
Query(params): Query<TrendingQueryParams>,
|
||||||
|
State(tenor): State<tenor::Tenor>,
|
||||||
|
) -> Result<Json<types::PaginatedMediaResponse>> {
|
||||||
|
tenor
|
||||||
|
.featured(¶ms.locale, params.limit.unwrap_or(50), params.position.as_deref().unwrap_or_default())
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_error!(InternalError))
|
||||||
|
.map(|results| results.as_ref().clone().into())
|
||||||
|
.map(Json)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration};
|
|||||||
use lru_time_cache::LruCache;
|
use lru_time_cache::LruCache;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use revolt_coalesced::{CoalescionService, CoalescionServiceConfig};
|
use revolt_coalesced::{CoalescionService, CoalescionServiceConfig};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use urlencoding::encode as url_encode;
|
use urlencoding::encode as url_encode;
|
||||||
|
|
||||||
@@ -19,34 +20,89 @@ pub enum TenorError {
|
|||||||
pub struct Tenor {
|
pub struct Tenor {
|
||||||
pub key: Arc<str>,
|
pub key: Arc<str>,
|
||||||
pub client: Client,
|
pub client: Client,
|
||||||
pub coalescion: CoalescionService<String, Result<Arc<types::SearchResponse>, TenorError>>,
|
|
||||||
pub cache: Arc<RwLock<LruCache<String, Arc<types::SearchResponse>>>>,
|
pub cache: Arc<RwLock<LruCache<String, Arc<types::PaginatedMediaResponse>>>>,
|
||||||
|
pub cache_coalescion:
|
||||||
|
CoalescionService<String, Result<Arc<types::PaginatedMediaResponse>, TenorError>>,
|
||||||
|
|
||||||
|
pub categories: Arc<RwLock<LruCache<String, Arc<types::CategoriesResponse>>>>,
|
||||||
|
pub categories_coalescion:
|
||||||
|
CoalescionService<String, Result<Arc<types::CategoriesResponse>, TenorError>>,
|
||||||
|
|
||||||
|
pub featured: Arc<RwLock<LruCache<String, Arc<types::PaginatedMediaResponse>>>>,
|
||||||
|
pub featured_coalescion:
|
||||||
|
CoalescionService<String, Result<Arc<types::PaginatedMediaResponse>, TenorError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tenor {
|
impl Tenor {
|
||||||
pub fn new(key: &str) -> Self {
|
pub fn new(key: &str) -> Self {
|
||||||
// 1 hour, 1k queries
|
|
||||||
let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60 * 60), 1000);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
key: Arc::from(key),
|
key: Arc::from(key),
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
|
||||||
|
// 1 hour, 1k requests
|
||||||
|
cache: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity(
|
||||||
|
Duration::from_secs(60 * 60),
|
||||||
|
1000,
|
||||||
|
))),
|
||||||
|
cache_coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
||||||
|
max_concurrent: Some(100),
|
||||||
|
queue_requests: true,
|
||||||
|
max_queue: None,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 1 day, 1k requests
|
||||||
|
categories: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity(
|
||||||
|
Duration::from_secs(60 * 60 * 24),
|
||||||
|
1000,
|
||||||
|
))),
|
||||||
|
categories_coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
||||||
|
max_concurrent: Some(100),
|
||||||
|
queue_requests: true,
|
||||||
|
max_queue: None,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 1 day, 1k requests
|
||||||
|
featured: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity(
|
||||||
|
Duration::from_secs(60 * 60 * 24),
|
||||||
|
1000,
|
||||||
|
))),
|
||||||
|
featured_coalescion: CoalescionService::from_config(CoalescionServiceConfig {
|
||||||
max_concurrent: Some(100),
|
max_concurrent: Some(100),
|
||||||
queue_requests: true,
|
queue_requests: true,
|
||||||
max_queue: None,
|
max_queue: None,
|
||||||
}),
|
}),
|
||||||
cache: Arc::new(RwLock::new(cache)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn request<T: DeserializeOwned>(&self, path: String) -> Result<Arc<T>, TenorError> {
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.get(format!("{TENOR_API_BASE_URL}{path}"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
revolt_config::capture_error(e);
|
||||||
|
})
|
||||||
|
.map_err(|_| TenorError::HttpError)?;
|
||||||
|
|
||||||
|
let text = response.text().await.map_err(|e| {
|
||||||
|
println!("{e:?}");
|
||||||
|
TenorError::HttpError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Arc::new(serde_json::from_str(&text).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn search(
|
pub async fn search(
|
||||||
&self,
|
&self,
|
||||||
query: &str,
|
query: &str,
|
||||||
locale: &str,
|
locale: &str,
|
||||||
position: Option<&str>,
|
limit: u32,
|
||||||
) -> Result<Arc<types::SearchResponse>, TenorError> {
|
is_category: bool,
|
||||||
let unique_key = format!("{query}:{locale}:{position:?}");
|
position: &str,
|
||||||
|
) -> Result<Arc<types::PaginatedMediaResponse>, TenorError> {
|
||||||
|
let unique_key = format!("s:{query}:{locale}:{is_category}:{position}");
|
||||||
|
|
||||||
if self.cache.read().await.contains_key(&unique_key) {
|
if self.cache.read().await.contains_key(&unique_key) {
|
||||||
if let Some(response) = self.cache.write().await.get(&unique_key) {
|
if let Some(response) = self.cache.write().await.get(&unique_key) {
|
||||||
@@ -54,32 +110,24 @@ impl Tenor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = self.coalescion.execute(unique_key.clone(), || {
|
let res = self.cache_coalescion.execute(unique_key.clone(), || {
|
||||||
let client = self.client.clone();
|
let mut path = format!(
|
||||||
|
"/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}",
|
||||||
|
&self.key,
|
||||||
|
url_encode(query),
|
||||||
|
url_encode(locale),
|
||||||
|
);
|
||||||
|
|
||||||
async move {
|
if !position.is_empty() {
|
||||||
let mut url = format!(
|
path.push_str("&pos=");
|
||||||
"{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=medium&limit=1",
|
path.push_str(position);
|
||||||
&self.key,
|
};
|
||||||
url_encode(query),
|
|
||||||
url_encode(locale),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(position) = position {
|
if is_category {
|
||||||
url.push_str("&pos=");
|
path.push_str("&component=categories");
|
||||||
url.push_str(position);
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = client.get(url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| { revolt_config::capture_error(e); })
|
|
||||||
.map_err(|_| TenorError::HttpError)?;
|
|
||||||
|
|
||||||
let text = response.text().await.map_err(|e| { println!("{e:?}"); TenorError::HttpError })?;
|
|
||||||
|
|
||||||
Ok(Arc::new(serde_json::from_str(&text).unwrap()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.request(path)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -90,4 +138,81 @@ impl Tenor {
|
|||||||
|
|
||||||
(*res).clone()
|
(*res).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn categories(
|
||||||
|
&self,
|
||||||
|
locale: &str,
|
||||||
|
) -> Result<Arc<types::CategoriesResponse>, TenorError> {
|
||||||
|
let unique_key = format!("c-{locale}");
|
||||||
|
|
||||||
|
if self.categories.read().await.contains_key(&unique_key) {
|
||||||
|
if let Some(response) = self.categories.write().await.get(&unique_key) {
|
||||||
|
return Ok(response.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.categories_coalescion
|
||||||
|
.execute(unique_key.clone(), || {
|
||||||
|
let path = format!(
|
||||||
|
"/categories?key={}&client_key=Gifbox&locale={}&contentfilter=high",
|
||||||
|
&self.key,
|
||||||
|
url_encode(locale),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.request(path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Ok(resp) = &*res {
|
||||||
|
self.categories
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(unique_key, resp.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
(*res).clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn featured(
|
||||||
|
&self,
|
||||||
|
locale: &str,
|
||||||
|
limit: u32,
|
||||||
|
position: &str,
|
||||||
|
) -> Result<Arc<types::PaginatedMediaResponse>, TenorError> {
|
||||||
|
let unique_key = format!("f-{locale}-{limit}-{position}");
|
||||||
|
|
||||||
|
if self.categories.read().await.contains_key(&unique_key) {
|
||||||
|
if let Some(response) = self.featured.write().await.get(&unique_key) {
|
||||||
|
return Ok(response.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = self.featured_coalescion.execute(unique_key.clone(), || {
|
||||||
|
let mut path = format!(
|
||||||
|
"/featured?key={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}",
|
||||||
|
&self.key,
|
||||||
|
url_encode(locale),
|
||||||
|
);
|
||||||
|
|
||||||
|
if !position.is_empty() {
|
||||||
|
path.push_str("&pos=");
|
||||||
|
path.push_str(position);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.request(path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Ok(resp) = &*res {
|
||||||
|
self.featured
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(unique_key, resp.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
(*res).clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use std::collections::HashMap;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
pub struct SearchResponse {
|
pub struct PaginatedMediaResponse {
|
||||||
pub results: Vec<MediaResult>,
|
pub results: Vec<MediaResponse>,
|
||||||
pub next: String,
|
pub next: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ pub struct MediaObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
pub struct MediaResult {
|
pub struct MediaResponse {
|
||||||
pub created: f64,
|
pub created: f64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hasaudio: bool,
|
pub hasaudio: bool,
|
||||||
@@ -33,3 +33,17 @@ pub struct MediaResult {
|
|||||||
pub bg_color: Option<String>,
|
pub bg_color: Option<String>,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct CategoriesResponse {
|
||||||
|
pub locale: String,
|
||||||
|
pub tags: Vec<CategoryResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct CategoryResponse {
|
||||||
|
pub searchterm: String,
|
||||||
|
pub path: String,
|
||||||
|
pub image: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
use crate::tenor::types;
|
use crate::tenor::types;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
|
/// Successful root response
|
||||||
|
#[derive(Serialize, Debug, ToSchema)]
|
||||||
|
pub struct RootResponse<'a> {
|
||||||
|
pub message: &'a str,
|
||||||
|
pub version: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
/// Response containing the current results and the id of the next result for pagination.
|
/// Response containing the current results and the id of the next result for pagination.
|
||||||
pub struct SearchResponse {
|
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
|
||||||
|
pub struct PaginatedMediaResponse {
|
||||||
/// Current gif results.
|
/// Current gif results.
|
||||||
pub results: Vec<MediaResult>,
|
pub results: Vec<MediaResult>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -15,19 +22,19 @@ pub struct SearchResponse {
|
|||||||
pub next: Option<String>,
|
pub next: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
|
|
||||||
/// Indivual gif result.
|
/// Indivual gif result.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
|
||||||
pub struct MediaResult {
|
pub struct MediaResult {
|
||||||
/// Unique Tenor id.
|
/// Unique Tenor id.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// Mapping of each file format and url of the file.
|
/// Mapping of each file format and url of the file.
|
||||||
pub media_formats: HashMap<String, MediaObject>,
|
pub media_formats: HashMap<String, MediaObject>,
|
||||||
/// Public Tenor web url for the gif.
|
/// Public Tenor web url for the gif.
|
||||||
pub url: String
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
|
|
||||||
/// Represents the gif in a certain file format.
|
/// Represents the gif in a certain file format.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
|
||||||
pub struct MediaObject {
|
pub struct MediaObject {
|
||||||
/// File url of the gif in a certain format.
|
/// File url of the gif in a certain format.
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@@ -35,26 +42,42 @@ pub struct MediaObject {
|
|||||||
pub dimensions: Vec<u64>,
|
pub dimensions: Vec<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a GIF category
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
|
||||||
|
pub struct CategoryResponse {
|
||||||
|
/// Category title
|
||||||
|
pub title: String,
|
||||||
|
/// Category image
|
||||||
|
pub image: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl From<types::SearchResponse> for SearchResponse {
|
impl From<types::PaginatedMediaResponse> for PaginatedMediaResponse {
|
||||||
fn from(value: types::SearchResponse) -> Self {
|
fn from(value: types::PaginatedMediaResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
results: value.results.into_iter().map(|result| result.into()).collect(),
|
results: value
|
||||||
|
.results
|
||||||
|
.into_iter()
|
||||||
|
.map(|result| result.into())
|
||||||
|
.collect(),
|
||||||
next: if value.next.is_empty() {
|
next: if value.next.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(value.next)
|
Some(value.next)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<types::MediaResult> for MediaResult {
|
impl From<types::MediaResponse> for MediaResult {
|
||||||
fn from(value: types::MediaResult) -> Self {
|
fn from(value: types::MediaResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
media_formats: value.media_formats.into_iter().map(|(k, v)| (k, v.into())).collect(),
|
media_formats: value
|
||||||
url: value.url
|
.media_formats
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, v.into()))
|
||||||
|
.collect(),
|
||||||
|
url: value.url,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +86,16 @@ impl From<types::MediaObject> for MediaObject {
|
|||||||
fn from(value: types::MediaObject) -> Self {
|
fn from(value: types::MediaObject) -> Self {
|
||||||
Self {
|
Self {
|
||||||
url: value.url,
|
url: value.url,
|
||||||
dimensions: value.dims
|
dimensions: value.dims,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::CategoryResponse> for CategoryResponse {
|
||||||
|
fn from(value: types::CategoryResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
title: value.searchterm,
|
||||||
|
image: value.image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user