First commit

This commit is contained in:
Egor 2024-11-27 18:56:48 +03:00
commit fce7dbd6da
13 changed files with 3166 additions and 0 deletions

3
.env.example Normal file
View file

@ -0,0 +1,3 @@
BOT_TOKEN="token"
YA_TOKEN="token"
WHITELISTED="user1,user2,user3"

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.env

2828
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

16
Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "ya-metrics-bot"
version = "0.1.0"
edition = "2021"
[dependencies]
teloxide = { version = "0.13", features = ["macros", "sqlite-storage-nativetls"] }
log = "0.4"
pretty_env_logger = "0.5"
tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
thiserror = "1.0.66"
serde = "1.0.214"
dotenvy = "0.15.7"
diesel = { version = "2.2.5", features = ["sqlite"] }
reqwest = { version = "0.12.9", features = ["json"] }
bitflags = "2.6.0"

58
src/bot.rs Normal file
View file

@ -0,0 +1,58 @@
pub mod keyboard;
use std::error::Error;
use teloxide::dispatching::dialogue::ErasedStorage;
use teloxide::dispatching::{DefaultKey, DispatcherBuilder};
use teloxide::dispatching::{HandlerExt, UpdateFilterExt};
use teloxide::payloads::SendMessageSetters;
use teloxide::prelude::{dptree, Dispatcher, Requester};
use teloxide::types::Update;
use teloxide::Bot;
use teloxide::{dispatching::dialogue, types::Message};
use crate::config;
type Dialogue = dialogue::Dialogue<State, dialogue::ErasedStorage<State>>;
type HandlerResult = Result<(), Box<dyn Error + Send + Sync>>;
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub enum State {
#[default]
Start,
SelectAction,
UpdateWhitelist,
GetUsername,
AddToWhitelist,
RemoveFromWhitelist,
GetPeriod,
}
async fn start(bot: Bot, dialogue: Dialogue, msg: Message) -> HandlerResult {
bot.send_message(msg.chat.id, "За какой период вывести статистику?")
.reply_markup(keyboard::period_keyboard());
dialogue.update(State::GetPeriod).await?;
Ok(())
}
async fn select_action(bot: Bot, dialogue: Dialogue, msg: Message) -> HandlerResult {
Ok(())
}
async fn recieve_period(bot: Bot, dialogue: Dialogue, msg: Message) -> HandlerResult {
Ok(())
}
pub fn bot_dispatcher(
settings: config::Settings,
) -> DispatcherBuilder<Bot, Box<dyn Error + Send + Sync>, DefaultKey> {
let bot = Bot::new(settings.get_bot_token());
Dispatcher::builder(
bot,
Update::filter_message()
.enter_dialogue::<Message, ErasedStorage<State>, State>()
.branch(dptree::case![State::Start].endpoint(start))
.branch(dptree::case![State::SelectAction].endpoint(select_action))
.branch(dptree::case![State::GetPeriod].endpoint(recieve_period)),
)
.dependencies(dptree::deps![ErasedStorage::<State>::new()])
}

14
src/bot/keyboard.rs Normal file
View file

@ -0,0 +1,14 @@
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup};
pub fn period_keyboard() -> InlineKeyboardMarkup {
InlineKeyboardMarkup::new(vec![
vec![
InlineKeyboardButton::callback("Сегодня", "today"),
InlineKeyboardButton::callback("Вчера", "yesterday"),
],
vec![
InlineKeyboardButton::callback("Этот месяц", "thismonth"),
InlineKeyboardButton::callback("Пред. месяц", "lastmonth"),
],
])
}

100
src/config.rs Normal file
View file

