mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 17:57:47 +03:00
feat!: add telegram auth and async refactor
- Removed "/schedule/update-download-url" endpoint, this mechanism was replaced by Yandex Cloud FaaS. Ура :) - Improved schedule caching mechanism. - Added Telegram WebApp authentication support. - Reworked endpoints responses and errors mechanism. - Refactored application state management. - Make synchronous database operations, middlewares and extractors to asynchronous. - Made user password field optional to support multiple auth methods. - Renamed users table column "version" to "android_version" and made it nullable.
This commit is contained in:
@@ -7,17 +7,5 @@ use actix_web::{get, web};
|
||||
))]
|
||||
#[get("/cache-status")]
|
||||
pub async fn cache_status(app_state: web::Data<AppState>) -> CacheStatus {
|
||||
// Prevent thread lock
|
||||
let has_schedule = app_state
|
||||
.schedule
|
||||
.lock()
|
||||
.as_ref()
|
||||
.map(|res| res.is_some())
|
||||
.unwrap();
|
||||
|
||||
match has_schedule {
|
||||
true => CacheStatus::from(&app_state),
|
||||
false => CacheStatus::default(),
|
||||
}
|
||||
.into()
|
||||
CacheStatus::from(&app_state).await.into()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::database::models::User;
|
||||
use crate::extractors::base::SyncExtractor;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::extractors::base::AsyncExtractor;
|
||||
use crate::routes::schedule::schema::ScheduleEntryResponse;
|
||||
use crate::routes::schema::ResponseError;
|
||||
use actix_web::{get, web};
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = Response),
|
||||
(status = OK, body = ScheduleEntryResponse),
|
||||
(
|
||||
status = SERVICE_UNAVAILABLE,
|
||||
body = ResponseError<ErrorCode>,
|
||||
@@ -25,68 +26,42 @@ use actix_web::{get, web};
|
||||
),
|
||||
))]
|
||||
#[get("/group")]
|
||||
pub async fn group(user: SyncExtractor<User>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn group(user: AsyncExtractor<User>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
match &user.into_inner().group {
|
||||
None => Err(ErrorCode::SignUpNotCompleted),
|
||||
|
||||
match schedule_lock.as_ref() {
|
||||
None => ErrorCode::NoSchedule.into_response(),
|
||||
Some(schedule) => match schedule.data.groups.get(&user.into_inner().group) {
|
||||
None => ErrorCode::NotFound.into_response(),
|
||||
Some(entry) => Ok(entry.clone().into()).into(),
|
||||
Some(group) => match app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.groups
|
||||
.get(group)
|
||||
{
|
||||
None => Err(ErrorCode::NotFound),
|
||||
|
||||
Some(entry) => Ok(entry.clone().into()),
|
||||
},
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use crate::routes::schedule::schema::ScheduleEntryResponse;
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
pub type ServiceResponse = crate::routes::schema::Response<ScheduleEntryResponse, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[schema(as = GetGroup::Response)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Response {
|
||||
/// Group schedule.
|
||||
pub group: ScheduleEntry,
|
||||
|
||||
/// ## Outdated variable.
|
||||
///
|
||||
/// By default, an empty list is returned.
|
||||
#[deprecated = "Will be removed in future versions"]
|
||||
pub updated: Vec<i32>,
|
||||
|
||||
/// ## Outdated variable.
|
||||
///
|
||||
/// By default, the initial date for unix.
|
||||
#[deprecated = "Will be removed in future versions"]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl From<ScheduleEntry> for Response {
|
||||
fn from(group: ScheduleEntry) -> Self {
|
||||
Self {
|
||||
group,
|
||||
updated: Vec::new(),
|
||||
updated_at: NaiveDateTime::default().and_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = GroupSchedule::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
/// Schedules have not yet been parsed.
|
||||
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||
#[display("Schedule not parsed yet.")]
|
||||
NoSchedule,
|
||||
/// The user tried to access the API without completing singing up.
|
||||
#[status_code = "actix_web::http::StatusCode::FORBIDDEN"]
|
||||
#[display("You have not completed signing up.")]
|
||||
SignUpNotCompleted,
|
||||
|
||||
/// Group not found.
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
|
||||
|
||||
@@ -1,48 +1,34 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::routes::schedule::schema::ErrorCode;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use actix_web::{get, web};
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = Response),
|
||||
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>),
|
||||
))]
|
||||
#[utoipa::path(responses((status = OK, body = Response)))]
|
||||
#[get("/group-names")]
|
||||
pub async fn group_names(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn group_names(app_state: web::Data<AppState>) -> Response {
|
||||
let mut names: Vec<String> = app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.groups
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
match schedule_lock.as_ref() {
|
||||
None => ErrorCode::NoSchedule.into_response(),
|
||||
Some(schedule) => {
|
||||
let mut names: Vec<String> = schedule.data.groups.keys().cloned().collect();
|
||||
names.sort();
|
||||
|
||||
Ok(names.into()).into()
|
||||
}
|
||||
}
|
||||
.into()
|
||||
names.sort();
|
||||
|
||||
Response { names }
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schedule::schema::ErrorCode;
|
||||
use actix_macros::ResponderJson;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, ToSchema, ResponderJson)]
|
||||
#[schema(as = GetGroupNames::Response)]
|
||||
pub struct Response {
|
||||
/// List of group names sorted in alphabetical order.
|
||||
#[schema(examples(json!(["ИС-214/23"])))]
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for Response {
|
||||
fn from(names: Vec<String>) -> Self {
|
||||
Self { names }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ mod schedule;
|
||||
mod schema;
|
||||
mod teacher;
|
||||
mod teacher_names;
|
||||
mod update_download_url;
|
||||
|
||||
pub use cache_status::*;
|
||||
pub use group::*;
|
||||
@@ -13,4 +12,3 @@ pub use group_names::*;
|
||||
pub use schedule::*;
|
||||
pub use teacher::*;
|
||||
pub use teacher_names::*;
|
||||
pub use update_download_url::*;
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
use self::schema::*;
|
||||
use crate::app_state::AppState;
|
||||
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::routes::schedule::schema::ScheduleView;
|
||||
use crate::state::AppState;
|
||||
use actix_web::{get, web};
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = ScheduleView),
|
||||
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>)
|
||||
))]
|
||||
#[utoipa::path(responses((status = OK, body = ScheduleView)))]
|
||||
#[get("/")]
|
||||
pub async fn schedule(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
match ScheduleView::try_from(&app_state) {
|
||||
Ok(res) => Ok(res).into(),
|
||||
Err(e) => match e {
|
||||
ErrorCode::NoSchedule => ErrorCode::NoSchedule.into_response(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schedule::schema::{ErrorCode, ScheduleView};
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<ScheduleView, ErrorCode>;
|
||||
pub async fn schedule(app_state: web::Data<AppState>) -> ScheduleView {
|
||||
ScheduleView::from(&app_state).await
|
||||
}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
use crate::app_state::{AppState, Schedule};
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use actix_macros::{IntoResponseErrorNamed, ResponderJson, StatusCode};
|
||||
use crate::state::{AppState, ScheduleSnapshot};
|
||||
use actix_macros::{OkResponse, ResponderJson};
|
||||
use actix_web::web;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use derive_more::Display;
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Response from schedule server.
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, ToSchema, OkResponse, ResponderJson)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScheduleView {
|
||||
/// ETag schedules on polytechnic server.
|
||||
etag: String,
|
||||
|
||||
/// Schedule update date on polytechnic website.
|
||||
uploaded_at: DateTime<Utc>,
|
||||
|
||||
/// Date last downloaded from the Polytechnic server.
|
||||
downloaded_at: DateTime<Utc>,
|
||||
/// Url to xls file.
|
||||
url: String,
|
||||
|
||||
/// Groups schedule.
|
||||
groups: HashMap<String, ScheduleEntry>,
|
||||
@@ -28,80 +21,55 @@ pub struct ScheduleView {
|
||||
teachers: HashMap<String, ScheduleEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = ScheduleShared::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
/// Schedules not yet parsed.
|
||||
#[display("Schedule not parsed yet.")]
|
||||
NoSchedule,
|
||||
#[derive(Serialize, ToSchema, OkResponse)]
|
||||
pub struct ScheduleEntryResponse(ScheduleEntry);
|
||||
|
||||
impl From<ScheduleEntry> for ScheduleEntryResponse {
|
||||
fn from(value: ScheduleEntry) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&web::Data<AppState>> for ScheduleView {
|
||||
type Error = ErrorCode;
|
||||
impl ScheduleView {
|
||||
pub async fn from(app_state: &web::Data<AppState>) -> Self {
|
||||
let schedule = app_state.get_schedule_snapshot().await.clone();
|
||||
|
||||
fn try_from(app_state: &web::Data<AppState>) -> Result<Self, Self::Error> {
|
||||
if let Some(schedule) = app_state.schedule.lock().unwrap().clone() {
|
||||
Ok(Self {
|
||||
etag: schedule.etag,
|
||||
uploaded_at: schedule.updated_at,
|
||||
downloaded_at: schedule.parsed_at,
|
||||
groups: schedule.data.groups,
|
||||
teachers: schedule.data.teachers,
|
||||
})
|
||||
} else {
|
||||
Err(ErrorCode::NoSchedule)
|
||||
Self {
|
||||
url: schedule.url,
|
||||
groups: schedule.data.groups,
|
||||
teachers: schedule.data.teachers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached schedule status.
|
||||
#[derive(Serialize, Deserialize, ToSchema, ResponderJson)]
|
||||
#[derive(Serialize, Deserialize, ToSchema, ResponderJson, OkResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CacheStatus {
|
||||
/// Schedule hash.
|
||||
pub cache_hash: String,
|
||||
|
||||
/// Whether the schedule reference needs to be updated.
|
||||
pub cache_update_required: bool,
|
||||
pub hash: String,
|
||||
|
||||
/// Last cache update date.
|
||||
pub last_cache_update: i64,
|
||||
pub fetched_at: i64,
|
||||
|
||||
/// Cached schedule update date.
|
||||
///
|
||||
/// Determined by the polytechnic's server.
|
||||
pub last_schedule_update: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
impl CacheStatus {
|
||||
pub fn default() -> Self {
|
||||
CacheStatus {
|
||||
cache_hash: "0000000000000000000000000000000000000000".to_string(),
|
||||
cache_update_required: true,
|
||||
last_cache_update: 0,
|
||||
last_schedule_update: 0,
|
||||
}
|
||||
pub async fn from(value: &web::Data<AppState>) -> Self {
|
||||
From::<&ScheduleSnapshot>::from(value.get_schedule_snapshot().await.deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&web::Data<AppState>> for CacheStatus {
|
||||
fn from(value: &web::Data<AppState>) -> Self {
|
||||
let schedule_lock = value.schedule.lock().unwrap();
|
||||
let schedule = schedule_lock.as_ref().unwrap();
|
||||
|
||||
CacheStatus::from(schedule)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Schedule> for CacheStatus {
|
||||
fn from(value: &Schedule) -> Self {
|
||||
impl From<&ScheduleSnapshot> for CacheStatus {
|
||||
fn from(value: &ScheduleSnapshot) -> Self {
|
||||
Self {
|
||||
cache_hash: value.hash(),
|
||||
cache_update_required: (Utc::now() - value.fetched_at) > Duration::minutes(5),
|
||||
last_cache_update: value.fetched_at.timestamp(),
|
||||
last_schedule_update: value.updated_at.timestamp(),
|
||||
hash: value.hash(),
|
||||
fetched_at: value.fetched_at.timestamp(),
|
||||
updated_at: value.updated_at.timestamp(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
use self::schema::*;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::AppState;
|
||||
use crate::routes::schema::ResponseError;
|
||||
use actix_web::{get, web};
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = Response),
|
||||
(
|
||||
status = SERVICE_UNAVAILABLE,
|
||||
body = ResponseError<ErrorCode>,
|
||||
example = json!({
|
||||
"code": "NO_SCHEDULE",
|
||||
"message": "Schedule not parsed yet."
|
||||
})
|
||||
),
|
||||
(status = OK, body = ScheduleEntry),
|
||||
(
|
||||
status = NOT_FOUND,
|
||||
body = ResponseError<ErrorCode>,
|
||||
@@ -23,72 +16,34 @@ use actix_web::{get, web};
|
||||
),
|
||||
))]
|
||||
#[get("/teacher/{name}")]
|
||||
pub async fn teacher(
|
||||
name: web::Path<String>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn teacher(name: web::Path<String>, app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
match app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.teachers
|
||||
.get(&name.into_inner())
|
||||
{
|
||||
None => Err(ErrorCode::NotFound),
|
||||
|
||||
match schedule_lock.as_ref() {
|
||||
None => ErrorCode::NoSchedule.into_response(),
|
||||
Some(schedule) => match schedule.data.teachers.get(&name.into_inner()) {
|
||||
None => ErrorCode::NotFound.into_response(),
|
||||
Some(entry) => Ok(entry.clone().into()).into(),
|
||||
},
|
||||
Some(entry) => Ok(entry.clone().into()),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use schedule_parser::schema::ScheduleEntry;
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use crate::routes::schedule::schema::ScheduleEntryResponse;
|
||||
use actix_macros::ErrResponse;
|
||||
use derive_more::Display;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
pub type ServiceResponse = crate::routes::schema::Response<ScheduleEntryResponse, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[schema(as = GetTeacher::Response)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Response {
|
||||
/// Teacher's schedule.
|
||||
pub teacher: ScheduleEntry,
|
||||
|
||||
/// ## Deprecated variable.
|
||||
///
|
||||
/// By default, an empty list is returned.
|
||||
#[deprecated = "Will be removed in future versions"]
|
||||
pub updated: Vec<i32>,
|
||||
|
||||
/// ## Deprecated variable.
|
||||
///
|
||||
/// Defaults to the Unix start date.
|
||||
#[deprecated = "Will be removed in future versions"]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl From<ScheduleEntry> for Response {
|
||||
fn from(teacher: ScheduleEntry) -> Self {
|
||||
Self {
|
||||
teacher,
|
||||
updated: Vec::new(),
|
||||
updated_at: NaiveDateTime::default().and_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||
#[derive(Clone, Serialize, Display, ToSchema, ErrResponse)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
#[schema(as = TeacherSchedule::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
/// Schedules have not yet been parsed.
|
||||
#[status_code = "actix_web::http::StatusCode::SERVICE_UNAVAILABLE"]
|
||||
#[display("Schedule not parsed yet.")]
|
||||
NoSchedule,
|
||||
|
||||
/// Teacher not found.
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_FOUND"]
|
||||
#[display("Required teacher not found.")]
|
||||
|
||||
@@ -1,48 +1,34 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::routes::schedule::schema::ErrorCode;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use actix_web::{get, web};
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = Response),
|
||||
(status = SERVICE_UNAVAILABLE, body = ResponseError<ErrorCode>),
|
||||
))]
|
||||
#[utoipa::path(responses((status = OK, body = Response)))]
|
||||
#[get("/teacher-names")]
|
||||
pub async fn teacher_names(app_state: web::Data<AppState>) -> ServiceResponse {
|
||||
// Prevent thread lock
|
||||
let schedule_lock = app_state.schedule.lock().unwrap();
|
||||
pub async fn teacher_names(app_state: web::Data<AppState>) -> Response {
|
||||
let mut names: Vec<String> = app_state
|
||||
.get_schedule_snapshot()
|
||||
.await
|
||||
.data
|
||||
.teachers
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
match schedule_lock.as_ref() {
|
||||
None => ErrorCode::NoSchedule.into_response(),
|
||||
Some(schedule) => {
|
||||
let mut names: Vec<String> = schedule.data.teachers.keys().cloned().collect();
|
||||
names.sort();
|
||||
names.sort();
|
||||
|
||||
Ok(names.into()).into()
|
||||
}
|
||||
}
|
||||
.into()
|
||||
Response { names }
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::routes::schedule::schema::ErrorCode;
|
||||
use actix_macros::ResponderJson;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<Response, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[derive(Serialize, ToSchema, ResponderJson)]
|
||||
#[schema(as = GetTeacherNames::Response)]
|
||||
pub struct Response {
|
||||
/// List of teacher names sorted alphabetically.
|
||||
#[schema(examples(json!(["Хомченко Н.Е."])))]
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for Response {
|
||||
fn from(names: Vec<String>) -> Self {
|
||||
Self { names }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::app_state::Schedule;
|
||||
use schedule_parser::parse_xls;
|
||||
use crate::routes::schedule::schema::CacheStatus;
|
||||
use crate::routes::schema::{IntoResponseAsError, ResponseError};
|
||||
use crate::xls_downloader::interface::{FetchError, XLSDownloader};
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{patch, web};
|
||||
use chrono::Utc;
|
||||
|
||||
#[utoipa::path(responses(
|
||||
(status = OK, body = CacheStatus),
|
||||
(status = NOT_ACCEPTABLE, body = ResponseError<ErrorCode>),
|
||||
))]
|
||||
#[patch("/update-download-url")]
|
||||
pub async fn update_download_url(
|
||||
data: Json<Request>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> ServiceResponse {
|
||||
if !data.url.starts_with("https://politehnikum-eng.ru/") {
|
||||
return ErrorCode::NonWhitelistedHost.into_response();
|
||||
}
|
||||
|
||||
let mut downloader = app_state.downloader.lock().unwrap();
|
||||
|
||||
if let Some(url) = &downloader.url {
|
||||
if url.eq(&data.url) {
|
||||
return Ok(CacheStatus::from(&app_state)).into();
|
||||
}
|
||||
}
|
||||
|
||||
match downloader.set_url(data.url.clone()).await {
|
||||
Ok(fetch_result) => {
|
||||
let mut schedule = app_state.schedule.lock().unwrap();
|
||||
|
||||
if schedule.is_some()
|
||||
&& fetch_result.uploaded_at < schedule.as_ref().unwrap().updated_at
|
||||
{
|
||||
return ErrorCode::OutdatedSchedule.into_response();
|
||||
}
|
||||
|
||||
match downloader.fetch(false).await {
|
||||
Ok(download_result) => match parse_xls(&download_result.data.unwrap()) {
|
||||
Ok(data) => {
|
||||
*schedule = Some(Schedule {
|
||||
etag: download_result.etag,
|
||||
fetched_at: download_result.requested_at,
|
||||
updated_at: download_result.uploaded_at,
|
||||
parsed_at: Utc::now(),
|
||||
data,
|
||||
});
|
||||
|
||||
Ok(CacheStatus::from(schedule.as_ref().unwrap())).into()
|
||||
}
|
||||
Err(error) => {
|
||||
sentry::capture_error(&error);
|
||||
|
||||
ErrorCode::InvalidSchedule(error).into_response()
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
if let FetchError::Unknown(error) = &error {
|
||||
sentry::capture_error(&error);
|
||||
}
|
||||
|
||||
ErrorCode::DownloadFailed(error).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if let FetchError::Unknown(error) = &error {
|
||||
sentry::capture_error(&error);
|
||||
}
|
||||
|
||||
ErrorCode::FetchFailed(error).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use schedule_parser::schema::ParseError;
|
||||
use crate::routes::schedule::schema::CacheStatus;
|
||||
use actix_macros::{IntoResponseErrorNamed, StatusCode};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use utoipa::ToSchema;
|
||||
use crate::xls_downloader::interface::FetchError;
|
||||
|
||||
pub type ServiceResponse = crate::routes::schema::Response<CacheStatus, ErrorCode>;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Request {
|
||||
/// Schedule link.
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, ToSchema, StatusCode, Display, IntoResponseErrorNamed)]
|
||||
#[status_code = "actix_web::http::StatusCode::NOT_ACCEPTABLE"]
|
||||
#[schema(as = SetDownloadUrl::ErrorCode)]
|
||||
pub enum ErrorCode {
|
||||
/// Transferred link with host different from politehnikum-eng.ru.
|
||||
#[display("URL with unknown host provided. Provide url with 'politehnikum-eng.ru' host.")]
|
||||
NonWhitelistedHost,
|
||||
|
||||
/// Failed to retrieve file metadata.
|
||||
#[display("Unable to retrieve metadata from the specified URL: {_0}")]
|
||||
FetchFailed(FetchError),
|
||||
|
||||
/// Failed to download the file.
|
||||
#[display("Unable to retrieve data from the specified URL: {_0}")]
|
||||
DownloadFailed(FetchError),
|
||||
|
||||
/// The link leads to an outdated schedule.
|
||||
///
|
||||
/// An outdated schedule refers to a schedule that was published earlier
|
||||
/// than is currently available.
|
||||
#[display("The schedule is older than it already is.")]
|
||||
OutdatedSchedule,
|
||||
|
||||
/// Failed to parse the schedule.
|
||||
#[display("{_0}")]
|
||||
InvalidSchedule(ParseError),
|
||||
}
|
||||
|
||||
impl Serialize for ErrorCode {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
ErrorCode::NonWhitelistedHost => serializer.serialize_str("NON_WHITELISTED_HOST"),
|
||||
ErrorCode::FetchFailed(_) => serializer.serialize_str("FETCH_FAILED"),
|
||||
ErrorCode::DownloadFailed(_) => serializer.serialize_str("DOWNLOAD_FAILED"),
|
||||
ErrorCode::OutdatedSchedule => serializer.serialize_str("OUTDATED_SCHEDULE"),
|
||||
ErrorCode::InvalidSchedule(_) => serializer.serialize_str("INVALID_SCHEDULE"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user