First commit
This commit is contained in:
commit
fce7dbd6da
13 changed files with 3166 additions and 0 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
BOT_TOKEN="token"
|
||||
YA_TOKEN="token"
|
||||
WHITELISTED="user1,user2,user3"
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.env
|
2828
Cargo.lock
generated
Normal file
2828
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal 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
58
src/bot.rs
Normal 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
14
src/bot/keyboard.rs
Normal 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
100
src/config.rs
Normal 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
2
src/config/models.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod proxy;
|
||||
pub mod user;
|
69
src/config/models/proxy.rs
Normal file
69
src/config/models/proxy.rs
Normal 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
27
src/config/models/user.rs
Normal 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
17
src/config/schema.rs
Normal 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
30
src/main.rs
Normal 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
0
src/ya_api.rs
Normal file
Loading…
Reference in a new issue