3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
.env
|
||||||
|
*.session
|
@ -6,8 +6,12 @@
|
|||||||
# Note that environment variables can be set in several places
|
# Note that environment variables can be set in several places
|
||||||
# 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
61
.pre-commit-config.yaml
Normal 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
2307
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
Normal file
28
Cargo.toml
Normal 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
5
askama.toml
Normal 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
96
src/args.rs
Normal 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
17
src/bot/filters/base.rs
Normal 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
39
src/bot/filters/chain.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
1
src/bot/filters/groups.rs
Normal file
1
src/bot/filters/groups.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
74
src/bot/filters/message_fitlers.rs
Normal file
74
src/bot/filters/message_fitlers.rs
Normal 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
3
src/bot/filters/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod base;
|
||||||
|
pub mod chain;
|
||||||
|
pub mod message_fitlers;
|
10
src/bot/handlers/base.rs
Normal file
10
src/bot/handlers/base.rs
Normal 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);
|
17
src/bot/handlers/basic/get_chat_id.rs
Normal file
17
src/bot/handlers/basic/get_chat_id.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
17
src/bot/handlers/basic/help.rs
Normal file
17
src/bot/handlers/basic/help.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
2
src/bot/handlers/basic/mod.rs
Normal file
2
src/bot/handlers/basic/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod get_chat_id;
|
||||||
|
pub mod help;
|
39
src/bot/handlers/fun/blyaficator.rs
Normal file
39
src/bot/handlers/fun/blyaficator.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
31
src/bot/handlers/fun/greeter.rs
Normal file
31
src/bot/handlers/fun/greeter.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
2
src/bot/handlers/fun/mod.rs
Normal file
2
src/bot/handlers/fun/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod blyaficator;
|
||||||
|
pub mod greeter;
|
5
src/bot/handlers/mod.rs
Normal file
5
src/bot/handlers/mod.rs
Normal 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
131
src/bot/main.rs
Normal 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
6
src/bot/mod.rs
Normal 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
1
src/bot/utils.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
27
src/logging.rs
Normal file
27
src/logging.rs
Normal 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
50
src/main.rs
Normal 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
29
src/server/main.rs
Normal 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
6
src/server/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
mod main;
|
||||||
|
mod routes;
|
||||||
|
mod schema;
|
||||||
|
mod templates;
|
||||||
|
|
||||||
|
pub use main::start;
|
49
src/server/routes.rs
Normal file
49
src/server/routes.rs
Normal 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
7
src/server/schema.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct LoginRequestDTO {
|
||||||
|
pub code: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
181
src/server/templates/index.html
Normal file
181
src/server/templates/index.html
Normal 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>
|
9
src/server/templates/mod.rs
Normal file
9
src/server/templates/mod.rs
Normal 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
139
src/utils/inter_join.rs
Normal 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
9
src/utils/messages.rs
Normal 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
2
src/utils/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod inter_join;
|
||||||
|
pub mod messages;
|
BIN
static/fonts/ps2p_latin.woff2
Normal file
BIN
static/fonts/ps2p_latin.woff2
Normal file
Binary file not shown.
BIN
static/images/girl_0.png
Normal file
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
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
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
11
static/nes.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user