Initial commit
This commit is contained in:
22
.gitignore
vendored
Executable file
22
.gitignore
vendored
Executable file
@@ -0,0 +1,22 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/rust
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
|
||||
|
||||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust
|
||||
|
||||
config.toml
|
||||
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "rust_vk_stats"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
colored = "2.1.0"
|
||||
mysql_async = { version = "0.34.1", features = ["chrono"]}
|
||||
reqwest = {version = "0.12.2", features = ["json"]}
|
||||
serde = { version = "1.0.97", features = ["derive"] }
|
||||
serde_json = "1.0.115"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
toml = "0.8.12"
|
||||
203
src/main.rs
Executable file
203
src/main.rs
Executable file
@@ -0,0 +1,203 @@
|
||||
use colored::Colorize;
|
||||
use mysql_async::{prelude::*, Pool, Transaction};
|
||||
use std::time::Instant;
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use structs::*;
|
||||
pub mod structs;
|
||||
|
||||
// Construct URL for API access
|
||||
fn api_url(method: &str, params: Vec<&str>, config: &Config) -> String {
|
||||
let mut url: String = "https://api.vk.com/method/".to_string() + method + "?";
|
||||
for param in params {
|
||||
url = url + "&" + param;
|
||||
}
|
||||
url + "&v=" + &config.vk_version + "&access_token=" + &config.access_token
|
||||
}
|
||||
|
||||
// Parse config from config.toml
|
||||
fn parse_config() -> Result<Config, String> {
|
||||
let config_contents =
|
||||
fs::read_to_string("config.toml").expect("Couldn't read config file. Aborting! :(");
|
||||
let deserialized = toml::from_str::<TomlFile>(&config_contents);
|
||||
if deserialized.is_err() {
|
||||
return Err("wrong_config".to_string());
|
||||
}
|
||||
let yaml: TomlFile = deserialized.unwrap();
|
||||
let config = yaml.config;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
async fn db_connect(config: &Config) -> Result<Pool, Box<dyn Error>> {
|
||||
let database_url = "mysql://".to_string()
|
||||
+ &config.database_username
|
||||
+ ":"
|
||||
+ &config.database_password
|
||||
+ "@"
|
||||
+ &config.database_hostname
|
||||
+ ":"
|
||||
+ &config.database_port
|
||||
+ "/"
|
||||
+ &config.database_name;
|
||||
let pool = mysql_async::Pool::new(database_url.as_str());
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
// Write array to database
|
||||
async fn write_arr_to_db(transaction: &mut Transaction<'_>, arr: &Vec<VkMessage>) -> Result<(), Box<dyn Error>> {
|
||||
let start = Instant::now();
|
||||
transaction.exec_batch(
|
||||
r"INSERT INTO messages (time, id, sender, message, sticker) VALUES (:time, :id, :sender, :message, :sticker)",
|
||||
arr.iter().map(|p| {
|
||||
params! {
|
||||
"time" => p.date,
|
||||
"id" => p.id,
|
||||
"sender" => p.from_id,
|
||||
"message" => p.text.clone(),
|
||||
"sticker" => {
|
||||
if p.attachments.len() > 0 && p.attachments[0].sticker.is_some() {
|
||||
p.attachments[0].sticker.as_ref().unwrap().sticker_id
|
||||
} else {
|
||||
0
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let duration = start.elapsed();
|
||||
println!("Time elapsed in write_arr_to_db() is: {:?}", duration);
|
||||
println!("{}", "Inserted into DB!".green());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Clear database and start anew
|
||||
async fn rebuild(pool: &mut Pool, config: &Config) -> Result<(), Box<dyn Error>> {
|
||||
let mut conn = pool.get_conn().await?;
|
||||
let result = conn.query_drop("DROP TABLE messages").await;
|
||||
if result.is_err() {
|
||||
println!(
|
||||
"{}",
|
||||
"Couldn't delete table. Doesn't exist? Proceeding..."
|
||||
.yellow()
|
||||
.bold()
|
||||
);
|
||||
} else {
|
||||
println!("{}", "Dropped messages table.".red().bold());
|
||||
}
|
||||
|
||||
conn.query_drop(
|
||||
"CREATE TABLE IF NOT EXISTS messages
|
||||
(n MEDIUMINT NOT NULL AUTO_INCREMENT,
|
||||
time INT, id INT, sender INT, message TEXT,
|
||||
sticker INT,
|
||||
sticker_url TEXT,
|
||||
CONSTRAINT n PRIMARY KEY (n))",
|
||||
)
|
||||
.await?;
|
||||
println!("{}", "Recreated table.".green());
|
||||
drop(conn);
|
||||
let count: i64;
|
||||
loop {
|
||||
let result = reqwest::get(api_url(
|
||||
"messages.getHistory",
|
||||
vec!["count=200", &("peer_id=".to_owned() + &config.peer_id)],
|
||||
config,
|
||||
))
|
||||
.await?
|
||||
.json::<VkResponse>()
|
||||
.await;
|
||||
if result.is_ok() {
|
||||
count = result.unwrap().response.count;
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(340)).await;
|
||||
}
|
||||
println!("{}", count);
|
||||
let conn = &mut pool.get_conn().await?;
|
||||
let mut transaction = conn.start_transaction(mysql_async::TxOpts::new()).await?;
|
||||
for offset in (0..count).step_by(200) {
|
||||
let start = Instant::now();
|
||||
let unwrapped_result: Option<VkResponse>;
|
||||
loop {
|
||||
let result = reqwest::get(api_url(
|
||||
"messages.getHistory",
|
||||
vec![
|
||||
"count=200",
|
||||
&format!("offset={}", offset),
|
||||
&("peer_id=".to_owned() + &config.peer_id),
|
||||
"rev=1",
|
||||
],
|
||||
config,
|
||||
))
|
||||
.await?
|
||||
.json::<VkResponse>()
|
||||
.await;
|
||||
if result.is_err() {
|
||||
continue;
|
||||
}
|
||||
println!("{}", format!("Processed {}", offset).green());
|
||||
unwrapped_result = Some(result.unwrap());
|
||||
break;
|
||||
}
|
||||
println!("Writing to DB");
|
||||
|
||||
write_arr_to_db(
|
||||
&mut transaction,
|
||||
&unwrapped_result.unwrap().response.items,
|
||||
)
|
||||
.await?;
|
||||
let duration = start.elapsed();
|
||||
print!("Time elapsed in main FOR is: {:?}", duration);
|
||||
if duration.as_millis() < 340 {
|
||||
let new_dur = tokio::time::Duration::from_millis(340) - duration;
|
||||
println!(", so sleeping for {}ms", new_dur.as_millis());
|
||||
tokio::time::sleep(new_dur).await;
|
||||
} else {print!("\n");}
|
||||
}
|
||||
transaction.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli = clap::Args::parse();
|
||||
println!(
|
||||
"{}",
|
||||
"VK dialogue statistics collector initializing..."
|
||||
.yellow()
|
||||
.bold()
|
||||
);
|
||||
println!("{}", "Parsing config from config.yml.".yellow().bold());
|
||||
let config = parse_config();
|
||||
if config.is_err() {
|
||||
println!(
|
||||
"{}",
|
||||
"Error parsing config. Please check if it's correct! Shutting down :("
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let config = config.unwrap();
|
||||
let pool = db_connect(&config).await;
|
||||
let mut pool = pool.expect("Error while connecting to DB. Shutting down :(");
|
||||
println!(
|
||||
"{}",
|
||||
"Connected to MYSQL database successfully!".green().bold()
|
||||
);
|
||||
|
||||
if cli.rebuild > 0 {
|
||||
println!("{}", "Rebuilding mode.".yellow());
|
||||
rebuild(&mut pool, &config).await?;
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
"No argument specified. Running in usual mode.".yellow()
|
||||
);
|
||||
}
|
||||
|
||||
pool.disconnect().await?;
|
||||
Ok(())
|
||||
}
|
||||
58
src/structs.rs
Normal file
58
src/structs.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use clap::{command, Parser};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct TomlFile {
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
pub peer_id: String,
|
||||
pub access_token: String,
|
||||
pub vk_version: String,
|
||||
pub database_hostname: String,
|
||||
pub database_port: String,
|
||||
pub database_username: String,
|
||||
pub database_password: String,
|
||||
pub database_password_root: String,
|
||||
pub database_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VkResponse {
|
||||
pub response: VkResp,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VkResp {
|
||||
pub count: i64,
|
||||
pub items: Vec<VkMessage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VkMessage {
|
||||
pub date: i64,
|
||||
pub from_id: i64,
|
||||
pub id: i64,
|
||||
pub text: String,
|
||||
pub peer_id: i64,
|
||||
pub attachments: Vec<Attachment>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Attachment {
|
||||
pub sticker: Option<Sticker>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Sticker {
|
||||
pub sticker_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
pub rebuild: u8,
|
||||
}
|
||||
Reference in New Issue
Block a user