Initial commit.

Signed-off-by: Pavel Kirilin <win10@list.ru>
This commit is contained in:
2023-02-20 01:20:41 +04:00
parent 9a04acd753
commit faae5e4898
39 changed files with 3420 additions and 2 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
.env
*.session

View File

@ -7,7 +7,11 @@
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages: stages:
- test - test
sast: sast:
tags:
- kube
stage: test stage: test
include: include:
- template: Security/SAST.gitlab-ci.yml - template: Security/SAST.gitlab-ci.yml

61
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,61 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.1.0
hooks:
- id: trailing-whitespace
- repo: local
hooks:
- id: fmt
types:
- rust
name: cargo fmt
language: system
entry: cargo
pass_filenames: false
args:
- fmt
- --
- --config
- use_try_shorthand=true,imports_granularity=Crate
- id: clippy
types:
- rust
name: cargo clippy
language: system
pass_filenames: false
entry: cargo
args:
- clippy
- --all-features
- --all
- -p
- rustus
- --
- -W
- clippy::all
- -W
- clippy::pedantic
- -D
- warnings
- id: check
types:
- rust
name: cargo check
language: system
entry: cargo
pass_filenames: false
args:
- check
- id: check-all
types:
- rust
name: cargo check all
language: system
entry: cargo
pass_filenames: false
args:
- check
- --all-features

2307
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

28
Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "s3bot"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-files = "0.6.2"
actix-web = "4.3.0"
anyhow = "1.0.69"
askama = { version = "0.11.1", features = ["with-actix-web"] }
askama_actix = "0.13.0"
async-trait = "0.1.64"
chrono = { version = "0.4.23", features = ["serde"] }
clap = { version = "^4.1.4", features = ["derive", "env"] }
dotenvy = "^0.15.6"
dyn-clone = "1.0.10"
fern = { version = "0.6.1", features = ["chrono", "colored"] }
futures = "0.3.26"
grammers-client = { version = "0.4.0", features = ["markdown"] }
grammers-session = "0.4.0"
log = "0.4.17"
rand = "0.8.5"
rayon = "1.6.1"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.93"
tokio = { version = "1.25.0", features = ["bytes", "rt", "macros"] }

5
askama.toml Normal file
View File

@ -0,0 +1,5 @@
[general]
# Directories to search for templates, relative to the crate root.
dirs = ["src/server/templates"]
# Unless you add a `-` in a block, whitespace characters won't be trimmed.
whitespace = "preserve"

96
src/args.rs Normal file
View File

@ -0,0 +1,96 @@
use std::path::PathBuf;
use clap::Parser;
#[derive(Clone, Parser, Debug)]
pub struct ServerConfig {
/// Server's host.
#[arg(
name = "server-host",
long,
default_value = "0.0.0.0",
env = "BOT_SERVER_HOST"
)]
/// Port that server is listening to.
pub host: String,
#[arg(
name = "server-port",
long,
default_value = "8000",
env = "BOT_SERVER_PORT"
)]
pub port: u16,
/// This password is used to authenticate
/// along with telegram code.
#[arg(
name = "server-pass",
long,
default_value = "pass",
env = "BOT_SERVER_PASS"
)]
pub server_pass: String,
/// This name is displayed on password page.
#[arg(
name = "server-username",
long,
default_value = "s3rius_san",
env = "BOT_SERVER_USERNAME"
)]
pub username: String,
#[arg(
name = "server-static-dir",
long,
default_value = "static",
env = "BOT_SERVER_STATIC_DIR"
)]
pub static_dir: PathBuf,
}
#[derive(Clone, Parser, Debug)]
pub struct BotConfig {
/// Telegram APP id. How to get it: https://core.telegram.org/api/obtaining_api_id
#[arg(name = "bot-app-id", long, env = "BOT_APP_ID")]
pub app_id: i32,
/// Telegram API hash.
#[arg(name = "bot-api-hash", long, env = "BOT_API_HASH")]
pub api_hash: String,
/// Your account's phone.
#[arg(name = "bot-phone", long, env = "BOT_ACCOUNT_PHONE")]
pub phone: String,
/// Password for two-factor authentication.
#[arg(name = "bot-tfa", long, env = "BOT_TFA_PASSWORD")]
pub tfa_password: Option<String>,
/// Name of a file to save session to.
#[arg(
name = "bot-session-file",
long,
default_value = "saved.session",
env = "BOT_SESSION_FILE"
)]
pub session_file: String,
#[arg(name = "bot-excluded-chats", long, env = "BOT_EXCLUDED_CHATS")]
pub excluded_chats: Vec<i64>,
}
#[derive(Clone, Parser, Debug)]
#[command(author, version, about)]
pub struct Arguments {
/// Application log level.
#[arg(long, default_value = "INFO", env = "BOT_LOG_LEVEL")]
pub log_level: log::LevelFilter,
#[command(flatten)]
pub server: ServerConfig,
#[command(flatten)]
pub bot: BotConfig,
}

