Skip to content

Commit

Permalink
migrated the rest api to be more complient with convention
Browse files Browse the repository at this point in the history
  • Loading branch information
CommanderStorm committed Jul 28, 2024
1 parent ad1e333 commit 18f7c09
Show file tree
Hide file tree
Showing 23 changed files with 264 additions and 249 deletions.
2 changes: 1 addition & 1 deletion data/processors/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def export_for_status() -> None:


def export_for_api(data: dict) -> None:
"""Add some more information about parents to the data and export for the /get/:id api"""
"""Add some more information about parents to the data and export for the /locations/:id api"""
export_data = []
for _id, entry in data.items():
entry.setdefault("maps", {})["default"] = "interactive"
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ services:
- "traefik.enable=true"
- "traefik.http.routers.navigatum-main-api.entrypoints=webs"
- "traefik.http.routers.navigatum-main-api.tls.certresolver=leacme"
- "traefik.http.routers.navigatum-main-api.rule=Host(`nav.tum.de`) && (PathPrefix(`/api/get/`) || PathPrefix(`/api/locations/`) || Path(`/api/search`) || PathPrefix(`/api/preview/`) || PathPrefix(`/api/feedback/`) || Path(`/api/calendar`) || Path(`/api/status`) || Path(`/api/metrics`))"
- "traefik.http.routers.navigatum-main-api.rule=Host(`nav.tum.de`) && (PathPrefix(`/api/locations/`) || Path(`/api/search`) || PathPrefix(`/api/feedback/`) || Path(`/api/calendar`) || Path(`/api/status`) || Path(`/api/metrics`))"
- "traefik.http.services.navigatum-main-api.loadbalancer.server.port=3003"
networks:
- traefik_traefik
Expand Down
16 changes: 7 additions & 9 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ paths:
$ref: '#/components/schemas/NearbyLocationsResponse'
examples:
mi-hs-1: null
'/api/get/{id}':
'/api/locations/{id}':
get:
operationId: details
summary: Get entry-details
Expand Down Expand Up @@ -832,7 +832,7 @@ paths:
- Waiting for first sync with TUMonline
tags:
- core
'/api/preview/{id}':
'/api/locations/{id}/preview':
get:
operationId: previews
summary: Get a entry-preview
Expand Down Expand Up @@ -1078,7 +1078,7 @@ paths:
operationId: img_cdn
summary: Get title images
description: |
This endpoint is designed to fetch the images, that are described by the `/api/get/{id}`-endpoint.
This endpoint is designed to fetch the images, that are described by the `/api/locations/{id}`-endpoint.
You HAVE to get the proper attribution from that endpoint and use it.
parameters:
- name: size
Expand Down Expand Up @@ -1177,7 +1177,7 @@ paths:
operationId: maps_cdn
summary: Get title images
description: |
This endpoint is designed to fetch the images, that are described by the `/api/get/{id}`-endpoint.
This endpoint is designed to fetch the images, that are described by the `/api/locations/{id}`-endpoint.
You HAVE to get the proper attribution from that endpoint and use it.
parameters:
- name: source
Expand Down Expand Up @@ -2430,15 +2430,13 @@ components:
lon: 11.5240016913201
tags:
- name: core
description: the API to access/search for room information
- name: maps
description: Maps related functionality
description: API to access/search for location information
- name: cdn
description: Images for some of the rooms
- name: feedback
description: the API to access/search for room information
description: APIs to give feedback
- name: health
description: These endpoints are used to check the health of our components
description: endpoints used to check the health of our components
externalDocs:
description: Visit our Github Page for more in-depth documentation
url: 'https://github.com/TUM-Dev/navigatum'
Expand Down
4 changes: 2 additions & 2 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ FROM rust:1.80-alpine AS compiler
# this is not an issue since we copy the generated binary to a more minimal envornment
# Descriptions:
# - musl-dev is needed for musl to compile the binary
# - openssl is needed for https://github.com/meilisearch/meilisearch-rust to compile the binary, as their http-libraryy won't support tls until v2.0 https://github.com/sagebind/isahc/issues/199
# I somehow could not get openssl to cooperate => we are contibuing with libpq-dev
# - mold is used to link faster
# - I somehow could not get openssl to cooperate => we are contibuing with libpq-dev
RUN apk add -q --update-cache --no-cache musl-dev libpq-dev mold

WORKDIR /compiler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tracing::error;
use crate::models::LocationKeyAlias;
use crate::localisation;

