dev/nm17 #2

Merged
nm17 merged 3 commits from dev/nm17 into master 2024-05-30 13:33:18 +03:00
20 changed files with 353 additions and 265 deletions

62
Cargo.lock generated
View file

@ -984,6 +984,37 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.7" version = "0.4.7"
@ -1156,37 +1187,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 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]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"

View file

@ -1,5 +1,5 @@
[package] [package]
name = "narodmon-server" name = "iotishnik-server"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
@ -15,7 +15,7 @@ derive_more = "0.99.17"
dotenvy = "0.15.7" dotenvy = "0.15.7"
fred = { version = "6.3.0", features = ["nom"] } fred = { version = "6.3.0", features = ["nom"] }
heapless = { version = "0.7.16", features = ["ufmt-impl"] } heapless = { version = "0.7.16", features = ["ufmt-impl"] }
hex = { version = "0.4.3", default-features = false } hex = { version = "0.4.3", default-features = false, features = ["std", "alloc"] }
hifitime = "3.8.2" hifitime = "3.8.2"
lazy_static = "1.4.0" lazy_static = "1.4.0"
nom = { version = "7.1.3", default-features = false, features = ["std", "alloc"] } 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"] } smallstr = { version = "0.3.0", features = ["std", "union"] }
thiserror = "1.0.40" thiserror = "1.0.40"
tokio = { version = "1.28.2", features = ["full"] } tokio = { version = "1.28.2", features = ["full"] }
ufmt = "0.2.0" ufmt = { version = "0.2.0", features = ["std"] }

22
README.md Normal file
View file

@ -0,0 +1,22 @@
# 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 для входа используя чужие сервисы и не только. И многое другое что я мб забыл упомянуть

View file

@ -8,6 +8,8 @@
- Поля - Поля
- Вся информация о девайсе - Вся информация о девайсе
- `devices_{device_id}_{tai_timestamp}_{sensor_id}` - `devices_{device_id}_{tai_timestamp}_{sensor_id}`
- Только значение
- `devices_{device_id}`
- Поля - Поля
- `value` - `exists`: bool
- `unit` - `unit`: str

View file

@ -1,3 +1,5 @@
//! Модуль для парсинга всего, что связанно с данными от устройств.
pub mod error; pub mod error;
mod packet_types; mod packet_types;
pub mod parser; pub mod parser;

View file

@ -1,16 +1,34 @@
use crate::hashes::SupportedUnit; //! Сборник типов для внутренней обработки данных с датчиков.
//!
//! Предполагается, что struct-ы будут совместимы с JSON API для передачи данных датчиков.
// Не забывайте про:
// #[serde(rename = "...")]
// #[serde(alias = "...")]
use crate::utils::SupportedUnit;
use crate::ingest_protocol::error::Error; use crate::ingest_protocol::error::Error;
use crate::ingest_protocol::parser::parse_mac_address; use crate::ingest_protocol::parser::parse_mac_address;
use hifitime::Epoch; use hifitime::Epoch;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::formats::Separator;
use serde_with::serde_as; use serde_with::serde_as;
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::{Hash, Hasher}; 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)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SensorValue { pub struct SensorValue {
pub mac: String, pub mac: String,
@ -26,14 +44,9 @@ impl Hash for SensorValue {
} }
} }
pub struct DashSeparator {} /// Функция-помощник для [MacAsArray], предназначенный для использования с [serde_with].
///
impl Separator for DashSeparator { /// Преобразует MAC-адрес.
fn separator() -> &'static str {
"-"
}
}
fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> { fn mac_as_array(value: &str) -> Result<[u8; 6], Error<&str>> {
Ok(parse_mac_address(value)?.1) Ok(parse_mac_address(value)?.1)
} }

View file

@ -3,7 +3,6 @@ use nom::bytes::complete::tag;
use nom::bytes::complete::take_until1; use nom::bytes::complete::take_until1;
use nom::bytes::complete::{take, take_while, take_while1}; use nom::bytes::complete::{take, take_while, take_while1};
use nom::character::complete::hex_digit1; use nom::character::complete::hex_digit1;
use nom::{Parser};
use std::str::FromStr; use std::str::FromStr;
use crate::ingest_protocol::error::Error; use crate::ingest_protocol::error::Error;

View file

