273 lines
7.8 KiB
Rust
273 lines
7.8 KiB
Rust
pub mod admin;
|
|
pub mod bot_handler;
|
|
pub mod bot_manager;
|
|
pub mod botscript;
|
|
pub mod commands;
|
|
pub mod db;
|
|
pub mod handlers;
|
|
pub mod message_answerer;
|
|
pub mod mongodb_storage;
|
|
pub mod utils;
|
|
|
|
use bot_manager::BotManager;
|
|
use botscript::application::attach_user_application;
|
|
use botscript::{Runner, RunnerConfig, ScriptError, ScriptResult};
|
|
use db::application::Application;
|
|
use db::bots::BotInstance;
|
|
use db::callback_info::CallbackInfo;
|
|
use handlers::admin::admin_handler;
|
|
use log::{error, info};
|
|
use message_answerer::MessageAnswererError;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use crate::db::{CallDB, DB};
|
|
use crate::mongodb_storage::MongodbStorage;
|
|
|
|
use db::DbError;
|
|
use envconfig::Envconfig;
|
|
use serde::{Deserialize, Serialize};
|
|
use teloxide::dispatching::dialogue::serializer::Json;
|
|
use teloxide::dispatching::dialogue::Serializer;
|
|
use teloxide::prelude::*;
|
|
|
|
type BotDialogue = Dialogue<State, MongodbStorage<Json>>;
|
|
|
|
#[derive(Envconfig, Clone)]
|
|
pub struct Config {
|
|
#[envconfig(from = "BOT_TOKEN")]
|
|
pub bot_token: String,
|
|
#[envconfig(from = "DATABASE_URL")]
|
|
pub db_url: String,
|
|
#[envconfig(from = "ADMIN_PASS")]
|
|
pub admin_password: String,
|
|
#[envconfig(from = "ADMIN_ID")]
|
|
pub admin_id: u64,
|
|
#[envconfig(from = "BOT_NAME")]
|
|
pub bot_name: String,
|
|
}
|
|
|
|
trait LogMsg {
|
|
fn log(self) -> Self;
|
|
}
|
|
|
|
impl LogMsg for <teloxide::Bot as teloxide::prelude::Requester>::SendMessage {
|
|
fn log(self) -> Self {
|
|
info!("msg: {}", self.text);
|
|
self
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
|
|
pub enum State {
|
|
#[default]
|
|
Start,
|
|
Edit {
|
|
literal: String,
|
|
variant: Option<String>,
|
|
lang: String,
|
|
is_caption_set: bool,
|
|
},
|
|
EditButton,
|
|
MessageForwardReply,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Callback {
|
|
MoreInfo,
|
|
ProjectPage { id: u32 },
|
|
GoHome,
|
|
LeaveApplication,
|
|
AskQuestion, // Add this line for the new callback
|
|
}
|
|
|
|
type CallbackStore = CallbackInfo<Callback>;
|
|
|
|
#[derive(Clone)]
|
|
pub struct BotController {
|
|
pub bot: Bot,
|
|
pub db: DB,
|
|
pub runtime: Arc<Mutex<BotRuntime>>,
|
|
}
|
|
|
|
pub struct BotRuntime {
|
|
pub rc: RunnerConfig,
|
|
pub runner: Runner,
|
|
}
|
|
unsafe impl Send for BotRuntime {}
|
|
|
|
impl Drop for BotController {
|
|
fn drop(&mut self) {
|
|
info!("called drop for BotController");
|
|
}
|
|
}
|
|
|
|
const MAIN_BOT_SCRIPT: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/mainbot.js"));
|
|
|
|
impl BotController {
|
|
pub async fn new(config: &Config) -> ScriptResult<Self> {
|
|
Self::create(
|
|
&config.bot_token,
|
|
&config.db_url,
|
|
&config.bot_name,
|
|
MAIN_BOT_SCRIPT,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn create(token: &str, db_url: &str, name: &str, script: &str) -> ScriptResult<Self> {
|
|
let db = DB::init(db_url, name.to_owned()).await?;
|
|
|
|
Self::with_db(db, token, script).await
|
|
}
|
|
|
|
pub async fn with_db(mut db: DB, token: &str, script: &str) -> ScriptResult<Self> {
|
|
let bot = Bot::new(token);
|
|
|
|
let mut runner = Runner::init_with_db(&mut db)?;
|
|
runner.call_attacher(|c, o| attach_user_application(c, o, db.clone(), bot.clone()))??;
|
|
let rc = runner.init_config(script)?;
|
|
let runtime = Arc::new(Mutex::new(BotRuntime { rc, runner }));
|
|
|
|
Ok(Self { bot, db, runtime })
|
|
}
|
|
}
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum BotError {
|
|
DBError(#[from] DbError),
|
|
TeloxideError(#[from] teloxide::RequestError),
|
|
// TODO: not a really good to hardcode types, better to extend it later
|
|
StorageError(#[from] mongodb_storage::MongodbStorageError<<Json as Serializer<State>>::Error>),
|
|
MsgTooOld(String),
|
|
BotLogicError(String),
|
|
AdminMisconfiguration(String),
|
|
ScriptError(#[from] ScriptError),
|
|
IoError(#[from] std::io::Error),
|
|
RwLockError(String),
|
|
MAError(#[from] MessageAnswererError),
|
|
}
|
|
|
|
pub type BotResult<T> = Result<T, BotError>;
|
|
|
|
impl std::fmt::Display for BotError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{:?}", self)
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
dotenvy::dotenv()?;
|
|
pretty_env_logger::init();
|
|
let config = Config::init_from_env()?;
|
|
|
|
let mut db = DB::init(&config.db_url, config.bot_name.to_owned()).await?;
|
|
|
|
BotInstance::restart_all(&mut db, false).await?;
|
|
// if we can't get info for main bot, we should stop anyway
|
|
#[allow(clippy::unwrap_used)]
|
|
let bm = BotManager::with(
|
|
async || {
|
|
let config = config.clone();
|
|
|
|
let mut db = DB::init(config.db_url, config.bot_name.to_owned())
|
|
.await
|
|
.unwrap();
|
|
let bi = BotInstance::new(
|
|
config.bot_name,
|
|
config.bot_token,
|
|
MAIN_BOT_SCRIPT.to_string(),
|
|
);
|
|
let instances = BotInstance::get_all(&mut db).await.unwrap();
|
|
BotInstance::restart_all(&mut db, false).await.unwrap();
|
|
std::iter::once(bi).chain(instances)
|
|
},
|
|
async |_| vec![admin_handler()].into_iter(),
|
|
);
|
|
|
|
bm.dispatch(&mut db).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_application_to_chat(
|
|
bot: &Bot,
|
|
db: &mut DB,
|
|
app: &Application<teloxide::types::User>,
|
|
) -> BotResult<Message> {
|
|
let chat_id: i64 = match db.get_literal_value("support_chat_id").await? {
|
|
Some(strcid) => match strcid.parse() {
|
|
Ok(cid) => cid,
|
|
Err(err) => {
|
|
notify_admin(&format!(
|
|
"Support chat_id should be a number. Got: {strcid}, err: {err}.\n\
|
|
Anyways, applied user: {:?}",
|
|
app.from
|
|
))
|
|
.await;
|
|
return Err(BotError::BotLogicError(format!("somewhere in bots logic support_chat_id literal not stored as a number, got: {strcid}")));
|
|
}
|
|
},
|
|
None => {
|
|
notify_admin(&format!(
|
|
"support_chat_id is not set!!!\nAnyways, applied user: {:?}",
|
|
app.from
|
|
))
|
|
.await;
|
|
return Err(BotError::AdminMisconfiguration(
|
|
"admin forget to set support_chat_id".to_string(),
|
|
));
|
|
}
|
|
};
|
|
let msg = match db.get_literal_value("application_format").await? {
|
|
Some(msg) => msg
|
|
.replace("{user_id}", app.from.id.0.to_string().as_str())
|
|
.replace(
|
|
"{username}",
|
|
app.from
|
|
.username
|
|
.clone()
|
|
.unwrap_or("Username not set".to_string())
|
|
.as_str(),
|
|
),
|
|
None => {
|
|
notify_admin("format for support_chat_id is not set").await;
|
|
return Err(BotError::AdminMisconfiguration(
|
|
"admin forget to set application_format".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
Ok(bot.send_message(ChatId(chat_id), msg).await?)
|
|
}
|
|
|
|
/// This is an emergent situation function, so it should not return any Result, but handle Results
|
|
/// on its own
|
|
async fn notify_admin(text: &str) {
|
|
let config = match Config::init_from_env() {
|
|
Ok(config) => config,
|
|
Err(err) => {
|
|
error!("notify_admin: Failed to get config from env, err: {err}");
|
|
return;
|
|
}
|
|
};
|
|
let bot = Bot::new(&config.bot_token);
|
|
match bot.send_message(UserId(config.admin_id), text).await {
|
|
Ok(_) => {}
|
|
Err(err) => {
|
|
error!("notify_admin: Failed to send message to admin, WHATS WRONG???, err: {err}");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn update_user_tg(user: db::User, tguser: &teloxide::types::User) -> db::User {
|
|
db::User {
|
|
first_name: tguser.first_name.clone(),
|
|
last_name: tguser.last_name.clone(),
|
|
username: tguser.username.clone(),
|
|
language_code: tguser.language_code.clone(),
|
|
..user
|
|
}
|
|
}
|