17
src/bot/filters/base.rs Normal file
View File

@ -0,0 +1,17 @@
use dyn_clone::DynClone;
use grammers_client::Update;
pub trait Filter: DynClone + Send + Sync {
fn filter(&self, update: &Update) -> anyhow::Result<bool>;
}
dyn_clone::clone_trait_object!(Filter);
impl<T> Filter for T
where
T: Fn(&Update) -> anyhow::Result<bool> + Clone + Send + Sync,
{
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
self(update)
}
}

39
src/bot/filters/chain.rs Normal file
View File

@ -0,0 +1,39 @@
use grammers_client::Update;
use crate::bot::handlers::Handler;
use super::base::Filter;
#[derive(Clone)]
pub struct FilteredHandler {
filters: Vec<Box<dyn Filter>>,
pub handler: Box<dyn Handler>,
}
impl FilteredHandler {
pub fn new<H: Handler + 'static>(handler: H) -> Self {
Self {
filters: vec![],
handler: Box::new(handler),
}
}
pub fn add_filter<F: Filter + 'static>(mut self, filter: F) -> Self {
self.filters.push(Box::new(filter));
self
}
pub fn check(&self, update: &Update) -> bool {
for filter in &self.filters {
match filter.filter(update) {
Err(error) => {
log::error!("Fitler error: {error}");
return false;
}
Ok(false) => return false,
_ => {}
}
}
true
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,74 @@
use grammers_client::Update;
use super::base::Filter;
#[allow(dead_code)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MessageDirection {
Outgoing,
Incoming,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TextMatchMethod {
IStartsWith,
IMatches,
}
#[derive(Clone)]
pub struct ExcludedChatsFilter(pub Vec<i64>);
#[derive(Clone)]
pub struct MessageDirectionFilter(pub MessageDirection);
#[derive(Clone)]
pub struct TextFilter<'a>(pub &'a [&'a str], pub TextMatchMethod);
impl Filter for ExcludedChatsFilter {
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
let a = match update {
Update::NewMessage(message) | Update::MessageEdited(message) => message.chat(),
Update::CallbackQuery(query) => query.chat().clone(),
_ => return Ok(false),
};
if self.0.contains(&a.id()) {
return Ok(false);
}
Ok(true)
}
}
impl Filter for MessageDirectionFilter {
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
let Update::NewMessage(message) = update else {return Ok(false)};
let res = matches!(
(self.0, message.outgoing()),
(MessageDirection::Outgoing, true) | (MessageDirection::Incoming, false)
);
Ok(res)
}
}
impl<'a> Filter for TextFilter<'a> {
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
let message_text = match update {
Update::NewMessage(message) | Update::MessageEdited(message) => message.text(),
Update::InlineQuery(query) => query.text(),
_ => return Ok(false),
};
for text in self.0 {
let matches = match self.1 {
TextMatchMethod::IStartsWith => message_text
.to_lowercase()
.starts_with(text.to_lowercase().as_str()),
TextMatchMethod::IMatches => message_text.to_lowercase() == text.to_lowercase(),
};
if matches {
return Ok(true);
}
}
Ok(false)
}
}

3
src/bot/filters/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod base;
pub mod chain;
pub mod message_fitlers;

10
src/bot/handlers/base.rs Normal file
View File