@ -1,69 +1,13 @@
/* #![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)] #![feature(try_blocks)]
extern crate core; extern crate core;
mod hashes; mod utils;
mod ingest_protocol; mod ingest_protocol;
mod web_server; mod web_server;
use crate::web_server::server_main; 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 {} struct Params {}
#[ntex::main] #[ntex::main]

View file

@ -1,11 +1,20 @@
//! Глобальный модуль для вспомогательных типов и утилит.
//!
use phf::phf_map; use phf::phf_map;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow; use std::borrow::Cow;
/// Поддерживаемые типы.
///
/// TODO: Решить необходимо ли к данным прикреплять единицы измерения.
/// TODO: Сейчас вообще сомнительно оставлять ли это или нет.
#[derive(Debug, Clone, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Hash, PartialEq, Eq)]
#[repr(u64)] #[repr(u64)]
pub enum SupportedUnit { pub enum SupportedUnit {
Celsius, // Needs verification > 273.15 Celsius, // Needs verification > -273.15
Percentage, // Needs verification >= 100 && <= 0 Percentage, // Needs verification >= 100 && <= 0
MillimeterHg, // Needs verification MillimeterHg, // Needs verification
UVIndex, // Needs verification UVIndex, // Needs verification
@ -56,6 +65,7 @@ impl<'de> Deserialize<'de> for SupportedUnit {
} }
} }
/// Таблица преобразования текстового представления единиц в значения [SupportedUnit].
static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! { static STR_TO_UNITS: phf::Map<&'static str, SupportedUnit> = phf_map! {
"C" => SupportedUnit::Celsius, "C" => SupportedUnit::Celsius,
"%" => SupportedUnit::Percentage, "%" => SupportedUnit::Percentage,

View file

@ -1,22 +1,22 @@
use derive_more::Display; use derive_more::Display;
use fred::prelude::*; use fred::prelude::*;
use ntex::http::{StatusCode}; use ntex::http::StatusCode;
use ntex::web; use ntex::web;
use ntex::web::{HttpRequest, HttpResponse}; use ntex::web::{HttpRequest, HttpResponse};
use thiserror::Error; use thiserror::Error;
use crate::insert_header; use crate::insert_header;
use crate::web_server::old_devices_api::QSParserError; use crate::web_server::old_device_sensor_api::qs_parser::QSParserError;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use serde_json::json; use serde_json::json;
/// Главный объект ошибки [std::error::Error] для всего Web API.
///
/// В целом, все Result у Web сервера должны использовать этот Error.
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
pub enum AppError { pub enum AppError {
#[display(fmt = "IDK")] #[display(fmt = "IDK")]
@ -48,6 +48,9 @@ pub enum AppError {
json_err: Option<serde_json::Error>, json_err: Option<serde_json::Error>,
query_error: Option<serde_qs::Error>, query_error: Option<serde_qs::Error>,
}, },
#[display(fmt = "IDK")]
DeviceNotFound(String)
} }
impl web::error::WebResponseError for AppError { impl web::error::WebResponseError for AppError {
@ -61,6 +64,7 @@ impl web::error::WebResponseError for AppError {
AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST, AppError::ApiKeyInvalid { .. } => StatusCode::BAD_REQUEST,
AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST, AppError::UnknownBody { .. } => StatusCode::BAD_REQUEST,
AppError::QSError(..) => StatusCode::BAD_REQUEST, AppError::QSError(..) => StatusCode::BAD_REQUEST,
AppError::DeviceNotFound(..) => StatusCode::BAD_REQUEST
} }
} }
@ -76,6 +80,7 @@ impl web::error::WebResponseError for AppError {
"Can't figure out where and in what encoding the main data is" "Can't figure out where and in what encoding the main data is"
} }
AppError::QSError(..) => "UrlEncoded body or query params are incorrect", AppError::QSError(..) => "UrlEncoded body or query params are incorrect",
AppError::DeviceNotFound(..) => "Device not found",
}; };
let status_code = self.status_code(); let status_code = self.status_code();

View file

@ -1,12 +1,19 @@
use crate::web_server::old_app_api::old_api_handler; //! Модуль веб сервера.
//!
//! Все модули отвечают только за Web сторону. Такие вещи как
//! [crate::web_server::old_device_sensor_api] отвечают только за веб версию.
//!
//! TODO: Начать работу над TCP/UDP и MQTT сервером
use old_app_api::old_api_handler;
use fred::bytes_utils::Str; use fred::bytes_utils::Str;
use fred::prelude::*; use fred::prelude::*;
pub(crate) mod app_error; pub mod app_error;
pub mod old_app_api; pub mod old_app_api;
mod old_devices_api; pub mod old_device_sensor_api;
pub mod utils; pub mod utils;
#[derive(Clone)] #[derive(Clone)]
@ -14,7 +21,7 @@ pub struct NMAppState {
pub redis_client: RedisClient, pub redis_client: RedisClient,
} }
use crate::web_server::old_devices_api::device_handler; use crate::web_server::old_device_sensor_api::device_handler;
use ntex::web; use ntex::web;

View file

@ -1,45 +0,0 @@
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")
}

View file

@ -1,36 +0,0 @@
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)
}

View file

@ -1,7 +1,36 @@
mod methods; 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};
pub use methods::*; 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)
}

View file

@ -1,5 +1,53 @@
mod config_app; //! Модуль обработки данных для приложений конечных пользователей, которые используют старый API.
mod handlers; mod handlers;
mod types; mod types;
pub use config_app::old_api_handler; 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")
}

View file

@ -1,4 +1,3 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
@ -24,6 +23,11 @@ pub struct AddLikeRequest {
pub version: u64, pub version: u64,
} }
/// Обязательные параметры у JSON app API.
///
/// При обработке входящих данных производиться два запроса [serde_json::from_str]. Один вызов пытается
/// получить [MandatoryParams], другой в зависимости от `cmd`. Это позволяет не добвалять поля из
/// этой структуры в каждом специфичном типе.
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MandatoryParams<'a> { pub struct MandatoryParams<'a> {
#[serde(borrow)] #[serde(borrow)]
@ -32,6 +36,11 @@ pub struct MandatoryParams<'a> {
#[serde(borrow)] #[serde(borrow)]
pub lang: Cow<'a, str>, pub lang: Cow<'a, str>,
/// Уникальный ID клиента.
///
/// Используется на подобии как куки PHPSESSID в php.
///
/// См. также: <https://www.php.net/manual/en/book.session.php>
#[serde(borrow)] #[serde(borrow)]
pub uuid: Cow<'a, str>, pub uuid: Cow<'a, str>,

View file

@ -0,0 +1,124 @@
//! Модуль обработки данных с устройств, которые используют старый 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())
}

View file

@ -1,21 +1,14 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::num::ParseFloatError; use std::num::ParseFloatError;
use std::str::FromStr; 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 std::sync::Arc;
use fred::bytes_utils::Str;
use hifitime::efmt::consts::ISO8601;
use hifitime::Epoch; use hifitime::Epoch;
use ntex::util::HashMap;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use thiserror::Error; 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], но это слишком большая морока /// В иделае было бы хорошо сделать всё как у [serde_json::Error], но это слишком большая морока
#[derive(Error, Clone, Debug)] #[derive(Error, Clone, Debug)]
@ -47,6 +40,11 @@ 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> { pub fn qs_rest_to_values(parsed: HashMap<String, String>) -> Result<HashSet<SensorValue>, QSParserError> {
let mut hashset = HashSet::new(); let mut hashset = HashSet::new();
@ -118,56 +116,4 @@ pub async fn parse_nm_qs_format(input: &str) -> Result<NMDeviceDataPacket, QSPar
}; };
Ok(device_data) 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())
}

View file

@ -1,3 +1,6 @@
//! Сборник полезных функций которые используются в многих местах одновременно или слишком
//! неспециализированные.
pub mod redis; pub mod redis;
#[macro_export] #[macro_export]

View file

@ -1,3 +1,5 @@
//! Сборник утилит для работы с Redis.
use crate::web_server::app_error::AppError; use crate::web_server::app_error::AppError;
use fred::prelude::*; use fred::prelude::*;
use heapless::String as HeaplessString; use heapless::String as HeaplessString;
@ -6,13 +8,17 @@ use regex::Regex;
use ufmt::uwrite; use ufmt::uwrite;
lazy_static! { lazy_static! {
/// Разрешённые знаки для API ключа.
static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap(); static ref ALLOWED_API_KEY_CHARACTERS: Regex = Regex::new("[a-zA-Z0-9]{13}").unwrap();
} }
/// Описание полей в KV DB у `apikey_{}`.
pub struct ApiKeyDescription { pub struct ApiKeyDescription {
/// ID владельца API ключа.
apikey_owner: i64, apikey_owner: i64,
} }
/// Проверка API ключа на валидность.
pub async fn is_api_key_valid( pub async fn is_api_key_valid(
client: &RedisClient, client: &RedisClient,
api_key: &str, api_key: &str,