Compare commits
No commits in common. "dc81c21ea965e3d71cf3edb83de8a00a230425d3" and "f909cbbdd3d953020e3d77ebc1004b193910f29c" have entirely different histories.
dc81c21ea9
...
f909cbbdd3
20 changed files with 265 additions and 353 deletions
62
Cargo.lock
generated
62
Cargo.lock
generated
|
@ -984,37 +984,6 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iotishnik-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"derive_more",
|
||||
"dotenvy",
|
||||
"fred",
|
||||
"heapless",
|
||||
"hex",
|
||||
"hifitime",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"ntex",
|
||||
"phf",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serde_with",
|
||||
"smallstr",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ufmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.7"
|
||||
|
@ -1187,6 +1156,37 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
|
||||
|
||||
[[package]]
|
||||
name = "narodmon-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"derive_more",
|
||||
"dotenvy",
|
||||
"fred",
|
||||
"heapless",
|
||||
"hex",
|
||||
"hifitime",
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"ntex",
|
||||
"phf",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"serde_with",
|
||||
"smallstr",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ufmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "iotishnik-server"
|
||||
name = "narodmon-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -15,7 +15,7 @@ derive_more = "0.99.17"
|
|||
dotenvy = "0.15.7"
|
||||
fred = { version = "6.3.0", features = ["nom"] }
|
||||
heapless = { version = "0.7.16", features = ["ufmt-impl"] }
|
||||
hex = { version = "0.4.3", default-features = false, features = ["std", "alloc"] }
|
||||
hex = { version = "0.4.3", default-features = false }
|
||||
hifitime = "3.8.2"
|
||||
lazy_static = "1.4.0"
|
||||
nom = { version = "7.1.3", default-features = false, features = ["std", "alloc"] }
|
||||
|
@ -30,4 +30,4 @@ serde_with = { version = "3.6.1", features = ["hex"] }
|
|||
smallstr = { version = "0.3.0", features = ["std", "union"] }
|
||||
thiserror = "1.0.40"
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
ufmt = { version = "0.2.0", features = ["std"] }
|
||||
ufmt = "0.2.0"
|
||||
|
|
22
README.md
22
README.md
|
@ -1,22 +0,0 @@
|
|||
# IoTishnik
|
||||
Платформа для обработки данных окружающей среды с IoT устройств.
|
||||
|
||||
## Описание идеи
|
||||
> ### Что есть сейчас:
|
||||
>
|
||||
> Есть куча девайсов, которые сейчас со своих сенсоров, для публичного и в некоторых случаях частного использования выкладывают на сервер narodmon.ru данные.
|
||||
>
|
||||
> ### В чём проблема narodmon.ru:
|
||||
>
|
||||
>1. Там всё такоееее легаси, что ты не представляешь. Речь идёт о cgi скриптах написанных на bash.
|
||||
>2. Разрабы ищут помощь в виде лиц, в которых можно плевать без последствий сколько угодно.
|
||||
>3. Отсутствие тех поддержки для обычных пользователей от слову совсем.
|
||||
>4. Ужасное API которое ужасное не только из-за своей кривизны. В нём нужно отправлять телеметрию устройств, админ панель разработчика считай отсутствует. О OAuth2 вообще говорить не стоит. И если ты отправишь неправильные запросы с клиента своего, твой "api ключ" приложения заблокируют, и пофиг на то, что его можно спиздить с чужих приложений.
|
||||
>5. Интерфейс просто ужас. Можешь сам зайти и посмотреть, даже писать про это не буду. Даже сам UX в изоляции от UI ужасный.
|
||||
>6. Документация API не соблюдается. Даже когда мы с Андреем написали, что, мол, у вас поля изменились, им было плевать.
|
||||
>7. Работа с приватными датчиками ужастная и требует денег для того, чтобы оно вообще работало.
|
||||
>8. Некоторые очень важные API для устройств (такие как MQTT, на которых работают большое количество готовых продуктов не заточенные под narodmon.ru) доступны тоже только по подписке разрабам.
|
||||
>
|
||||
> ### Наше решение:
|
||||
>
|
||||
> Сделать с нуля своё решение, которое будет горизонтально масштабируемое, с поддержкой старых API для поддержки устройств сделанных под narodmon.ru . Выдать разрабам которые хотят новое и мощное API это API вместе с SDK в виде либ под ардуинку и т.п. Сделать поддержку OAuth для входа используя чужие сервисы и не только. И многое другое что я мб забыл упомянуть
|
|
@ -8,8 +8,6 @@
|
|||
- Поля
|
||||
- Вся информация о девайсе
|
||||
- `devices_{device_id}_{tai_timestamp}_{sensor_id}`
|
||||
- Только значение
|
||||
- `devices_{device_id}`
|
||||
- Поля
|
||||
- `exists`: bool
|
||||
- `unit`: str
|
||||
- `value`
|
||||
- `unit`
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
//! Глобальный модуль для вспомогательных типов и утилит.
|
||||
//!
|
||||
|
||||
|
||||
use phf::phf_map;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
/// Поддерживаемые типы.
|
||||
///
|
||||
/// TODO: Решить необходимо ли к данным прикреплять единицы измерения.
|
||||
/// TODO: Сейчас вообще сомнительно оставлять ли это или нет.
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum SupportedUnit {
|
||||
Celsius, // Needs verification > -273.15
|
||||
Celsius, // Needs verification > 273.15
|
||||
Percentage, // Needs verification >= 100 && <= 0
|
||||
MillimeterHg, // Needs verification
|
||||
UVIndex, // Needs verification
|
||||
|
@ -65,7 +56,6 @@ impl<'de> Deserialize<'de> for SupportedUnit {
|
|||
}
|
||||
}
|
||||
|
||||
/// Таблица преобразования текстового представления единиц в значения [SupportedUnit].
|
||||
static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! {
|
||||
"C" => SupportedUnit::Celsius,
|
||||
"%" => SupportedUnit::Percentage,
|
|
@ -1,5 +1,3 @@
|
|||
//! Модуль для парсинга всего, что связанно с данными от устройств.
|
||||
|
||||
pub mod error;
|
||||
mod packet_types;
|
||||
pub mod parser;
|
||||
|
|
|
@ -1,34 +1,16 @@
|
|||
//! Сборник типов для внутренней обработки данных с датчиков.
|
||||
//!
|
||||
//! Предполагается, что struct-ы будут совместимы с JSON API для передачи данных датчиков.
|
||||
|
||||
// Не забывайте про:
|
||||
// #[serde(rename = "...")]
|
||||
// #[serde(alias = "...")]
|
||||
|
||||
use crate::utils::SupportedUnit;
|
||||
use crate::hashes::SupportedUnit;
|
||||
use crate::ingest_protocol::error::Error;
|
||||
use crate::ingest_protocol::parser::parse_mac_address;
|
||||
|
||||
use hifitime::Epoch;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::formats::Separator;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Данные с одного датчика.
|
||||
///
|
||||
/// Основной идентификатор - поле `mac`. В случае с [crate::web_server::old_device_sensor_api],
|
||||
/// `mac` может быть не полем, а названием ключа в параметрах. Из-за двусмысленности документации
|
||||
/// NarodMon, `mac` может означать EUI-48 совместимый MAC адрес, или же просто
|
||||
/// уникальный идентификатор.
|
||||
///
|
||||
/// Парсинг этих данных отличается в разных транспортных протоколах.
|
||||
/// Для HTTP /post или /get: см. [crate::web_server::old_device_sensor_api]
|
||||
/// Для TCP/UDP: TODO
|
||||
/// Для MQTT: TODO
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SensorValue {
|
||||
pub mac: String,
|
||||
|
@ -44,9 +26,14 @@ impl Hash for SensorValue {
|
|||
}
|
||||
}
|
||||
|
||||
/// Функция-помощник для [MacAsArray], предназначенный для использования с [serde_with].
|
||||
///
|
||||
/// Преобразует MAC-адрес.
|
||||
pub struct DashSeparator {}
|
||||
|
||||
impl Separator for DashSeparator {
|
||||
fn separator() -> &'static str {
|
||||
"-"
|
||||
}
|
||||
}
|
||||
|
||||
fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> {
|
||||
Ok(parse_mac_address(value)?.1)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use nom::bytes::complete::tag;
|
|||
use nom::bytes::complete::take_until1;
|
||||
use nom::bytes::complete::{take, take_while, take_while1};
|
||||
use nom::character::complete::hex_digit1;
|
||||
use nom::{Parser};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::ingest_protocol::error::Error;
|
||||
|
|
60
src/main.rs
60
src/main.rs
|
@ -1,13 +1,69 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
/*
|
||||
Три датчика реалтайм:
|
||||
#26-94-1D-75-C2-F8
|
||||
#T1#6.93
|
||||
#H1#21
|
||||
#P1#700.91
|
||||
##
|
||||
|
||||
Формат пакета данных:
|
||||
#MAC[#NAME]\n
|
||||
#mac1#value1[#time1][#name1]\n
|
||||
...
|
||||
#macN#valueN[#timeN][#nameN]\n
|
||||
##
|
||||
|
||||
Загрузка истории показаний:
|
||||
#26-94-1D-75-C2-F8
|
||||
#T1#6.93#1687006667
|
||||
#T1#10.17#1687006067
|
||||
#T1#27.26#1687005467
|
||||
##
|
||||
|
||||
C названием и координатами:
|
||||
#26-94-1D-75-C2-F8#Метео
|
||||
#OWNER#nm17
|
||||
#T1#6.93#Улица
|
||||
#T2#27.26#Дом
|
||||
#P1#700.91#Барометр
|
||||
#LAT#54.308997
|
||||
#LON#48.395861
|
||||
#ALT#233
|
||||
##
|
||||
*/
|
||||
#![feature(try_blocks)]
|
||||
extern crate core;
|
||||
|
||||
mod utils;
|
||||
mod hashes;
|
||||
mod ingest_protocol;
|
||||
mod web_server;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use crate::web_server::server_main;
|
||||
|
||||
|
||||
/*fn parse_sensor_value(input: Vec<&str>) -> MyIError<Vec<&str>, NarodMonValues> {
|
||||
Ok(
|
||||
(input, NarodMonValues {
|
||||
mac: Default::default(),
|
||||
value: Default::default(),
|
||||
time: None,
|
||||
name: None,
|
||||
})
|
||||
)
|
||||
}*/
|
||||
|
||||
struct Params {}
|
||||
|
||||
#[ntex::main]
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
use derive_more::Display;
|
||||
use fred::prelude::*;
|
||||
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::http::{StatusCode};
|
||||
use ntex::web;
|
||||
|
||||
use ntex::web::{HttpRequest, HttpResponse};
|
||||
|
||||
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
use crate::insert_header;
|
||||
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
|
||||
use crate::web_server::old_devices_api::QSParserError;
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
/// Главный объект ошибки [std::error::Error] для всего Web API.
|
||||
///
|
||||
/// В целом, все Result у Web сервера должны использовать этот Error.
|
||||
#[derive(Debug, Error, Display)]
|
||||
pub enum AppError {
|
||||
#[display(fmt = "IDK")]
|
||||
|
@ -48,9 +48,6 @@ pub enum AppError {
|
|||
json_err: Option<serde_json::Error>,
|
||||
query_error: Option<serde_qs::Error>,
|
||||
},
|
||||
|
||||
#[display(fmt = "IDK")]
|
||||
DeviceNotFound(String)
|
||||
}
|
||||
|
||||
impl web::error::WebResponseError for AppError {
|
||||
|
@ -64,7 +61,6 @@ impl web::error::WebResponseError for AppError {
|
|||
AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST,
|
||||
AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST,
|
||||
AppError::QSError(..) => StatusCode::BAD_REQUEST,
|
||||
AppError::DeviceNotFound(..) => StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +76,6 @@ impl web::error::WebResponseError for AppError {
|
|||
"Can't figure out where and in what encoding the main data is"
|
||||
}
|
||||
AppError::QSError(..) => "UrlEncoded body or query params are incorrect",
|
||||
AppError::DeviceNotFound(..) => "Device not found",
|
||||
};
|
||||
|
||||
let status_code = self.status_code();
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
//! Модуль веб сервера.
|
||||
//!
|
||||
//! Все модули отвечают только за Web сторону. Такие вещи как
|
||||
//! [crate::web_server::old_device_sensor_api] отвечают только за веб версию.
|
||||
//!
|
||||
//! TODO: Начать работу над TCP/UDP и MQTT сервером
|
||||
|
||||
use old_app_api::old_api_handler;
|
||||
use crate::web_server::old_app_api::old_api_handler;
|
||||
use fred::bytes_utils::Str;
|
||||
use fred::prelude::*;
|
||||
|
||||
|
||||
|
||||
pub mod app_error;
|
||||
pub(crate) mod app_error;
|
||||
pub mod old_app_api;
|
||||
pub mod old_device_sensor_api;
|
||||
mod old_devices_api;
|
||||
pub mod utils;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -21,7 +14,7 @@ pub struct NMAppState {
|
|||
pub redis_client: RedisClient,
|
||||
}
|
||||
|
||||
use crate::web_server::old_device_sensor_api::device_handler;
|
||||
use crate::web_server::old_devices_api::device_handler;
|
||||
|
||||
use ntex::web;
|
||||
|
||||
|
|
45
src/web_server/old_app_api/config_app.rs
Normal file
45
src/web_server/old_app_api/config_app.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
|
||||
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::handlers::{app_init, version};
|
||||
use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams};
|
||||
use crate::web_server::utils::redis::is_api_key_valid;
|
||||
use crate::web_server::NMAppState;
|
||||
use nom::AsBytes;
|
||||
use ntex::util::Bytes;
|
||||
use ntex::web;
|
||||
use ntex::web::types::State;
|
||||
|
||||
|
||||
pub async fn old_api_handler(
|
||||
app_state: State<NMAppState>,
|
||||
body_bytes: Bytes,
|
||||
) -> Result<impl web::Responder, AppError> {
|
||||
if body_bytes.len() > 10 * 1024 {
|
||||
// 10 KiB
|
||||
return Err(AppError::RequestTooLarge);
|
||||
}
|
||||
|
||||
let body_bytes = body_bytes.as_bytes();
|
||||
|
||||
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON
|
||||
|
||||
// Ignore clippy singlematch
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"version" => return version((), &app_state).await,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?;
|
||||
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"appInit" => {
|
||||
let body: AppInitRequest = serde_json::from_slice(body_bytes)?;
|
||||
|
||||
app_init(body, &app_state).await
|
||||
}
|
||||
_ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())),
|
||||
}
|
||||
|
||||
//Ok("fuck")
|
||||
}
|
36
src/web_server/old_app_api/handlers/methods.rs
Normal file
36
src/web_server/old_app_api/handlers/methods.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::types::AppInitRequest;
|
||||
use crate::web_server::NMAppState;
|
||||
|
||||
use serde_json::{json};
|
||||
|
||||
|
||||
use crate::insert_header;
|
||||
use fred::interfaces::KeysInterface;
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web;
|
||||
|
||||
|
||||
|
||||
pub async fn app_init(
|
||||
_body: AppInitRequest<'_>,
|
||||
app_state: &NMAppState,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let _: () = app_state
|
||||
.redis_client
|
||||
.set("test", 123, None, None, true)
|
||||
.await?;
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!"))
|
||||
}
|
||||
|
||||
pub async fn version(_body: (), _app_state: &NMAppState) -> Result<web::HttpResponse, AppError> {
|
||||
let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({
|
||||
"version": "indev",
|
||||
"iotishnik": true
|
||||
}));
|
||||
|
||||
insert_header!(resp.headers_mut(), "Cache-Control", "no-cache");
|
||||
|
||||
Ok(resp)
|
||||
}
|
|
@ -1,36 +1,7 @@
|
|||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::old_app_api::types::AppInitRequest;
|
||||
use crate::web_server::NMAppState;
|
||||
|
||||
use serde_json::{json};
|
||||
mod methods;
|
||||
|
||||
|
||||
use crate::insert_header;
|
||||
use fred::interfaces::KeysInterface;
|
||||
use ntex::http::StatusCode;
|
||||
use ntex::web;
|
||||
pub use methods::*;
|
||||
|
||||
|
||||
|
||||
pub async fn app_init(
|
||||
_body: AppInitRequest<'_>,
|
||||
app_state: &NMAppState,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let _: () = app_state
|
||||
.redis_client
|
||||
.set("test", 123, None, None, true)
|
||||
.await?;
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).body("Hello world!"))
|
||||
}
|
||||
|
||||
pub async fn version(_body: (), _app_state: &NMAppState) -> Result<web::HttpResponse, AppError> {
|
||||
let mut resp = web::HttpResponse::build(StatusCode::OK).json(&json!({
|
||||
"version": "indev",
|
||||
"iotishnik": true
|
||||
}));
|
||||
|
||||
insert_header!(resp.headers_mut(), "Cache-Control", "no-cache");
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
|
|
@ -1,53 +1,5 @@
|
|||
//! Модуль обработки данных для приложений конечных пользователей, которые используют старый API.
|
||||
|
||||
mod config_app;
|
||||
mod handlers;
|
||||
mod types;
|
||||
|
||||
use ntex::web::types::State;
|
||||
use ntex::util::Bytes;
|
||||
use ntex::web;
|
||||
use nom::AsBytes;
|
||||
use crate::web_server::app_error::AppError;
|
||||
use crate::web_server::NMAppState;
|
||||
use crate::web_server::old_app_api::handlers::{app_init, version};
|
||||
use crate::web_server::old_app_api::types::{AppInitRequest, MandatoryParams};
|
||||
use crate::web_server::utils::redis::is_api_key_valid;
|
||||
|
||||
|
||||
/// Обработчик запросов от приложений.
|
||||
///
|
||||
/// Отвечает за разделение на функции по `cmd`.
|
||||
///
|
||||
/// Вызывается напрямую из ntex приложения.
|
||||
pub async fn old_api_handler(
|
||||
app_state: State<NMAppState>,
|
||||
body_bytes: Bytes,
|
||||
) -> Result<impl web::Responder, AppError> {
|
||||
if body_bytes.len() > 10 * 1024 {
|
||||
// 10 KiB
|
||||
return Err(AppError::RequestTooLarge);
|
||||
}
|
||||
|
||||
let body_bytes = body_bytes.as_bytes();
|
||||
|
||||
let mandatory_params: MandatoryParams<'_> = serde_json::from_slice(body_bytes)?; // TODO: Simd-JSON
|
||||
|
||||
// Ignore clippy singlematch
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"version" => return version((), &app_state).await,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
is_api_key_valid(&app_state.redis_client, mandatory_params.api_key.as_ref()).await?;
|
||||
|
||||
match mandatory_params.cmd.as_ref() {
|
||||
"appInit" => {
|
||||
let body: AppInitRequest = serde_json::from_slice(body_bytes)?;
|
||||
|
||||
app_init(body, &app_state).await
|
||||
}
|
||||
_ => Err(AppError::UnknownMethod(mandatory_params.cmd.to_string())),
|
||||
}
|
||||
|
||||
//Ok("fuck")
|
||||
}
|
||||
pub use config_app::old_api_handler;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
@ -23,11 +24,6 @@ pub struct AddLikeRequest {
|
|||
pub version: u64,
|
||||
}
|
||||
|
||||
/// Обязательные параметры у JSON app API.
|
||||
///
|
||||
/// При обработке входящих данных производиться два запроса [serde_json::from_str]. Один вызов пытается
|
||||
/// получить [MandatoryParams], другой в зависимости от `cmd`. Это позволяет не добвалять поля из
|
||||
/// этой структуры в каждом специфичном типе.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MandatoryParams<'a> {
|
||||
#[serde(borrow)]
|
||||
|
@ -36,11 +32,6 @@ pub struct MandatoryParams<'a> {
|
|||
#[serde(borrow)]
|
||||
pub lang: Cow<'a, str>,
|
||||
|
||||
/// Уникальный ID клиента.
|
||||
///
|
||||
/// Используется на подобии как куки PHPSESSID в php.
|
||||
///
|
||||
/// См. также: <https://www.php.net/manual/en/book.session.php>
|
||||
#[serde(borrow)]
|
||||
pub uuid: Cow<'a, str>,
|
||||
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
//! Модуль обработки данных с устройств, которые используют старый API.
|
||||
|
||||
pub mod qs_parser;
|
||||
|
||||
use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket};
|
||||
use crate::web_server::app_error::AppError;
|
||||
|
||||
|
||||
use fred::types::RedisMap;
|
||||
use ntex::http::{HttpMessage, StatusCode};
|
||||
use ntex::util::{Bytes, HashMap};
|
||||
use ntex::{http, web};
|
||||
use fred::prelude::*;
|
||||
use hifitime::Epoch;
|
||||
use ntex::web::types::State;
|
||||
use thiserror::Error;
|
||||
use ufmt::uwrite;
|
||||
use crate::web_server::NMAppState;
|
||||
use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Device not found")]
|
||||
DeviceNotFound(String),
|
||||
#[error("Time sent with the device is way too behind now")]
|
||||
TimeIsLongBehindNow,
|
||||
#[error("{0}")]
|
||||
QSParserError(#[from] QSParserError),
|
||||
}
|
||||
|
||||
|
||||
/// Обработчик данных датчиков с устройств.
|
||||
///
|
||||
/// Слушает /post и /get.
|
||||
/// Для того чтобы пользователям было легче, на оба пути можно отправлять и POST и GET.
|
||||
///
|
||||
/// На POST можно отправлять JSON или url-encoded тело, на GET - только через Query String.
|
||||
pub async fn device_handler<'a>(
|
||||
request: web::HttpRequest,
|
||||
body: Bytes,
|
||||
app_state: State<NMAppState>,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let mut real_body: Option<NMJsonPacket> = None;
|
||||
let mut json_error = None;
|
||||
let mut query_error = None;
|
||||
|
||||
dbg!(&request.content_type());
|
||||
dbg!(&request.query_string());
|
||||
|
||||
if request.method() == http::Method::POST {
|
||||
match request.content_type() {
|
||||
"application/json" => match serde_json::from_slice::<NMJsonPacket>(body.as_ref()) {
|
||||
Ok(json_body) => real_body = Some(json_body),
|
||||
Err(error) => json_error = Some(error),
|
||||
},
|
||||
"application/x-www-form-urlencoded" => {
|
||||
match serde_qs::from_bytes::<NMDeviceDataPacket>(body.as_ref()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if request.method() == http::Method::GET {
|
||||
match serde_qs::from_str::<NMDeviceDataPacket>(request.query_string()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(real_body) = real_body {
|
||||
for device in real_body.devices {
|
||||
let mut device_key_str = String::new();
|
||||
|
||||
let now = Epoch::now().unwrap();
|
||||
let mut device_time = device.time
|
||||
.unwrap_or(now);
|
||||
|
||||
// TODO: Добавить гистерезис
|
||||
// Отчёт совместимости: отсутствует
|
||||
if device_time > now {
|
||||
device_time = now;
|
||||
}
|
||||
|
||||
let device_tai_timestamp = device_time
|
||||
.to_duration_since_j1900()
|
||||
.to_seconds();
|
||||
|
||||
uwrite!(device_key_str, "devices_{}", hex::encode(device.mac));
|
||||
|
||||
let device_exists: bool = app_state.redis_client.hget(device_key_str.as_str(), "exists").await?;
|
||||
|
||||
if !device_exists {
|
||||
return Err(AppError::DeviceNotFound(hex::encode(&device.mac)));
|
||||
}
|
||||
|
||||
// devices_{device_id}_{tai_timestamp}_{sensor_id}
|
||||
for sensor in device.values {
|
||||
let mut device_report_key_str = String::new();
|
||||
uwrite!(&mut device_report_key_str, "devices_{}_{}_{}", hex::encode(device.mac), device_tai_timestamp.to_string(), sensor.mac);
|
||||
|
||||
app_state.redis_client.set(
|
||||
device_key_str.as_str(), sensor.value.to_string(), None, None, false,
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(AppError::UnknownBody {
|
||||
json_err: json_error,
|
||||
query_error,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(web::HttpResponseBuilder::new(StatusCode::OK)
|
||||
.finish())
|
||||
}
|
|
@ -1,14 +1,21 @@
|
|||
use std::collections::HashSet;
|
||||
use std::num::ParseFloatError;
|
||||
use std::str::FromStr;
|
||||
use crate::ingest_protocol::error::Error;
|
||||
use crate::ingest_protocol::parser::parse_mac_address;
|
||||
use crate::ingest_protocol::{NMDeviceDataPacket, NMJsonPacket, SensorValue};
|
||||
use crate::web_server::app_error::AppError;
|
||||
|
||||
|
||||
use ntex::http::{HttpMessage, StatusCode};
|
||||
use ntex::util::{Bytes, HashMap};
|
||||
use ntex::{http, web};
|
||||
use std::sync::Arc;
|
||||
use fred::bytes_utils::Str;
|
||||
use hifitime::efmt::consts::ISO8601;
|
||||
use hifitime::Epoch;
|
||||
use ntex::util::HashMap;
|
||||
use rust_decimal::Decimal;
|
||||
use thiserror::Error;
|
||||
use crate::ingest_protocol::error::Error;
|
||||
use crate::ingest_protocol::{NMDeviceDataPacket, SensorValue};
|
||||
use crate::ingest_protocol::parser::parse_mac_address;
|
||||
|
||||
/// В иделае было бы хорошо сделать всё как у [serde_json::Error], но это слишком большая морока
|
||||
#[derive(Error, Clone, Debug)]
|
||||
|
@ -40,11 +47,6 @@ impl From<serde_qs::Error> for QSParserError {
|
|||
}
|
||||
}
|
||||
|
||||
/// Преобразование оставшихся параметров в urlencoded теле или query string в данные с датчиков
|
||||
/// [SensorValue].
|
||||
///
|
||||
/// Формат: `<SENSOR_MAC>=<SENSOR_VALUE>`.
|
||||
/// Других данных на подобии названия и времени нет.
|
||||
pub fn qs_rest_to_values(parsed: HashMap<String, String>) -> Result<HashSet<SensorValue>, QSParserError> {
|
||||
let mut hashset = HashSet::new();
|
||||
|
||||
|
@ -116,4 +118,56 @@ pub async fn parse_nm_qs_format(input: &str) -> Result<NMDeviceDataPacket, QSPar
|
|||
};
|
||||
|
||||
Ok(device_data)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn device_handler<'a>(
|
||||
request: web::HttpRequest,
|
||||
body: Bytes,
|
||||
) -> Result<web::HttpResponse, AppError> {
|
||||
let mut real_body: Option<NMJsonPacket> = None;
|
||||
let mut json_error = None;
|
||||
let mut query_error = None;
|
||||
|
||||
dbg!(&request.content_type());
|
||||
dbg!(&request.query_string());
|
||||
|
||||
if request.method() == http::Method::POST {
|
||||
match request.content_type() {
|
||||
"application/json" => match serde_json::from_slice::<NMJsonPacket>(body.as_ref()) {
|
||||
Ok(json_body) => real_body = Some(json_body),
|
||||
Err(error) => json_error = Some(error),
|
||||
},
|
||||
"application/x-www-form-urlencoded" => {
|
||||
match serde_qs::from_bytes::<NMDeviceDataPacket>(body.as_ref()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if request.method() == http::Method::GET {
|
||||
match serde_qs::from_str::<NMDeviceDataPacket>(request.query_string()) {
|
||||
Ok(qs_body) => {
|
||||
real_body = Some(NMJsonPacket {
|
||||
devices: Vec::from([qs_body]),
|
||||
})
|
||||
}
|
||||
Err(error) => query_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(_body) = real_body {
|
||||
// TODO
|
||||
} else {
|
||||
return Err(AppError::UnknownBody {
|
||||
json_err: json_error,
|
||||
query_error,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(web::HttpResponse::build(StatusCode::OK).finish())
|
||||
}
|
|
@ -1,6 +1,3 @@
|
|||
//! Сборник полезных функций которые используются в многих местах одновременно или слишком
|
||||
//! неспециализированные.
|
||||
|
||||
pub mod redis;
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
//! Сборник утилит для работы с Redis.
|
||||
|
||||
use crate::web_server::app_error::AppError;
|
||||
use fred::prelude::*;
|
||||
use heapless::String as HeaplessString;
|
||||
|
@ -8,17 +6,13 @@ use regex::Regex;
|
|||
use ufmt::uwrite;
|
||||
|
||||
lazy_static! {
|
||||
/// Разрешённые знаки для API ключа.
|
||||
static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap();
|
||||
}
|
||||
|
||||
/// Описание полей в KV DB у `apikey_{}`.
|
||||
pub struct ApiKeyDescription {
|
||||
/// ID владельца API ключа.
|
||||
apikey_owner: i64,
|
||||
}
|
||||
|
||||
/// Проверка API ключа на валидность.
|
||||
pub async fn is_api_key_valid(
|
||||
client: &RedisClient,
|
||||
api_key: &str,
|
||||
|
|
Loading…
Add table
Reference in a new issue