@ -0,0 +1,10 @@
use async_trait::async_trait;
use dyn_clone::DynClone;
use grammers_client::{Client, Update};
#[async_trait]
pub trait Handler: DynClone + Sync + Send {
async fn react(&self, client: &Client, update: &Update) -> anyhow::Result<()>;
}
dyn_clone::clone_trait_object!(Handler);

View File

@ -0,0 +1,17 @@
use grammers_client::{Client, Update};
use crate::{bot::handlers::Handler, utils::messages::get_message};
#[derive(Clone)]
pub struct GetChatId;
#[async_trait::async_trait]
impl Handler for GetChatId {
async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
let message = get_message(update);
if let Some(msg) = message {
msg.reply(msg.chat().id().to_string()).await?;
}
Ok(())
}
}

View File

@ -0,0 +1,17 @@
use grammers_client::{Client, Update};
use crate::bot::handlers::Handler;
#[derive(Clone)]
pub struct Help;
#[async_trait::async_trait]
impl Handler for Help {
async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
let Update::NewMessage(message) = update else {return Ok(())};
message.reply("Хелпа").await?;
Ok(())
}
}

View File

@ -0,0 +1,2 @@
pub mod get_chat_id;
pub mod help;

View File

@ -0,0 +1,39 @@
use crate::{
bot::handlers::Handler,
utils::{inter_join::RandomIntersperse, messages::get_message},
};
use grammers_client::{Client, Update};
const BLYA_WORDS: &[&str] = &[", бля,", ", сука,", ", ёбаный рот,", ", охуеть конечно,"];
#[derive(Clone)]
pub struct Blyaficator;
#[async_trait::async_trait]
impl Handler for Blyaficator {
async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
if let Some(message) = get_message(update) {
let maybe_blyaficated = message.text().strip_prefix(".bl").map(|stripped| {
stripped
// Trim string after removing prefix, to
// remove leading spaces.
.trim()
// split by commas
.split(',')
// choose random strings from BLYA_WORDS
// and insert them between splitted strings.
.random_itersperse(BLYA_WORDS, &mut rand::thread_rng())
// Collect it back to vec
.collect::<Vec<_>>()
// Creating one string with all words concatenated.
.join("")
});
// If the text was blyaficated we send it as a reply.
if let Some(blyficated) = maybe_blyaficated {
message.reply(blyficated).await?;
}
}
Ok(())
}
}

View File

@ -0,0 +1,31 @@
use async_trait::async_trait;
use grammers_client::{Client, Update};
use rand::seq::IteratorRandom;
use crate::bot::handlers::base::Handler;
#[derive(Clone)]
pub struct Greeter;
#[async_trait]
impl Handler for Greeter {
async fn react(&self, client: &Client, update: &Update) -> anyhow::Result<()> {
let Update::NewMessage(message) = update else {return Ok(())};
// Check if chat has less than 100 participants.
let participants = client.iter_participants(message.chat()).total().await?;
if participants >= 100 {
return Ok(());
}
let reply_text = ["Привет!", "Добрый день!", "Здравствуйте.", "Приетствую"]
.into_iter()
.choose(&mut rand::thread_rng());
if let Some(text) = reply_text {
message.reply(text).await?;
}
Ok(())
}
}

View File

@ -0,0 +1,2 @@
pub mod blyaficator;
pub mod greeter;

5
src/bot/handlers/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod base;
pub mod basic;
pub mod fun;
pub use base::Handler;

131
src/bot/main.rs Normal file
View File