#[get("/api/get/{id}")]
#[get("/{id}")]
pub async fn get_handler(
params: web::Path<String>,
web::Query(args): web::Query<localisation::LangQueryArgs>,
Expand Down
16 changes: 16 additions & 0 deletions server/main-api/src/locations/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use actix_web::web;

mod details;
mod nearby;
mod preview;

pub fn configure(cfg: &mut web::ServiceConfig) {
cfg
.service(details::get_handler)
.service(nearby::nearby_handler)
.service(preview::maps_handler);
let tile_cache = std::env::temp_dir().join("tiles");
if !tile_cache.exists() {
std::fs::create_dir(tile_cache).unwrap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct NearbyResponse {
public_transport: Vec<Transportation>,
}

#[get("/api/location/{id}/nearby")]
#[get("/{id}/nearby")]
pub async fn nearby_handler(
params: web::Path<String>,
data: web::Data<crate::AppData>,
Expand Down
205 changes: 205 additions & 0 deletions server/main-api/src/locations/preview.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use std::io::Cursor;

use actix_web::http::header::LOCATION;
use actix_web::{get, web, HttpResponse};
use image::{ImageBuffer, Rgba};
use serde::Deserialize;
use sqlx::Error::RowNotFound;
use sqlx::PgPool;
use tracing::{error, warn};
use unicode_truncate::UnicodeTruncateStr;

use crate::limited::vec::LimitedVec;
use crate::maps::overlay_map::OverlayMapTask;
use crate::maps::overlay_text::{OverlayText, CANTARELL_BOLD, CANTARELL_REGULAR};
use crate::models::Location;
use crate::models::LocationKeyAlias;
use crate::localisation;

#[tracing::instrument(skip(pool))]
async fn get_localised_data(
pool: &PgPool,
id: &str,
should_use_english: bool,
) -> Result<Location, HttpResponse> {
let result = if should_use_english {
sqlx::query_as!(Location, "SELECT key,name,last_calendar_scrape_at,calendar_url,type,type_common_name,lat,lon FROM en WHERE key = $1", id)
.fetch_all(pool)
.await
} else {
sqlx::query_as!(Location, "SELECT key,name,last_calendar_scrape_at,calendar_url,type,type_common_name,lat,lon FROM de WHERE key = $1", id)
.fetch_all(pool)
.await
};

match result {
Ok(r) => match r.len() {
0 => Err(HttpResponse::NotFound()
.content_type("text/plain")
.body("Not found")),
_ => Ok(r[0].clone()),
},
Err(e) => {
error!("Error preparing statement: {e:?}");
return Err(HttpResponse::InternalServerError()
.content_type("text/plain")
.body("Internal Server Error"));
}
}
}

#[tracing::instrument]
async fn construct_image_from_data(
data: Location,
format: PreviewFormat,
) -> Option<LimitedVec<u8>> {
let mut img = match format {
PreviewFormat::OpenGraph => image::RgbaImage::new(1200, 630),
PreviewFormat::Square => image::RgbaImage::new(1200, 1200),
};

// add the map
if !OverlayMapTask::from(&data).draw_onto(&mut img).await {
return None;
}
draw_pin(&mut img);

draw_bottom(&data, &mut img);
Some(wrap_image_in_response(&img))
}

/// add the location pin image to the center
#[tracing::instrument(skip(img),level = tracing::Level::DEBUG, )]
fn draw_pin(img: &mut ImageBuffer<Rgba<u8>, Vec<u8>>) {
let pin = image::load_from_memory(include_bytes!("static/pin.png")).unwrap();
image::imageops::overlay(
img,
&pin,
(img.width() as i64) / 2 - i64::from(pin.width()) / 2,
((img.height() as i64) - 125) / 2 - i64::from(pin.height()),
);
}

fn wrap_image_in_response(img: &image::RgbaImage) -> LimitedVec<u8> {
let mut w = Cursor::new(Vec::new());
img.write_to(&mut w, image::ImageFormat::Png).unwrap();
LimitedVec(w.into_inner())
}
const WHITE_PIXEL: Rgba<u8> = Rgba([255, 255, 255, 255]);

#[tracing::instrument(skip(img),level = tracing::Level::DEBUG)]
fn draw_bottom(data: &Location, img: &mut image::RgbaImage) {
// draw background white
for x in 0..img.width() {
for y in img.height() - 125..img.height() {
img.put_pixel(x, y, WHITE_PIXEL);
}
}
// add our logo so the bottom
let logo = image::load_from_memory(include_bytes!("static/logo.png")).unwrap();
image::imageops::overlay(
img,
&logo,
15,
img.height() as i64 - (125 / 2) - (i64::from(logo.height()) / 2) + 9,
);
let name = if data.name.chars().count() >= 45 {
format!("{}...", data.name.unicode_truncate(45).0)
} else {
data.name.clone()
};
OverlayText::with(&name, &CANTARELL_BOLD)
.at(10, 125 - 10)
.draw_onto(img);
OverlayText::with(&data.type_common_name, &CANTARELL_REGULAR)
.at(10, 125 - 50)
.draw_onto(img);
}

fn load_default_image() -> LimitedVec<u8> {
warn!("Loading default preview image, as map rendering failed. Check the connection to the tileserver");
let img = image::load_from_memory(include_bytes!("static/logo-card.png")).unwrap();
// encode the image as PNG
let mut w = Cursor::new(Vec::new());
img.write_to(&mut w, image::ImageFormat::Png).unwrap();
LimitedVec(w.into_inner())
}

#[tracing::instrument(skip(pool))]
async fn get_possible_redirect_url(pool: &PgPool, query: &str, args: &QueryArgs) -> Option<String> {
let result = sqlx::query_as!(
LocationKeyAlias,
r#"
SELECT key, visible_id, type
FROM aliases
WHERE alias = $1 AND key <> alias
LIMIT 1"#,
query
)
.fetch_one(pool)
.await;
match result {
Ok(d) => Some(format!(
"https://nav.tum.de/api/locations/{key}/preview?lang={lang}&format={format}",
key = d.key,
lang = args.lang.serialise(),
format = args.format.serialise()
)),
Err(RowNotFound) => None,
Err(e) => {
error!("Error requesting alias for {query}: {e:?}");
None
}
}
}

#[derive(Deserialize, Default, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
enum PreviewFormat {
#[default]
OpenGraph,
Square,
}
impl PreviewFormat {
fn serialise(self) -> String {
match self {
PreviewFormat::OpenGraph => "open_graph".to_string(),
PreviewFormat::Square => "square".to_string(),
}
}
}

#[derive(Deserialize, Default, Debug)]
#[serde(rename_all = "snake_case")]
#[serde(default)]
struct QueryArgs {
#[serde(flatten)]
lang: localisation::LangQueryArgs,
format: PreviewFormat,
}

#[get("/{id}/preview")]
pub async fn maps_handler(
params: web::Path<String>,
web::Query(args): web::Query<QueryArgs>,
data: web::Data<crate::AppData>,
) -> HttpResponse {
let id = params
.into_inner()
.replace(|c: char| c.is_whitespace() || c.is_control(), "");
if let Some(redirect_url) = get_possible_redirect_url(&data.pool, &id, &args).await {
let mut res = HttpResponse::PermanentRedirect();
res.insert_header((LOCATION, redirect_url));
return res.finish();
}
let data = match get_localised_data(&data.pool, &id, args.lang.should_use_english()).await {
Ok(data) => data,
Err(e) => {
return e;
}
};
let img = construct_image_from_data(data, args.format)
.await
.unwrap_or_else(load_default_image);
HttpResponse::Ok().content_type("image/png").body(img.0)
}
File renamed without changes
File renamed without changes
File renamed without changes
24 changes: 17 additions & 7 deletions server/main-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use std::error::Error;
use std::sync::Arc;

use actix_cors::Cors;
use actix_web::{get, middleware, web, App, HttpResponse, HttpServer};
use actix_web::{get, middleware, web, App, HttpResponse, HttpServer, Responder};
use actix_web::web::Redirect;
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use meilisearch_sdk::client::Client;
use sentry::SessionMode;
Expand All @@ -16,15 +17,14 @@ use tracing::{debug_span, error, info};
use tracing_actix_web::TracingLogger;

mod calendar;
mod details;
mod feedback;
mod limited;
mod maps;
mod models;
mod nearby;
mod search;
mod setup;
mod localisation;
mod locations;

type BoxedError = Box<dyn Error + Send + Sync>;

Expand Down Expand Up @@ -70,6 +70,16 @@ async fn health_status_handler(data: web::Data<AppData>) -> HttpResponse {
}
}
}
#[get("/api/get/{id}")]
async fn details_redirect(params: web::Path<String>) -> impl Responder {
let id = params.into_inner();
Redirect::to(format!("https://nav.tum.de/locations/{id}")).permanent()
}
#[get("/api/preview/{id}")]
async fn preview_redirect(params: web::Path<String>) -> impl Responder {
let id = params.into_inner();
Redirect::to(format!("https://nav.tum.de/locations/{id}/preview")).permanent()
}

fn connection_string() -> String {
let username = std::env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string());
Expand Down Expand Up @@ -191,11 +201,11 @@ async fn run() -> Result<(), BoxedError> {
.app_data(web::Data::new(data.clone()))
.service(health_status_handler)
.service(calendar::calendar_handler)
.service(web::scope("/api/preview").configure(maps::configure))
.service(web::scope("/api/feedback").configure(feedback::configure))
.service(details::get_handler)
.service(search::search_handler)
.service(nearby::nearby_handler)
.service(web::scope("/api/feedback").configure(feedback::configure))
.service(web::scope("/api/locations").configure(locations::configure))
.service(details_redirect)
.service(preview_redirect)
})
.bind(std::env::var("BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0:3003".to_string()))?
.run()
Expand Down
Loading

0 comments on commit 18f7c09

Please sign in to comment.