aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/hooks/mod.rs1
-rw-r--r--src/hooks/url.rs190
-rw-r--r--src/main.rs13
3 files changed, 198 insertions, 6 deletions
diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs
index c5d6e1d..924fe2e 100644
--- a/src/hooks/mod.rs
+++ b/src/hooks/mod.rs
@@ -7,6 +7,7 @@ pub mod intensify;
pub mod pet;
pub mod sed;
pub mod shifty_eyes;
+pub mod url;
pub use intensify::intensify;
pub use shifty_eyes::shifty_eyes;
diff --git a/src/hooks/url.rs b/src/hooks/url.rs
new file mode 100644
index 0000000..21f4b06
--- /dev/null
+++ b/src/hooks/url.rs
@@ -0,0 +1,190 @@
+use anyhow::{bail, Context, Error, Result};
+use irc::client::prelude::*;
+
+use regex::Regex;
+
+extern crate kuchiki;
+use kuchiki::{parse_html, traits::*};
+use reqwest::{get, Url};
+use tracing::trace;
+
+pub const URL_REGEX: &str = r#"(https?://|www.)\S+"#;
+
+#[tracing::instrument]
+pub fn url_parser(msg: &str) -> Vec<String> {
+ let url_regex = Regex::new(URL_REGEX).unwrap();
+
+ url_regex
+ .find_iter(msg)
+ .map(|u| u.as_str().to_string().replace("www.", "https://"))
+ .collect::<Vec<String>>()
+}
+
+#[tracing::instrument]
+pub async fn url_title(url: &str) -> Result<String, Error> {
+ let body = get(Url::parse(url).context("Failed to parse url")?)
+ .await
+ .context("Failed to make request")?
+ .text()
+ .await
+ .context("failed to get request response text")?;
+
+ let document = parse_html().one(body);
+ match document.select("title") {
+ Ok(title) => Ok(title
+ .into_iter()
+ .nth(0)
+ .context("title did not have text")?
+ .text_contents()),
+ Err(_) => bail!("could not find title"),
+ }
+}
+
+#[tracing::instrument(skip(bot))]
+pub async fn url_preview(bot: &crate::Bot, msg: Message) -> Result<()> {
+ if let Command::PRIVMSG(target, text) = msg.command.clone() {
+ let mut futures: Vec<tokio::task::JoinHandle<_>> = Vec::new();
+
+ for url in url_parser(&text) {
+ futures.push(tokio::spawn(async move {
+ trace!("got url: {:?}", url);
+ match url_title(&url.as_str()).await {
+ Ok(title) => {
+ trace!("extracted title from url: {:?}, {:?}", title, url);
+ Ok(title)
+ }
+ Err(err) => bail!("Failed to get urls title: {:?}", err),
+ }
+ }))
+ }
+
+ let titles = futures::future::join_all(futures).await;
+
+ let titles: Vec<String> = titles
+ .into_iter()
+ .filter_map(|x| x.ok())
+ .filter_map(|x| x.ok())
+ .collect();
+
+ if !titles.is_empty() {
+ bot.send_privmsg(&target, &msg_builder(&titles))?;
+ }
+ }
+ Ok(())
+}
+
+#[tracing::instrument]
+pub fn msg_builder(titles: &Vec<String>) -> String {
+ format!(
+ "Title{}: {}",
+ if titles.len() > 1 { "s" } else { "" },
+ titles.join(" --- ")
+ )
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::msg_builder;
+ use super::url_parser;
+ use super::url_title;
+ use mockito;
+
+ #[test]
+ fn test_url_titel() {
+ assert!(tokio_test::block_on(url_title(&mockito::server_url())).is_err());
+
+ let _m = mockito::mock("GET", "/")
+ .with_body(
+ r#"
+<html>
+ <head>
+ <title>This is test site</title>
+ </head>
+ <body>
+ <h1>some heading</h1>
+ </body>
+</html>
+"#,
+ )
+ .create();
+ mockito::start();
+
+ let title: String = tokio_test::block_on(url_title(&mockito::server_url())).unwrap();
+ assert_eq!(title.as_str(), "This is test site");
+ }
+ #[test]
+ fn test_url_parser() {
+ let url = url_parser("some message https://news.ycombinator.com/ here");
+ assert_eq!(url[0], "https://news.ycombinator.com/");
+
+ let url = url_parser("no url here!");
+ assert!(url.is_empty());
+
+ let url = url_parser(
+ &[
+ "https://new.ycombinator.com/ ",
+ "http://news.ycombinator.com/ ",
+ "www.google.com",
+ ]
+ .concat(),
+ );
+ assert_eq!(url.len(), 3);
+ }
+
+ #[test]
+ fn test_msg_builder() {
+ let msg = msg_builder(&Vec::from(["hello".to_string(), "world".to_string()]));
+ assert_eq!("Titles: hello --- world", msg);
+
+ let msg = msg_builder(&Vec::from(["hello".to_string()]));
+ assert_eq!("Title: hello", msg);
+ }
+
+ #[test]
+ /**
+ Integration test ish. this tries to replicate url_preview, to make sure
+ everything works together.
+ */
+ fn test_all() {
+ let _urls = [
+ mockito::mock("GET", "/1")
+ .with_body(
+ r#"
+<html>
+ <head>
+ <title>test site 1</title>
+ </head>
+</html>
+"#,
+ )
+ .create(),
+ mockito::mock("GET", "/2")
+ .with_body(
+ r#"
+<html>
+ <head>
+ <title>test site 2</title>
+ </head>
+"#,
+ )
+ .create(),
+ ];
+
+ let mut titles: Vec<String> = Vec::new();
+ let text = format!(
+ "some text {u}/1 other text {u}/2",
+ u = &mockito::server_url()
+ );
+ let urls = url_parser(&text);
+
+ assert_eq!(urls.len(), 2);
+
+ for url in &urls {
+ if let Ok(title) = tokio_test::block_on(url_title(&url.as_str())) {
+ titles.push(title);
+ }
+ }
+ assert_eq!(msg_builder(&titles), "Titles: test site 1 --- test site 2");
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 9ab7f6c..9af449e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,12 +2,7 @@
async fn main() {
use catinator::catinator;
- tracing_subscriber::fmt()
- .compact()
- .with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
- .with_max_level(tracing::Level::DEBUG)
- .with_thread_ids(true)
- .init();
+ tracing_subscriber::fmt::init();
let mut sed = catinator::hooks::sed::Sed::new();
@@ -24,6 +19,12 @@ async fn main() {
PRIVMSG,
sed.log
),
+ async hook(
+ "url_preview",
+ "Send preview of website",
+ PRIVMSG,
+ catinator::hooks::url::url_preview
+ )
matcher(
"shifty_eyes",
">.>",