@ -0,0 +1,131 @@
use std::{sync::Arc, time::Duration};
use crate::args::BotConfig;
use grammers_client::{Client, Config, SignInError, Update};
use grammers_session::Session;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use tokio::sync::RwLock;
use super::{
filters::{
chain::FilteredHandler,
message_fitlers::{
ExcludedChatsFilter, MessageDirection, MessageDirectionFilter, TextFilter,
TextMatchMethod,
},
},
handlers::{
basic::{get_chat_id::GetChatId, help::Help},
fun::{blyaficator::Blyaficator, greeter::Greeter},
Handler,
},
};
async fn authorize(
args: &BotConfig,
client: &Client,
web_code: Arc<RwLock<Option<String>>>,
) -> anyhow::Result<()> {
log::info!("Requesting login code.");
let token = client
.request_login_code(&args.phone, args.app_id, &args.api_hash)
.await?;
let mut code = None;
while code.is_none() {
tokio::time::sleep(Duration::from_secs(1)).await;
{
code = web_code.read().await.clone();
}
}
let signed_in = client.sign_in(&token, &code.unwrap()).await;
match signed_in {
Err(SignInError::PasswordRequired(password_token)) => {
// Note: this `prompt` method will echo the password in the console.
// Real code might want to use a better way to handle this.
let hint = password_token.hint().unwrap_or("None");
let password = args
.tfa_password
.as_ref()
.unwrap_or_else(|| panic!("2FA password is required. Hint for password: `{hint}`"));
log::info!("Checking client's password.");
client
.check_password(password_token, password.trim())
.await?;
}
Ok(_) => (),
Err(e) => panic!("{}", e),
}
Ok(())
}
async fn handle_with_log(handler: Box<dyn Handler>, client: Client, update_data: Update) {
if let Err(err) = handler.react(&client, &update_data).await {
log::error!("{err}");
}
}
async fn run(args: BotConfig, client: Client) {
let handlers: Vec<FilteredHandler> = vec![
FilteredHandler::new(Greeter)
.add_filter(MessageDirectionFilter(MessageDirection::Incoming))
.add_filter(TextFilter(&["привет"], TextMatchMethod::IStartsWith))
.add_filter(ExcludedChatsFilter(args.excluded_chats)),
FilteredHandler::new(Help).add_filter(TextFilter(&[".h"], TextMatchMethod::IMatches)),
FilteredHandler::new(GetChatId)
.add_filter(TextFilter(&[".cid"], TextMatchMethod::IMatches)),
FilteredHandler::new(Blyaficator)
.add_filter(TextFilter(&[".bl"], TextMatchMethod::IStartsWith)),
];
loop {
let update = client.next_update().await;
if update.is_err() {
log::error!("{}", update.unwrap_err());
break;
}
if let Some(update_data) = update.unwrap() {
let update_ref = &update_data;
let matched_handlers = handlers
.par_iter()
.filter(move |val| val.check(update_ref))
.collect::<Vec<_>>();
for handler in matched_handlers {
tokio::spawn(handle_with_log(
handler.handler.clone(),
client.clone(),
update_data.clone(),
));
}
}
}
}
pub async fn start(args: BotConfig, web_code: Arc<RwLock<Option<String>>>) -> anyhow::Result<()> {
log::info!("Connecting to Telegram...");
let client = Client::connect(Config {
session: Session::load_file_or_create(args.session_file.as_str())?,
api_id: args.app_id,
api_hash: args.api_hash.clone(),
params: grammers_client::InitParams {
device_model: String::from("MEME_MACHINE"),
catch_up: false,
..Default::default()
},
})
.await?;
log::info!("Connected!");
if client.is_authorized().await? {
// If we already authrized, we write random token, so web won't update it.
let mut code_writer = web_code.write().await;
*code_writer = Some(String::new());
} else {
authorize(&args, &client, web_code).await?;
client.session().save_to_file(args.session_file.as_str())?;
}
run(args.clone(), client).await;
Ok(())
}

6
src/bot/mod.rs Normal file
View File

@ -0,0 +1,6 @@
mod filters;
mod handlers;
mod main;
pub mod utils;
pub use main::start;

1
src/bot/utils.rs Normal file
View File

@ -0,0 +1 @@

27
src/logging.rs Normal file
View File

@ -0,0 +1,27 @@
use fern::colors::{Color, ColoredLevelConfig};
pub fn setup_logger(log_level: log::LevelFilter) -> anyhow::Result<()> {
let colors = ColoredLevelConfig::new()
// use builder methods
.info(Color::Green)
.warn(Color::Yellow)
.debug(Color::BrightCyan)
.error(Color::BrightRed)
.trace(Color::Blue);
fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
colors.color(record.level()),
message
));
})
.level(log_level)
.chain(std::io::stdout())
.apply()?;
Ok(())
}

50
src/main.rs Normal file
View File

