22 Commits

Author SHA1 Message Date
7432ce6398 Bump many libs 2021-08-19 18:08:19 +03:00
AB
0831e3f503 Add хере command. 2021-06-12 15:02:09 +03:00
AB
a0f4c40be0 Add хере command. 2021-06-12 14:58:38 +03:00
AB
1facef6897 Add debug message. 2021-04-04 22:35:33 +03:00
AB
428416a2a3 Add handler in case of error in here command. 2021-03-10 19:57:45 +03:00
AB
77dec205f1 Bump version. 2021-01-20 20:16:08 +03:00
AB
6c761d7576 @here command now call only active users (at least 1 message in last 60 days) 2021-01-20 20:15:46 +03:00
AB
865fd3bbe4 Bump 2021-01-20 15:54:05 +03:00
AB
30bdb23a32 Add @here command 2021-01-20 15:53:22 +03:00
AB
f97562e9b7 Merge remote-tracking branch 'origin/main' into main 2021-01-11 11:39:34 +03:00
AB
2d000101c2 Merge 2021-01-11 11:39:20 +03:00
a26d227190 Merge pull request #10 from house-of-vanity/code
code
2021-01-11 11:26:51 +03:00
AB
cc44f0e23b Merge remote-tracking branch 'origin/main' into main
# Conflicts:
#	assets/help_text.rs
2021-01-11 11:22:47 +03:00
AB
96df636195 Add automerge action. 2021-01-11 11:21:18 +03:00
AB
a48e25800c Improve logging. Fix /sql limit. 2021-01-11 11:12:38 +03:00
36660d384d Merge pull request #9 from house-of-vanity/code
Code
2021-01-10 21:39:38 +03:00
AB
0d24976ec2 Fix warnings. Bump version. 2021-01-10 21:38:18 +03:00
AB
6ae3b2af1f Fix warnings. 2021-01-10 21:22:27 +03:00
AB
a39f6a8c2a Update /code feature. 2021-01-10 21:21:07 +03:00
AB
788c2cbbd4 Fix type in help. 2021-01-09 01:06:10 +03:00
AB
9d5e5a3217 Fix type in help. 2021-01-08 17:54:05 +03:00
945da05794 Update README 2021-01-08 06:42:32 -08:00
8 changed files with 153 additions and 84 deletions

27
.github/workflows/automerge.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
check_suite:
types:
- completed
status: {}
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- name: automerge
uses: "pascalgn/automerge-action@v0.13.0"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -1,40 +1,41 @@
[package] [package]
name = "desubot" name = "desubot"
version = "0.5.0" version = "0.5.5"
authors = ["AB <ab@hexor.ru>"] authors = ["AB <ab@hexor.ru>"]
edition = "2018" edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
bytes = "0.5" bytes = "0.5"
tokio = { version = "0.2", features = ["full"]} tokio = { version = "1.10.0", features = ["full"]}
tracing = "0.1.9" tracing = "0.1.9"
tracing-futures = "0.2" tracing-futures = "0.2"
multipart = { version = "0.16", default-features = false, features = ["client"] } multipart = { version = "0.16", default-features = false, features = ["client"] }
telegram-bot = "0.8.0" telegram-bot = "0.8.0"
silicon = "0.4.0" silicon = "0.4.0"
hyper = "0.13" hyper = "0.14"
hyper-tls = { version = "0.4", optional = true } hyper-tls = { version = "0.4", optional = true }
futures = "0.3" futures = "0.3"
hyper-rustls = { version = "0.19", optional = true } hyper-rustls = { version = "0.19", optional = true }
rusqlite = { version = "0.24.2", features = ["bundled"]} rusqlite = { version = "0.24.2", features = ["bundled"]}
html-escape = "0.2" html-escape = "0.2"
regex = "1" regex = "1"
reqwest = "0.10.9" reqwest = "0.11.4"
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }
sha1 = "0.6.0" sha1 = "0.6.0"
env_logger = "0.7" env_logger = "0.9.0"
log = { version = "^0.4.5", features = ["std"] } log = { version = "^0.4.5", features = ["std"] }
subprocess = "0.2.6" subprocess = "0.2.6"
serde_json = "1.0" serde_json = "1.0"
markov = "1.1.0" markov = "1.1.0"
rand = "0.7.3" rand = "0.7.3"
mystem = "0.2.1" mystem = "^0.2"
async-trait = "0.1.42" #mystem = { path = "../mystem-rs" }
sqlparser = "0.7.0" async-trait = "0.1.42"
sqlparser = "0.9.0"
[dependencies.syntect]
version = "4.4" [dependencies.syntect]
default-features = false version = "4.4"
features = ["parsing", "dump-load", "regex-onig"] default-features = false
features = ["parsing", "dump-load", "regex-onig"]

4
README
View File

