v0.2.0
This commit is contained in:
commit
0197f80dfd
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
3091
Cargo.lock
generated
Normal file
3091
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "matrix-lol"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
dirs = "5.0.0"
|
||||
matrix-sdk = "0.6.2"
|
||||
rand = "0.8.5"
|
||||
regex = "1.7.3"
|
||||
reqwest = "0.11.16"
|
||||
rpassword = "7.2.0"
|
||||
serde = "1.0.159"
|
||||
serde_json = "1.0.95"
|
||||
soup = "0.5.1"
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
url = "2.2.2"
|
70
src/http.rs
Normal file
70
src/http.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use reqwest::Client;
|
||||
use soup::prelude::*;
|
||||
use regex::Regex;
|
||||
|
||||
pub async fn random_words(quantity: i8) -> Vec<String> {
|
||||
let client = Client::new();
|
||||
|
||||
let res = client.post("https://randommer.io/word-generator")
|
||||
.body(format!("quantity={quantity}&wordType=0"))
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
|
||||
.header("Accept-Language", "en-US")
|
||||
.header("Content-Type","application/x-www-form-urlencoded")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
serde_json::from_str(res.text().await.unwrap().as_str()).unwrap()
|
||||
}
|
||||
|
||||
pub async fn ddg_scrape(query: String) -> Vec<String> {
|
||||
let client = Client::new();
|
||||
|
||||
let res = client.get(format!("https://duckduckgo.com/html?q={query}"))
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
|
||||
.header("Accept-Language", "en-US")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let html_text = res.text().await.unwrap();
|
||||
let soup = Soup::new(&html_text);
|
||||
|
||||
soup.tag("a").class("result__url").find_all().map(|a| a.get("href").unwrap()).collect()
|
||||
}
|
||||
|
||||
pub async fn get_content(link: String) -> String {
|
||||
let client = Client::new();
|
||||
|
||||
let res = client.get(&link)
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
|
||||
.header("Accept-Language", "en-US")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let res = match res {
|
||||
Ok(r) => r,
|
||||
Err(e) => { eprintln!("get_content: {e:?}"); return String::new(); }
|
||||
};
|
||||
|
||||
let html_text = res.text().await.unwrap();
|
||||
let soup = Soup::new(&html_text);
|
||||
|
||||
let mut p: Vec<String> = soup.tag("p").find_all().map(|a| a.text()).collect();
|
||||
let mut span: Vec<String> = soup.tag("span").find_all().map(|a| a.text()).collect();
|
||||
p.append(&mut span);
|
||||
|
||||
let mut texts: Vec<String> = vec![];
|
||||
|
||||
let whitespace_re = Regex::new("[\n\r\t]").unwrap();
|
||||
let tag_re = Regex::new("(<)(/)?[\\w](>)").unwrap();
|
||||
for e in p {
|
||||
let untagged = tag_re.replace_all(&e, "").to_string();
|
||||
let normalwhite = whitespace_re.replace_all(&untagged, "\n").to_string();
|
||||
texts.push(normalwhite);
|
||||
}
|
||||
|
||||
let mut joined = texts.join(" ");
|
||||
joined = format!("From: {link}\n\n{joined}");
|
||||
unsafe { joined.slice_unchecked(0, std::cmp::min(8192, joined.len())).to_string() }
|
||||
}
|
75
src/main.rs
Normal file
75
src/main.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use matrix_sdk::{
|
||||
room::Room,
|
||||
ruma::events::room::message::{
|
||||
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent
|
||||
},
|
||||
Error, LoopCtrl,
|
||||
};
|
||||
|
||||
pub mod matrix;
|
||||
pub mod http;
|
||||
|
||||
async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
|
||||
if let Room::Joined(room) = room {
|
||||
let MessageType::Text(text_content) = event.content.msgtype else {
|
||||
return;
|
||||
};
|
||||
|
||||
if text_content.body.contains("vcxz pls spam") {
|
||||
let random_words = http::random_words(5).await;
|
||||
println!("Spamming for {}: \"{}\"", event.sender, random_words.join(" "));
|
||||
|
||||
let content = RoomMessageEventContent::text_plain(format!("{}: Ok, searching for \"{}\"", event.sender, random_words.join(" ")));
|
||||
room.send(content, None).await.unwrap();
|
||||
|
||||
let links = http::ddg_scrape(random_words.join(" ")).await;
|
||||
let mut count = 0;
|
||||
for link in links {
|
||||
if count > 5 { break };
|
||||
|
||||
let content = http::get_content(link).await;
|
||||
|
||||
if content.len() < 128 { continue };
|
||||
|
||||
let msg = RoomMessageEventContent::notice_plain(&content);
|
||||
match room.send(msg, None).await {
|
||||
Ok(_r) => { count += 1; },
|
||||
Err(e) => eprintln!("Failed to send message: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.expect("no data_dir directory found")
|
||||
.join("vcxz_bot");
|
||||
let session_file = data_dir.join("session");
|
||||
|
||||
let (client, sync_token) = if session_file.exists() {
|
||||
matrix::restore_session(&session_file).await?
|
||||
} else {
|
||||
(matrix::login(&data_dir, &session_file).await?, None)
|
||||
};
|
||||
|
||||
let (client, sync_settings) = matrix::sync(client, sync_token)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client.add_event_handler(on_room_message);
|
||||
|
||||
client.sync_with_result_callback(sync_settings, |sync_result| async move {
|
||||
let response = sync_result?;
|
||||
|
||||
matrix::persist_sync_token(response.next_batch)
|
||||
.await
|
||||
.map_err(|err| Error::UnknownError(err.into()))?;
|
||||
|
||||
Ok(LoopCtrl::Continue)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
194
src/matrix.rs
Normal file
194
src/matrix.rs
Normal file
@ -0,0 +1,194 @@
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings, ruma::api::client::filter::FilterDefinition, Client, Session,
|
||||
};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ClientSession {
|
||||
homeserver: String,
|
||||
db_path: PathBuf,
|
||||
passphrase: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FullSession {
|
||||
client_session: ClientSession,
|
||||
user_session: Session,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sync_token: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn restore_session(session_file: &Path) -> anyhow::Result<(Client, Option<String>)> {
|
||||
let serialized_session = fs::read_to_string(session_file).await?;
|
||||
let FullSession {
|
||||
client_session,
|
||||
user_session,
|
||||
sync_token,
|
||||
} = serde_json::from_str(&serialized_session)?;
|
||||
|
||||
let client = Client::builder()
|
||||
.homeserver_url(client_session.homeserver)
|
||||
.sled_store(client_session.db_path, Some(&client_session.passphrase))
|
||||
.unwrap()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
println!("[*] Restoring session for {}…", user_session.user_id);
|
||||
|
||||
client.restore_login(user_session).await?;
|
||||
|
||||
Ok((client, sync_token))
|
||||
}
|
||||
|
||||
pub async fn login(data_dir: &Path, session_file: &Path) -> anyhow::Result<Client> {
|
||||
println!("[*] No previous session found, logging in…");
|
||||
|
||||
let (client, client_session) = build_client(data_dir).await?;
|
||||
|
||||
loop {
|
||||
let username = rpassword::prompt_password("User> ").unwrap();
|
||||
let password = rpassword::prompt_password("Password> ").unwrap();
|
||||
|
||||
match client
|
||||
.login_username(&username, &password)
|
||||
.initial_device_display_name("vcxz bot")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("[*] Logged in as {username}");
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
println!("[!] Error logging in: {error}");
|
||||
println!("[!] Please try again\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let user_session = client
|
||||
.session()
|
||||
.expect("A logged-in client should have a session");
|
||||
let serialized_session = serde_json::to_string(&FullSession {
|
||||
client_session,
|
||||
user_session,
|
||||
sync_token: None,
|
||||
})?;
|
||||
fs::write(session_file, serialized_session).await?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Build a new client.
|
||||
pub async fn build_client(data_dir: &Path) -> anyhow::Result<(Client, ClientSession)> {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
let db_subfolder: String = (&mut rng)
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let db_path = data_dir.join(db_subfolder);
|
||||
|
||||
let passphrase: String = (&mut rng)
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
loop {
|
||||
let mut homeserver = String::new();
|
||||
|
||||
print!("Homeserver> ");
|
||||
io::stdout().flush().expect("Unable to write to stdout");
|
||||
io::stdin()
|
||||
.read_line(&mut homeserver)
|
||||
.expect("Unable to read user input");
|
||||
|
||||
println!("\n[*] Checking homeserver…");
|
||||
|
||||
match Client::builder()
|
||||
.homeserver_url(&homeserver)
|
||||
.sled_store(&db_path, Some(&passphrase))
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
{
|
||||
Ok(client) => {
|
||||
return Ok((
|
||||
client,
|
||||
ClientSession {
|
||||
homeserver,
|
||||
db_path,
|
||||
passphrase,
|
||||
},
|
||||
))
|
||||
}
|
||||
Err(error) => match &error {
|
||||
matrix_sdk::ClientBuildError::AutoDiscovery(_)
|
||||
| matrix_sdk::ClientBuildError::Url(_)
|
||||
| matrix_sdk::ClientBuildError::Http(_) => {
|
||||
println!("[!] Error checking the homeserver: {error}");
|
||||
println!("[!] Please try again\n");
|
||||
}
|
||||
_ => {
|
||||
return Err(error.into());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sync<'a>(
|
||||
client: Client,
|
||||
initial_sync_token: Option<String>,
|
||||
) -> anyhow::Result<(Client, SyncSettings<'a>)> {
|
||||
println!("[*] Initial sync...");
|
||||
|
||||
let filter = FilterDefinition::empty();
|
||||
|
||||
let mut sync_settings = SyncSettings::default().filter(filter.into());
|
||||
|
||||
if let Some(sync_token) = initial_sync_token {
|
||||
sync_settings = sync_settings.token(sync_token);
|
||||
}
|
||||
|
||||
loop {
|
||||
match client.sync_once(sync_settings.clone()).await {
|
||||
Ok(response) => {
|
||||
sync_settings = sync_settings.token(response.next_batch.clone());
|
||||
persist_sync_token(response.next_batch).await?;
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
println!("[!] An error occurred during initial sync: {error}");
|
||||
println!("[!] Trying again…");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] The bot is ready!");
|
||||
|
||||
Ok((client, sync_settings))
|
||||
}
|
||||
|
||||
pub async fn persist_sync_token(sync_token: String) -> anyhow::Result<()> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.expect("no data_dir directory found")
|
||||
.join("vcxz_bot");
|
||||
let session_file = data_dir.join("session");
|
||||
|
||||
let serialized_session = fs::read_to_string(&session_file).await?;
|
||||
let mut full_session: FullSession = serde_json::from_str(&serialized_session)?;
|
||||
|
||||
full_session.sync_token = Some(sync_token);
|
||||
let serialized_session = serde_json::to_string(&full_session)?;
|
||||
fs::write(session_file, serialized_session).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue
Block a user