@ -0,0 +1,50 @@
use futures::{stream::FuturesUnordered, StreamExt};
use std::sync::Arc;
use tokio::sync::RwLock;
use clap::Parser;
use logging::setup_logger;
mod args;
mod bot;
mod logging;
mod server;
pub mod utils;
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let args = args::Arguments::parse();
setup_logger(args.log_level)?;
let token_lock = Arc::new(RwLock::new(None));
let bot_token = token_lock.clone();
let server_token = token_lock.clone();
let local_set = tokio::task::LocalSet::new();
local_set
.run_until(async {
let tasks = vec![
tokio::task::spawn_local(bot::start(args.bot.clone(), bot_token)),
tokio::task::spawn_local(server::start(args.server.clone(), server_token)),
];
// Here we wait for one async task to complete.
let completed = tasks
.into_iter()
.collect::<FuturesUnordered<_>>()
.take(1)
.collect::<Vec<_>>()
.await;
// Now we get all completed futures (one future),
// and return it's result.
if let Some(fut) = completed.into_iter().next() {
fut?
} else {
Ok(())
}
})
.await?;
Ok(())
}

29
src/server/main.rs Normal file
View File

@ -0,0 +1,29 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use actix_web::{web::Data, App, HttpServer};
use crate::args::ServerConfig;
use super::routes::{healthcheck, index, login};
pub async fn start(args: ServerConfig, token: Arc<RwLock<Option<String>>>) -> anyhow::Result<()> {
let addr = (args.host.clone(), args.port);
HttpServer::new(move || {
App::new()
.wrap(actix_web::middleware::Logger::new(
"\"%r\" \"-\" \"%s\" \"%a\" \"%D\"",
))
.app_data(Data::new(token.clone()))
.app_data(Data::new(args.clone()))
.service(login)
.service(index)
.service(healthcheck)
.service(actix_files::Files::new("/static", args.static_dir.clone()))
})
.bind(addr)?
.workers(1)
.run()
.await?;
Ok(())
}

6
src/server/mod.rs Normal file
View File

@ -0,0 +1,6 @@
mod main;
mod routes;
mod schema;
mod templates;
pub use main::start;

49
src/server/routes.rs Normal file
View File

@ -0,0 +1,49 @@
use std::sync::Arc;
use actix_web::{
get, post,
web::{Data, Json},
HttpResponse, Responder,
};
use tokio::sync::RwLock;
use super::{schema::LoginRequestDTO, templates::Index};
use crate::args::ServerConfig;
#[allow(clippy::unused_async)]
#[get("/health")]
async fn healthcheck() -> impl Responder {
HttpResponse::Ok().finish()
}
#[post("/login")]
pub async fn login(
code_data: Json<LoginRequestDTO>,
token: Data<Arc<RwLock<Option<String>>>>,
config: Data<ServerConfig>,
) -> impl Responder {
if code_data.password != config.server_pass {
log::warn!("Passwords don't match");
return HttpResponse::BadRequest().body("Passwords don't match");
}
if token.read().await.is_some() {
log::warn!("Token is already set.");
return HttpResponse::Conflict().body("Token is already set");
}
let mut token_write = token.write().await;
*token_write = Some(code_data.code.clone());
log::info!("Token is updated!");
HttpResponse::Ok().finish()
}
#[get("/")]
pub async fn index(
token: Data<Arc<RwLock<Option<String>>>>,
config: Data<ServerConfig>,
) -> impl Responder {
Index {
activated: token.read().await.is_some(),
username: config.username.clone(),
image_num: rand::random::<u8>() % 3,
}
}

7
src/server/schema.rs Normal file
View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoginRequestDTO {
pub code: String,
pub password: String,
}

View File