@ -7,10 +7,8 @@ Telegram bot with light group statistic and heavy spy features.
* /here command to mention all members. * /here command to mention all members.
* Alongside with saving whole message bot perform blacklist filter and stemming for every word (only Russian). "Красивую собаку мыли негры" -> "красивый собака мыть негр" * Alongside with saving whole message bot perform blacklist filter and stemming for every word (only Russian). "Красивую собаку мыли негры" -> "красивый собака мыть негр"
* Generate sentences using Markov Chains trained on history with /markov_all. * Generate sentences using Markov Chains trained on history with /markov_all.
== TODO ==
* Syntax highlighting for CODE exported to image. * Syntax highlighting for CODE exported to image.
== Important == == Important ==
* Desubot uses MyStem by Yandex for word stemming and assume that mystem binary is available in PATH. * Desubot uses MyStem by Yandex for word stemming and assume that mystem binary is available in PATH.
On Windows it may be placed on working directory. Both Linux and Windows mystem binary is in repo. On Windows it may be placed on working directory. Both Linux and Windows mystem binary is in repo.

View File

@ -1,10 +1,10 @@
static CODE: &str = "<b>Code highlighter</b> #[allow(dead_code)]
static CODE_HELP: &str = "<b>Code highlighter</b>
<i>Usage</i> <i>Usage</i>
<pre>/CODE <pre>/code
#&lt;theme - Dracula by default&gt;
&lt;CODE&gt; &lt;CODE&gt;
#&lt;lang - JS by default&gt;</pre> #&lt;lang - JS by default&gt; #&lt;theme - Dracula by default&gt;</pre>
Language may be defined by both name and extension - Rust, rs... Language may be defined by both name and extension - Rust, rs...
Max lines - 80 Max lines - 80
@ -33,4 +33,11 @@ gruvbox
gruvbox-light gruvbox-light
gruvbox-white gruvbox-white
zenburn zenburn
"; ";
#[allow(dead_code)]
pub static SQL_HELP: &str = "<b>Perform an SQL command</b>
<i>* Only one sentence per message.
* Only SELECT command.
* Max result length is 100 lines. Use LIMIT 100.
* SQLITE syntax is available only.</i>";

View File