@ -0,0 +1,100 @@
pub mod models;
pub mod schema;
use dotenvy::dotenv;
use std::collections::HashSet;
use std::env;
use std::fmt::Debug;
pub struct Settings {
bot_token: String,
ya_token: String,
whitelist: Whitelist,
}
impl Settings {
pub fn get_bot_token(&self) -> &str {
&self.bot_token
}
pub fn get_ya_token(&self) -> &str {
&self.ya_token
}
pub fn get_whitelist(&self) -> &Whitelist {
&self.whitelist
}
}
pub struct Whitelist {
enforce_whitelist: bool,
whitelisted: HashSet<String>,
}
impl Whitelist {
pub fn non_enforcable_whitelist() -> Self {
Self {
enforce_whitelist: false,
whitelisted: HashSet::new(),
}
}
pub fn enforcable_whitelist(whitelisted: HashSet<String>) -> Self {
Self {
enforce_whitelist: true,
whitelisted,
}
}
pub fn is_user_whitelisted(&self, user: &str) -> bool {
if self.enforce_whitelist {
return self.whitelisted.contains(user);
}
return true;
}
}
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
#[error("{0} env variable is not set, unable to continue")]
EnvVarNotSet(&'static str),
}
const BOT_TOKEN_ENV_VAR: &str = "BOT_TOKEN";
const YA_TOKEN_ENV_VAR: &str = "YA_TOKEN";
const WHITELISTED_ENV_VAR: &str = "WHITELISTED";
pub fn init_settings() -> Result<Settings, ConfigError> {
if let Err(error) = dotenv() {
log::warn!("could not read .env file: {error}");
}
let settings = Settings {
bot_token: env::var(BOT_TOKEN_ENV_VAR)
.map_err(|_| ConfigError::EnvVarNotSet(BOT_TOKEN_ENV_VAR))?,
ya_token: env::var(YA_TOKEN_ENV_VAR)
.map_err(|_| ConfigError::EnvVarNotSet(YA_TOKEN_ENV_VAR))?,
whitelist: init_whitelist(),
};
Ok(settings)
}
const WHITELIST_SEPARATOR: char = ',';
const WHITELIST_NON_ENFORCE: &str = "*";
fn init_whitelist() -> Whitelist {
if let Ok(whitelisted_list) = env::var(WHITELISTED_ENV_VAR) {
if whitelisted_list == WHITELIST_NON_ENFORCE {
log::info!("initializing non-enforcable whitelist");
return Whitelist::non_enforcable_whitelist();
}
log::info!("initializing enforcable whitelist for users {whitelisted_list}");
let whitelisted = whitelisted_list
.split(WHITELIST_SEPARATOR)
.map(|s| s.to_string())
.collect();
return Whitelist::enforcable_whitelist(whitelisted);
}
log::warn!("{WHITELISTED_ENV_VAR} env var is not set. Initializing non-enforcable whitelist");
Whitelist::non_enforcable_whitelist()
}

2
src/config/models.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod proxy;
pub mod user;

View file

@ -0,0 +1,69 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use diesel::{
backend::Backend, deserialize::FromSql, prelude::Queryable, serialize::ToSql,
sql_types::Integer, Selectable,
};
#[derive(Debug, Clone, PartialEq, Eq, Queryable, Selectable)]
#[diesel(table_name = crate::config::schema::proxies)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Socks5Proxy {
ip: IpAddr,
port: u16,
user: String,
password: String,
}
impl Socks5Proxy {
fn new(ip: IpAddr, port: u16, user: &str, password: &str) -> Self {
Self {
ip,
port,
user: user.to_string(),
password: password.to_string(),
}
}
}
impl ToString for Socks5Proxy {
fn to_string(&self) -> String {
format!(
"socks5://{}:{}@{}:{}",
self.user, self.password, self.ip, self.port
)
}
}
impl<DB> ToSql<Integer, DB> for IpAddr
where
DB: Backend,
u128: ToSql<Integer, DB>,
{
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, DB>,
) -> diesel::serialize::Result {
let bits = match self {
IpAddr::V4(ip) => ip.to_bits() as u128,
IpAddr::V6(ip) => ip.to_bits(),
};
bits.to_sql(out)
}
}
impl<DB> FromSql<Integer, DB> for IpAddr
where
DB: Backend,
u128: FromSql<Integer, DB>,
{
fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let bits = u128::from_sql(bytes)?;
let ip = if bits <= Ipv4Addr::BITS as u128 {
IpAddr::V4(Ipv4Addr::from_bits(bits as u32))
} else {
IpAddr::V6(Ipv6Addr::from_bits(bits))
};
diesel::deserialize::Result::Ok(ip)
}
}

27
src/config/models/user.rs Normal file
View file

@ -0,0 +1,27 @@
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Permissions: u8 {
const WHITELIST_MODIFY = 0b00000001;
const YA_API_URL_MODIFY = 0b00000010;
const PROXY_MODIFY = 0b00000100;
}
}
impl ToString for Permissions {
fn to_string(&self) -> String {
if self.is_empty() {
return "None".to_string();
}
let mut result = String::with_capacity(128);
bitflags::parser::to_writer(self, &mut result);
result
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
username: String,
permissions: Permissions,
}

17
src/config/schema.rs Normal file
View file

@ -0,0 +1,17 @@
diesel::table! {
users (id) {
id -> Integer,
username -> Text,
permissions -> Integer
}
}
diesel::table! {
proxies (id) {
id -> Integer,
ip -> Integer,
port -> Integer,
user -> Text,
password -> Text
}
}

30
src/main.rs Normal file
View file

@ -0,0 +1,30 @@
mod bot;
mod config;
mod ya_api;
use std::process::ExitCode;
use teloxide::Bot;
#[tokio::main]
async fn main() -> ExitCode {
// init logger with default log level "info"
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Info)
.parse_default_env()
.init();
log::info!("starting ya-metrics-bot...");
log::info!("initializing settings...");
let settings = match config::init_settings() {
Ok(settings) => settings,
Err(error) => {
log::error!("settings init failed: {error}");
return ExitCode::FAILURE;
}
};
log::info!("settings initialized");
log::info!("starting telegram bot...");
let bot = Bot::new(settings.get_bot_token());
return ExitCode::SUCCESS;
}

0
src/ya_api.rs Normal file
View file