@ -0,0 +1,181 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/nes.min.css" rel="stylesheet" />
<style>
@font-face {
font-family: 'Press Start 2P';
font-style: normal;
font-weight: 400;
src: url(/static/fonts/ps2p_latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
html,
body,
pre,
code,
kbd,
samp {
font-family: "Press Start 2P", serif;
}
</style>
<style>
.center {
position: absolute;
top: 50%;
left: 50%;
margin: 0 -50% 0 0;
transform: translate(-50%, -50%);
z-index: 10000;
}
.large-center-text {
text-align: center;
font-size: large;
}
.at_bottom {
position: absolute;
left: 0;
top: 100%;
font-size: large;
transform: translate(-0%, -100%)
}
html {
height: 100%;
background: #A1FFCE;
/* fallback for old browsers */
background: -webkit-linear-gradient(to right, #FAFFD1, #A1FFCE);
/* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to right, #FAFFD1, #A1FFCE);
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
.mt-15 {
margin-top: 15px;
}
.nes-input[type='submit']:hover {
background-color: #76c442;
}
.nes-input {
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABFklEQVRYR9WXURLDIAhE6/0PbSdOtUpcd1Gnpv1KGpTHBpCE1/cXq+vrMph7dGvXZTtpfW10DCA5jrH1H0Jhs5E0hnZdCR+vb5S8Nn8mQCeS9BdSalYJqMBjAGzq59xAESN7VFVUgV8AZB/dZBR7QTFDCqGquvUBVVoEtgIwpQRzmANSFHgWQKExHdIrPeuMvQNDarXe6nC/AutgV3JW+6bgqQLeV8FekRtgV+ToDKEKnACYKsfZjjkam7a0ZpYTytwmgainpC3HvwBocgKOxqRjehoR9DFKNFYtOwCGYCszobeCbl26N6yyQ6g8X/Wex/rBPsNEV6qAMaJPMynIHQCoSqS9JSMmwef51LflTgCRszU7DvAGiV6mHWfsaVUAAAAASUVORK5CYII=), auto;
}
.mtr-2 {
margin-top: 2rem;
}
</style>
<title>Bot activation</title>
</head>
<body>
{% if activated %}
<div class="center large-center-text">
<div>
Hi, there! The bot is running.
</div>
<div>
Just write a ".h" to <a href="tg://resolve?domain={{username}}">@{{username}}</a> in telegram.
</div>
<div class="mtr-2">
If you want to raise an issue or request feature,
</div>
<div>
you can ask <a href="tg://resolve?domain={{username}}">me</a> for that
</div>
</div>
{%else%}
<div class="center large-center-text">
<div>
Welcome back, {{ username }}!
</div>
<dialog class="nes-dialog is-error" id="dialog-error">
<form method="dialog">
<p class="title">Failed to authorize.</p>
<p id="error_description"></p>
<menu class="dialog-menu">
<button class="nes-btn is-primary">Confirm</button>
</menu>
</form>
</dialog>
<dialog class="nes-dialog" id="dialog-success">
<form method="dialog">
<p class="title">Successfully authorized</p>
<p>You were successfully authorized.</p>
<menu class="dialog-menu">
<button class="nes-btn is-primary" onclick="location.reload()">Confirm</button>
</menu>
</form>
</dialog>
<form id="code_form" action="/code" method="post" class="mtr-2" autocomplete="off">
<div class="nes-field is-inline">
<label for="code_field">Verification code</label>
<input type="text" id="code_field" class="nes-input is-success" name="code" placeholder="Your code">
</div>
<div class="nes-field is-inline mt-15">
<label for="pass_field">Password</label>
<input type="password" id="pass_field" class="nes-input is-success" name="password"
placeholder="Password">
</div>
<div class="nes-field mt-15">
<input type="submit" class="nes-input is-success" value="Submit code">
</div>
</form>
</div>
{% endif %}
<img src="/static/images/girl_{{image_num}}.png" alt="" class="at_bottom">
</body>
{% if !activated %}
<script>
function authorize(form) {
// Bind the FormData object and the form element
const form_data = new FormData(form);
// Construct request JSON.
let request_json = {};
form_data.forEach((value, key) => {
request_json[key] = value;
});
fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request_json),
}).then(async response => {
if (response.status == 200) {
document.getElementById('dialog-success').showModal();
return;
}
let desc = document.getElementById('error_description');
desc.innerText = await response.text();
document.getElementById('dialog-error').showModal();
});
}
const form = document.getElementById("code_form");
// ...and take over its submit event.
form.addEventListener("submit", function (event) {
event.preventDefault();
authorize(form);
});
</script>
{% endif %}
</html>

View File

@ -0,0 +1,9 @@
use askama::Template;
#[derive(Template)]
#[template(path = "index.html")]
pub struct Index {
pub activated: bool,
pub username: String,
pub image_num: u8,
}

139
src/utils/inter_join.rs Normal file
View File

@ -0,0 +1,139 @@
use std::iter::Peekable;
use rand::{seq::SliceRandom, Rng};
/// Trait for creating random intersperse.
///
/// It's implemented for all iterables.
///
/// The usage is the following:
/// ```
/// let words = &[4, 5, 6];
/// let result = [1, 2, 3]
/// .into_iter()
/// .random_itersperse(words, &mut rand::thread_rng())
/// .collect::<Vec<_>>();
/// ```
///
/// Now if you print the result, you'll see that
/// after every word from the source slice,
/// placed one word from the `words` slice.
/// Like this: `[1, 6, 2, 4, 3]`.
pub trait RandomIntersperse<'a, L, R>
where
L: IntoIterator,
R: Rng,
{
fn random_itersperse(
self,
choises: &'a [L::Item],
random: &'a mut R,
) -> RandomIntersperseStruct<'a, L, R>;
}
/// Struct used to create `random_intresperse`.
/// It has a peekable iterator, a reference to a
/// random generator, a slice for items to choose from
/// and boolean to check current state.
///
/// The iterator is peekable, because we need to check
/// if next item exists, to avoid inserting
/// a generated value at the end of iterator.
#[derive(Debug)]
pub struct RandomIntersperseStruct<'a, L, R>
where
L: IntoIterator,
R: Rng,
{
iterator: Peekable<L::IntoIter>,
choices: &'a [L::Item],
random: &'a mut R,
use_iter: bool,
}
/// Implement a `RandomIntersperse` trait for all
/// items that can be turned in iterators.
impl<'a, L, R> RandomIntersperse<'a, L, R> for L
where
L: IntoIterator,
R: Rng,
{
fn random_itersperse(
self,
choices: &'a [L::Item],
random: &'a mut R,
) -> RandomIntersperseStruct<'a, L, R> {
RandomIntersperseStruct {
random,
choices,
use_iter: true,
iterator: self.into_iter().peekable(),
}
}
}
/// Implementation of an interator for a randomitersperse structure,
/// so it can be used in chain.
impl<'a, L, R> Iterator for RandomIntersperseStruct<'a, L, R>
where
L: IntoIterator,
R: Rng,
L::Item: Clone,
{
// The type of item is the same as for
// original iterator.
type Item = L::Item;
fn next(&mut self) -> Option<Self::Item> {
// Peek a value from the iterator to check if we have values.
self.iterator.peek()?;
let choise = self.choices.choose(self.random);
if choise.is_none() {
self.use_iter = true;
}
if self.use_iter {
// We change use_iter, so a random
// value is chosen on the next step.
self.use_iter = false;
self.iterator.next()
} else {
self.use_iter = true;
self.choices.choose(self.random).cloned()
}
}
}
#[cfg(test)]
mod tests {
use crate::utils::inter_join::RandomIntersperse;
#[test]
pub fn success() {
let randoms = &[4, 5, 6];
let result = [1, 2, 3]
.into_iter()
.random_itersperse(randoms, &mut rand::thread_rng())
.collect::<Vec<_>>();
for value in [1, 2, 3] {
assert!(result.contains(&value));
}
assert_eq!(result.len(), 5);
}
#[test]
pub fn empty_array() {
let randoms = &[];
let result = [1, 2, 3]
.into_iter()
.random_itersperse(randoms, &mut rand::thread_rng())
.collect::<Vec<_>>();
for value in [1, 2, 3] {
assert!(result.contains(&value));
}
assert_eq!(result.len(), 3);
}
}

9
src/utils/messages.rs Normal file
View File

@ -0,0 +1,9 @@
use grammers_client::{types::Message, Update};
#[must_use]
pub fn get_message(update: &Update) -> Option<&Message> {
match update {
Update::NewMessage(msg) | Update::MessageEdited(msg) => Some(msg),
_ => None,
}
}

2
src/utils/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod inter_join;
pub mod messages;

Binary file not shown.

BIN
static/images/girl_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
static/images/girl_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
static/images/girl_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

11
static/nes.min.css vendored Normal file

File diff suppressed because one or more lines are too long