@ -17,11 +17,13 @@ use sqlparser::ast::Statement;
use sqlparser::dialect::GenericDialect; use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser; use sqlparser::parser::Parser;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::Theme;
use syntect::parsing::SyntaxReference; use syntect::parsing::SyntaxReference;
use syntect::util::LinesWithEndings; use syntect::util::LinesWithEndings;
use telegram_bot::prelude::*; use telegram_bot::prelude::*;
use telegram_bot::{Api, Message, ParseMode}; use telegram_bot::{Api, Message, ParseMode, UserId};
use syntect::highlighting::Theme;
include!("../assets/help_text.rs");
pub struct Here { pub struct Here {
pub data: String, pub data: String,
@ -65,6 +67,10 @@ impl Execute for Sql {
async fn exec_with_result(&self, api: &Api, message: &Message) -> Result<String, Error> { async fn exec_with_result(&self, api: &Api, message: &Message) -> Result<String, Error> {
let mut sql = self.data.clone(); let mut sql = self.data.clone();
debug!("PIZDA - {}", sql);
if sql == "/sql" || sql == "/sql-" {
return Ok(SQL_HELP.to_string());
}
let is_head = if sql.starts_with('-') { let is_head = if sql.starts_with('-') {
sql = sql.replacen("-", "", 1); sql = sql.replacen("-", "", 1);
false false
@ -137,7 +143,7 @@ impl Execute for Sql {
} }
res.push(tmp); res.push(tmp);
} }
if res.len() > 100 { if res.len() >= 100 {
return Err(Error::SQLResultTooLong( return Err(Error::SQLResultTooLong(
"SQL result too long. Lines limit is 100. Use LIMIT".to_string(), "SQL result too long. Lines limit is 100. Use LIMIT".to_string(),
)); ));
@ -186,7 +192,15 @@ impl Execute for Sql {
#[async_trait] #[async_trait]
impl Execute for Here { impl Execute for Here {
async fn exec(&self, api: &Api, message: &Message) -> Result<(), Error> { async fn exec(&self, api: &Api, message: &Message) -> Result<(), Error> {
let members: Vec<telegram_bot::User> = db::get_members(message.chat.id()).unwrap(); let members: Vec<telegram_bot::User> =
db::get_members(message.chat.id(), 60).unwrap_or(vec![telegram_bot::User {
id: UserId::new(124317807),
first_name: "Ultradesu".to_string(),
last_name: None,
username: None,
is_bot: false,
language_code: None,
}]);
for u in &members { for u in &members {
debug!("Found user {:?} in chat {}", u, message.chat.id()); debug!("Found user {:?} in chat {}", u, message.chat.id());
} }
@ -551,11 +565,16 @@ impl Execute for Code {
unimplemented!() unimplemented!()
} }
async fn exec_with_result(&self, api: &Api, message: &Message) -> Result<String, Error> { async fn exec_with_result(&self, api: &Api, message: &Message) -> Result<String, Error> {
let mut lines: Vec<String> = self.data.trim().split("\n").map(|s| s.to_string()).collect(); let mut lines: Vec<String> = self
.data
.trim()
.split("\n")
.map(|s| s.to_string())
.collect();
if lines.len() >= 81 { if lines.len() >= 81 {
return Err(CodeHighlightningError); return Err(CodeHighlightningError);
} }
let last_line = &lines[lines.len()-1]; let last_line = &lines[lines.len() - 1];
let tags = last_line let tags = last_line
.trim() .trim()
@ -593,7 +612,6 @@ impl Execute for Code {
.map(|s| s.unwrap()) .map(|s| s.unwrap())
.collect(); .collect();
let theme = if theme.len() != 1 { let theme = if theme.len() != 1 {
ts.themes.get("Dracula").unwrap() ts.themes.get("Dracula").unwrap()
} else { } else {
@ -606,6 +624,7 @@ impl Execute for Code {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let formatter = silicon::formatter::ImageFormatterBuilder::<String>::new() let formatter = silicon::formatter::ImageFormatterBuilder::<String>::new()
.window_controls(false) .window_controls(false)
.line_offset(1)
.round_corner(false); .round_corner(false);
let mut formatter = formatter.build().unwrap(); let mut formatter = formatter.build().unwrap();
let image = formatter.format(&highlight, &theme); let image = formatter.format(&highlight, &theme);

View File

@ -180,18 +180,29 @@ pub(crate) async fn get_messages_user_all(
Ok(messages) Ok(messages)
} }
pub(crate) fn get_members(id: telegram_bot::ChatId) -> Result<Vec<telegram_bot::User>> { pub(crate) fn get_members(id: telegram_bot::ChatId, limit: u32) -> Result<Vec<telegram_bot::User>> {
let where_statement = if limit > 0 {
format!("and days_seen <= {}", limit)
} else {
"".into()
};
debug!("{}", where_statement);
let conn = open()?; let conn = open()?;
let mut stmt = conn.prepare_cached( let mut stmt = conn.prepare_cached(&format!(
" "
SELECT DISTINCT(u.username), u.id, u.first_name, u.last_name, u.date SELECT DISTINCT(u.username), u.id, u.first_name, u.last_name, u.date,
(strftime('%s','now')-r.date)/60/60/24 as days_seen
FROM relations r FROM relations r
JOIN user u JOIN user u
ON u.id = r.user_id ON u.id = r.user_id
LEFT JOIN conf c LEFT JOIN conf c
ON r.conf_id = c.id ON r.conf_id = c.id
WHERE c.id = :id", WHERE c.id = :id
)?; {}
GROUP BY u.id
ORDER BY r.date DESC",
where_statement
))?;
let mut rows = stmt.query_named(&[(":id", &id.to_string())])?; let mut rows = stmt.query_named(&[(":id", &id.to_string())])?;
let mut users = Vec::new(); let mut users = Vec::new();
@ -214,7 +225,7 @@ pub(crate) async fn add_conf(message: Message) -> Result<(), Error> {
match get_conf(message.chat.id()) { match get_conf(message.chat.id()) {
Ok(_) => { Ok(_) => {
//info!("Group found: {:?}", message.chat.id()); debug!("Group found: {:?}", message.chat.id());
let update = Conf { let update = Conf {
id: message.chat.id(), id: message.chat.id(),
title, title,
@ -228,10 +239,10 @@ pub(crate) async fn add_conf(message: Message) -> Result<(), Error> {
id = :id", id = :id",
)?; )?;
stmt.execute_named(&[(":id", &update.id.to_string()), (":title", &update.title)])?; stmt.execute_named(&[(":id", &update.id.to_string()), (":title", &update.title)])?;
//info!("Conf {:?} updated: {:?}", update.title, get_conf(update.id)); debug!("Conf {:?} updated: {:?}", update.title, get_conf(update.id));
} }
Err(_) => { Err(_) => {
//info!("Group didn't found: {:?}", message.chat.id()); debug!("Group didn't found: {:?}", message.chat.id());
let update = Conf { let update = Conf {
id: message.chat.id(), id: message.chat.id(),
@ -250,7 +261,7 @@ pub(crate) async fn add_conf(message: Message) -> Result<(), Error> {
(":title", &update.title), (":title", &update.title),
(":date", &unix_time), (":date", &unix_time),
])?; ])?;
//info!("Conf {:?} added: {:?}", update.title, get_conf(update.id)); debug!("Conf {:?} added: {:?}", update.title, get_conf(update.id));
} }
} }
Ok(()) Ok(())
@ -283,7 +294,11 @@ pub(crate) async fn add_user(message: Message) -> Result<(), Error> {
(":first_name", &update.first_name), (":first_name", &update.first_name),
(":last_name", &update.last_name), (":last_name", &update.last_name),
])?; ])?;
//println!("User {} updated: {:?}", update.first_name, get_user(user.id)); debug!(
"User {} updated: {:?}",
update.first_name,
get_user(update.id)
);
} }
Err(_) => { Err(_) => {
let unix_time = SystemTime::now() let unix_time = SystemTime::now()
@ -310,7 +325,7 @@ pub(crate) async fn add_user(message: Message) -> Result<(), Error> {
(":last_name", &user.last_name), (":last_name", &user.last_name),
(":date", &unix_time), (":date", &unix_time),
])?; ])?;
//println!("User added: {:?}", user); debug!("User added: {:?}", user);
} }
} }
Ok(()) Ok(())
@ -417,6 +432,7 @@ pub(crate) async fn add_sentence(
}; };
// Save stemmed words // Save stemmed words
debug!("Going to stem: {}", text);
let words = mystem.stemming(text)?; let words = mystem.stemming(text)?;
conn.execute("BEGIN TRANSACTION", params![]); conn.execute("BEGIN TRANSACTION", params![]);
for word in words { for word in words {

View File

@ -6,7 +6,6 @@ use crate::utils;
use mystem::MyStem; use mystem::MyStem;
use telegram_bot::*; use telegram_bot::*;
include!("../assets/help_text.rs"); include!("../assets/help_text.rs");
pub async fn handler( pub async fn handler(
@ -25,19 +24,18 @@ pub async fn handler(
title, title,
&message.from.id, &message.from.id,
&message.from.first_name, &message.from.first_name,
{if data.len() <= 200 {data} else {&data[..200]}}.replace("\n", " ") data.replace("\n", " ")
); );
let cleaned_message = data.replace(&format!("@{}", me.clone().username.unwrap()), ""); let cleaned_message = data.replace(&format!("@{}", me.clone().username.unwrap()), "");
match cleaned_message.as_str() { match cleaned_message.as_str() {
s if s.to_string().starts_with("/code") => { s if s.to_string().starts_with("/code") => {
match { match {
Code { Code {
data: s.replace("/code", ""), data: s.replace("/code", ""),
} }
.exec_with_result(&api, &message) .exec_with_result(&api, &message)
.await .await
} { } {
Ok(path) => { Ok(path) => {
let file = InputFileUpload::with_path(path.clone()); let file = InputFileUpload::with_path(path.clone());
@ -50,12 +48,12 @@ pub async fn handler(
} }
Err(_) => { Err(_) => {
let _ = api let _ = api
.send(message.text_reply(CODE).parse_mode(ParseMode::Html)) .send(message.text_reply(CODE_HELP).parse_mode(ParseMode::Html))
.await?; .await?;
} }
} }
} }
s if s.contains("/here") => { s if s.contains("/here") || s.contains("@here") || s.contains("/хере") || s.contains("@хере") || s.contains("\"хере") => {
db::add_sentence(&message, mystem).await?; db::add_sentence(&message, mystem).await?;
Here { Here {
data: "".to_string(), data: "".to_string(),

View File

@ -1,3 +1,4 @@
#![allow(unreachable_code)]
use std::{env, process}; use std::{env, process};
use futures::StreamExt; use futures::StreamExt;
@ -16,7 +17,7 @@ use mystem::MyStem;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), errors::Error> { async fn main() -> Result<(), errors::Error> {
env_logger::from_env(Env::default().default_filter_or("info")).init(); env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let mut mystem = match MyStem::new() { let mut mystem = match MyStem::new() {
Ok(mystem) => mystem, Ok(mystem) => mystem,
Err(e) => { Err(e) => {
@ -44,16 +45,18 @@ async fn main() -> Result<(), errors::Error> {
me.first_name, me.first_name,
me.id me.id
); );
while let Some(update) = stream.next().await { loop {
let update = update?; while let Some(update) = stream.next().await {
if let UpdateKind::Message(message) = update.kind { let update = update?;
db::add_conf(message.clone()).await?; if let UpdateKind::Message(message) = update.kind {
db::add_user(message.clone()).await?; db::add_conf(message.clone()).await?;
match handlers::handler(api.clone(), message, token.clone(), &mut mystem, me.clone()) db::add_user(message.clone()).await?;
.await match handlers::handler(api.clone(), message, token.clone(), &mut mystem, me.clone())
{ .await
Ok(_) => {} {
Err(e) => warn!("An error occurred handling command. {:?}", e), Ok(_) => {}
Err(e) => warn!("An error occurred handling command. {:?}", e),
}
} }
} }
} }