Added id field to all model's structs
Implemented YandexRequester summary method (TODO: test it) Added BotData struct that will replace the Settings struct in the future Implemented update methods for some structs' tables in BotData (TODO: implement update_requester and new methods) Moved migrations to root directory Note: use sqlx-cli from cargo to manage migrations from now on Added *.db files to .gitignore Added serde_json to Cargo.toml to parse json in YandexRequester
This commit is contained in:
parent
79f21425b1
commit
2069d9104c
9 changed files with 244 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
.env
|
||||
*.db
|
||||
|
|
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -1805,9 +1805,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.132"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
@ -2963,6 +2963,7 @@ dependencies = [
|
|||
"pretty_env_logger",
|
||||
"reqwest 0.12.9",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"teloxide",
|
||||
"thiserror 2.0.3",
|
||||
|
|
|
@ -14,3 +14,4 @@ dotenvy = "0.15.7"
|
|||
reqwest = { version = "0.12.9", features = ["json"] }
|
||||
bitflags = "2.6.0"
|
||||
sqlx = { version = "0.8.2", features = ["runtime-tokio", "tls-rustls", "sqlite"] }
|
||||
serde_json = "1.0.133"
|
||||
|
|
|
@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
|
||||
CREATE TABLE IF NOT EXISTS proxies (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ip INTEGER NOT NULL,
|
||||
ip BLOB NOT NULL,
|
||||
port INTEGER NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
|
@ -17,5 +17,5 @@ CREATE TABLE IF NOT EXISTS requesters (
|
|||
name TEXT NOT NULL,
|
||||
api TEXT NOT NULL,
|
||||
proxy_id INTEGER,
|
||||
FOREIGN_KEY (proxy_id) REFERENCES proxies (id)
|
||||
FOREIGN KEY (proxy_id) REFERENCES proxies (id)
|
||||
);
|
|
@ -26,5 +26,5 @@ async fn main() -> ExitCode {
|
|||
|
||||
log::info!("starting telegram bot...");
|
||||
let bot = Bot::new(settings.get_bot_token());
|
||||
return ExitCode::SUCCESS;
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
|
156
src/model.rs
156
src/model.rs
|
@ -1,3 +1,159 @@
|
|||
pub mod proxy;
|
||||
pub mod requester;
|
||||
pub mod user;
|
||||
|
||||
use std::{collections::HashMap, net::IpAddr};
|
||||
|
||||
use proxy::Socks5Proxy;
|
||||
use requester::YandexRequester;
|
||||
use sqlx::SqlitePool;
|
||||
use user::User;
|
||||
|
||||
pub struct BotData {
|
||||
token: String,
|
||||
// map username to id to get users faster by name
|
||||
user_ids: HashMap<String, i32>,
|
||||
users: HashMap<i32, User>,
|
||||
proxies: HashMap<i32, Socks5Proxy>,
|
||||
requesters: HashMap<i32, YandexRequester>,
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum BotError {
|
||||
#[error("user with id {0} does not exist")]
|
||||
UserNotFound(i32),
|
||||
#[error("failed to update user with id {0}: {1}")]
|
||||
UserUpdateFailed(i32, sqlx::Error),
|
||||
#[error("failed to update user with id {0}: user exists in memory but not in database")]
|
||||
UserNotFoundInDatabase(i32),
|
||||
#[error("proxy with id {0} does not exist")]
|
||||
ProxyNotFound(i32),
|
||||
#[error("failed to update proxy with id {0}: {1}")]
|
||||
ProxyUpdateFailed(i32, sqlx::Error),
|
||||
#[error("failed to update proxy with id {0}: proxy exists in memory but not in database")]
|
||||
ProxyNotFoundInDatabase(i32),
|
||||
#[error("requester with id {0} does not exist")]
|
||||
RequesterNotFound(i32),
|
||||
#[error("failed to open db connection: {0}")]
|
||||
ConnectionFailed(sqlx::Error),
|
||||
}
|
||||
|
||||
impl BotData {
|
||||
pub fn new(token: String, pool: SqlitePool) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn get_token(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
|
||||
pub fn get_user_by_name(&self, username: &str) -> Option<&User> {
|
||||
let id = self.user_ids.get(username)?;
|
||||
self.users.get(id)
|
||||
}
|
||||
|
||||
pub fn get_user(&self, id: &i32) -> Option<&User> {
|
||||
self.users.get(id)
|
||||
}
|
||||
|
||||
pub async fn update_user<F>(&mut self, id: &i32, update_actions: F) -> Result<(), BotError>
|
||||
where
|
||||
F: FnOnce(&mut User),
|
||||
{
|
||||
let Some(user) = self.users.get_mut(id) else {
|
||||
return Err(BotError::UserNotFound(*id));
|
||||
};
|
||||
// old copy of a user - used to rollback user changes in memory
|
||||
// in case the database update fails
|
||||
let old_user = user.clone();
|
||||
update_actions(user);
|
||||
// return if no changes are applied
|
||||
if *user == old_user {
|
||||
return Ok(());
|
||||
}
|
||||
let (username, permissions) = (user.get_username(), user.get_permissions().bits());
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET username = ?1, permissions = ?2
|
||||
WHERE id = ?3
|
||||
"#,
|
||||
username,
|
||||
permissions,
|
||||
id
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
let Ok(query_result) = query_result else {
|
||||
self.users.insert(*id, old_user);
|
||||
return Err(BotError::UserUpdateFailed(*id, query_result.unwrap_err()));
|
||||
};
|
||||
if query_result.rows_affected() <= 0 {
|
||||
self.users.insert(*id, old_user);
|
||||
return Err(BotError::UserNotFoundInDatabase(*id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_proxy(&self, id: &i32) -> Option<&Socks5Proxy> {
|
||||
self.proxies.get(id)
|
||||
}
|
||||
|
||||
pub async fn update_proxy<F>(&mut self, id: &i32, update_actions: F) -> Result<(), BotError>
|
||||
where
|
||||
F: FnOnce(&mut Socks5Proxy),
|
||||
{
|
||||
let Some(proxy) = self.proxies.get_mut(id) else {
|
||||
return Err(BotError::ProxyNotFound(*id));
|
||||
};
|
||||
let old_proxy = proxy.clone();
|
||||
update_actions(proxy);
|
||||
if *proxy == old_proxy {
|
||||
return Ok(());
|
||||
}
|
||||
let ip = Self::ip_to_blob(proxy.get_ip());
|
||||
let (port, user, password) = (proxy.get_port(), proxy.get_user(), proxy.get_password());
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE proxies
|
||||
SET ip = ?1, port = ?2, user = ?3, password = ?4
|
||||
WHERE id = ?5
|
||||
"#,
|
||||
ip,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
id
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
let Ok(query_result) = query_result else {
|
||||
self.proxies.insert(*id, old_proxy);
|
||||
return Err(BotError::ProxyUpdateFailed(*id, query_result.unwrap_err()));
|
||||
};
|
||||
if query_result.rows_affected() <= 0 {
|
||||
self.proxies.insert(*id, old_proxy);
|
||||
return Err(BotError::ProxyNotFoundInDatabase(*id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_requester(&self, id: &i32) -> Option<&YandexRequester> {
|
||||
self.requesters.get(id)
|
||||
}
|
||||
|
||||
pub async fn update_requester<F>(&mut self, id: &i32, update_actions: F) -> Result<(), BotError>
|
||||
where
|
||||
F: FnOnce(&mut YandexRequester),
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn ip_to_blob(ip: IpAddr) -> Vec<u8> {
|
||||
match ip {
|
||||
IpAddr::V4(ipv4) => ipv4.octets().to_vec(),
|
||||
IpAddr::V6(ipv6) => ipv6.octets().to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::{fmt::Display, net::IpAddr};
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)]
|
||||
pub struct Socks5Proxy {
|
||||
pub id: i32,
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
user: String,
|
||||
|
@ -11,6 +12,7 @@ pub struct Socks5Proxy {
|
|||
impl Socks5Proxy {
|
||||
pub fn new(ip: IpAddr, port: u16, user: String, password: String) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
ip,
|
||||
port,
|
||||
user,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use reqwest::header::AUTHORIZATION;
|
||||
|
||||
use super::proxy::Socks5Proxy;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Period {
|
||||
|
@ -12,6 +12,7 @@ pub enum Period {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)]
|
||||
pub struct YandexRequester {
|
||||
pub id: i32,
|
||||
token: String,
|
||||
name: String,
|
||||
proxy: Option<Socks5Proxy>,
|
||||
|
@ -25,6 +26,8 @@ pub enum RequestError {
|
|||
ClientBuildFailed(reqwest::Error),
|
||||
#[error("get request failed: {0}")]
|
||||
GetRequestFailed(reqwest::Error),
|
||||
#[error("response missing json field: {0}")]
|
||||
MissingJSONField(&'static str),
|
||||
}
|
||||
|
||||
impl YandexRequester {
|
||||
|
@ -39,6 +42,7 @@ impl YandexRequester {
|
|||
|
||||
pub fn new_with_proxy(token: String, name: String, proxy: Socks5Proxy) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
token,
|
||||
name,
|
||||
proxy: Some(proxy),
|
||||
|
@ -47,6 +51,7 @@ impl YandexRequester {
|
|||
|
||||
pub fn new(token: String, name: String) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
token,
|
||||
name,
|
||||
proxy: None,
|
||||
|
@ -65,10 +70,11 @@ impl YandexRequester {
|
|||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
pub async fn summary(&self, period: Period) -> Result<(), RequestError> {
|
||||
let period = Self::period_as_param(period);
|
||||
pub async fn summary(&self, period: Period) -> Result<YandexResponse, RequestError> {
|
||||
log::info!("requester '{}' attempts to request data...", self.name);
|
||||
let mut builder = reqwest::Client::builder();
|
||||
if let Some(proxy) = &self.proxy {
|
||||
log::info!("requester '{}' uses proxy '{}'", self.name, proxy.url());
|
||||
let proxy =
|
||||
reqwest::Proxy::all(proxy.url()).map_err(|e| RequestError::InvalidProxy(e))?;
|
||||
builder = builder.proxy(proxy);
|
||||
|
@ -77,15 +83,69 @@ impl YandexRequester {
|
|||
.build()
|
||||
.map_err(|e| RequestError::ClientBuildFailed(e))?;
|
||||
// TODO: deserialize response to json and return something already
|
||||
let response = client
|
||||
let json_data = client
|
||||
.get(Self::YANDEX_PARTNER_URL)
|
||||
.header(AUTHORIZATION, self.token.clone())
|
||||
.query(&Self::YANDEX_PARTNER_DEFAULT_QUERY)
|
||||
.query(&("period", &period))
|
||||
.query(&("period", &Self::period_as_param(period)))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RequestError::ClientBuildFailed(e))?;
|
||||
Ok(())
|
||||
.map_err(|e| RequestError::GetRequestFailed(e))?
|
||||
.json::<Map<String, Value>>()
|
||||
.await
|
||||
.map_err(|e| RequestError::GetRequestFailed(e))?;
|
||||
log::info!(
|
||||
"requester '{}' successfully got json response, trying to parse it...",
|
||||
self.name
|
||||
);
|
||||
let response = Self::json_to_response(json_data)?;
|
||||
log::info!("requester '{}' json successfully parsed", self.name);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn json_to_response(json: Map<String, Value>) -> Result<YandexResponse, RequestError> {
|
||||
let fields = Self::get_json_relevant_fields(&json)?;
|
||||
let resposne = YandexResponse {
|
||||
shows: fields["shows"]
|
||||
.as_u64()
|
||||
.ok_or(RequestError::MissingJSONField("shows"))?,
|
||||
hits_render: fields["hits_render"]
|
||||
.as_u64()
|
||||
.ok_or(RequestError::MissingJSONField("hits_render"))?,
|
||||
partner_wo_nds: fields["partner_wo_nds"]
|
||||
.as_f64()
|
||||
.ok_or(RequestError::MissingJSONField("partner_wo_nds"))?,
|
||||
cpmv_partner_wo_nds: fields["cpmv_partner_wo_nds"]
|
||||
.as_f64()
|
||||
.ok_or(RequestError::MissingJSONField("cpmv_partner_wo_nds"))?,
|
||||
};
|
||||
Ok(resposne)
|
||||
}
|
||||
|
||||
fn get_json_relevant_fields(
|
||||
json: &Map<String, Value>,
|
||||
) -> Result<&Map<String, Value>, RequestError> {
|
||||
let data = Self::get_field(json, "data")?;
|
||||
let totals = Self::get_field(data, "totals")?;
|
||||
let field2 = totals["2"]
|
||||
.as_array()
|
||||
.ok_or(RequestError::MissingJSONField("2"))?;
|
||||
if field2.len() < 1 {
|
||||
return Err(RequestError::MissingJSONField("array element 0"));
|
||||
}
|
||||
let fields = field2[0]
|
||||
.as_object()
|
||||
.ok_or(RequestError::MissingJSONField("array element 0"))?;
|
||||
Ok(fields)
|
||||
}
|
||||
|
||||
fn get_field<'a>(
|
||||
json: &'a Map<String, Value>,
|
||||
field: &'static str,
|
||||
) -> Result<&'a Map<String, Value>, RequestError> {
|
||||
json[field]
|
||||
.as_object()
|
||||
.ok_or(RequestError::MissingJSONField(field))
|
||||
}
|
||||
|
||||
fn period_as_param(period: Period) -> String {
|
||||
|
@ -98,3 +158,11 @@ impl YandexRequester {
|
|||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct YandexResponse {
|
||||
shows: u64,
|
||||
hits_render: u64,
|
||||
partner_wo_nds: f64,
|
||||
cpmv_partner_wo_nds: f64,
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ impl Display for Permissions {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
username: String,
|
||||
permissions: Permissions,
|
||||
}
|
||||
|
@ -29,6 +30,7 @@ pub struct User {
|
|||
impl User {
|
||||
pub fn new_with_perms(username: String, permissions: Permissions) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
username,
|
||||
permissions,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue