diff --git a/Cargo.lock b/Cargo.lock index ed06f79..17f6e79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,12 +277,6 @@ dependencies = [ "instant", ] -[[package]] -name = "fastrand" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" - [[package]] name = "flate2" version = "1.0.26" @@ -316,9 +310,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -466,29 +460,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" -[[package]] -name = "hoot" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df22a4d90f1b0e65fe3e0d6ee6a4608cc4d81f4b2eb3e670f44bb6bde711e452" -dependencies = [ - "httparse", - "log", -] - -[[package]] -name = "hootbin" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "354e60868e49ea1a39c44b9562ad207c4259dc6eabf9863bf3b0f058c55cfdb2" -dependencies = [ - "fastrand 2.0.1", - "hoot", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "http" version = "0.2.9" @@ -574,9 +545,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -829,9 +800,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -1369,7 +1340,7 @@ checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ "autocfg", "cfg-if", - "fastrand 1.9.0", + "fastrand", "redox_syscall", "rustix 0.37.23", "windows-sys", @@ -1553,13 +1524,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.4" +version = "2.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399dd89e2af196ae4f83a47bb37a1455e664fe2fed97b3ae68a1c4a3f8216e76" +checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" dependencies = [ "base64", "flate2", - "hootbin", "log", "once_cell", "rustls", @@ -1573,9 +1543,9 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -1719,7 +1689,7 @@ dependencies = [ [[package]] name = "wiki_location_bot" -version = "0.2.0" +version = "0.3.0" dependencies = [ "log", "pretty_env_logger", @@ -1728,6 +1698,7 @@ dependencies = [ "teloxide", "tokio", "ureq", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6109644..595d673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "wiki_location_bot" -version = "0.2.0" +version = "0.3.0" edition = "2021" [dependencies] -teloxide = { version = "0.12.2", features = ["macros"] } +teloxide = { version = "0.12", features = ["macros"] } log = "0.4" pretty_env_logger = "0.5.0" -tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] } -ureq = { version = "2.9.4", features = ["json"] } -serde = { version = "1", features = ["derive"] } +tokio = { version = "1.36", features = ["rt-multi-thread", "macros"] } +ureq = { version = "2.9", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +url = "2.5" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..201f836 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +error_on_unformatted = true +error_on_line_overflow = true +max_width = 130 diff --git a/src/main.rs b/src/main.rs index 7349be2..30c610d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,21 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use teloxide::prelude::*; -use teloxide::types::{ButtonRequest, KeyboardButton, KeyboardMarkup}; +use teloxide::types::{ButtonRequest, KeyboardButton, KeyboardMarkup, MessageKind}; +use teloxide::types::{MediaKind, Message}; -#[derive(Debug, Deserialize, Serialize)] -pub struct PageInfo { - pageid: usize, - ns: usize, - title: String, - contentmodel: String, - pagelanguage: String, - pagelanguagehtmlcode: String, - pagelanguagedir: String, - touched: String, - lastrevid: usize, - length: usize, -} +mod openstreetmap; +mod wikipedia; -#[derive(Debug, Deserialize, Serialize)] -pub struct GeoSearch { - pageid: usize, - ns: usize, - title: String, - lat: f64, - lon: f64, - dist: f32, - primary: Option, -} +fn help_text() -> String { + r#" +This is a simple bot that takes a location and returns all of the nearby locations that have a wikipedia page -#[derive(Debug, Deserialize, Serialize)] -pub struct Query { - geosearch: Option>, - pages: Option>, -} +The bot can either be used by the `Send Location` button, sending a location from the share menu, or typing an address -#[derive(Debug, Deserialize, Serialize)] -pub struct Root { - batchcomplete: bool, - query: Query, +This bot was made possible by: + [OSM Foundation](https://nominatim.org/) for location searching + [Wikipedia](https://www.wikipedia.org/) for having great APIs +"# + .to_owned() } #[tokio::main] @@ -49,9 +27,10 @@ async fn main() { teloxide::repl(bot, |bot: Bot, msg: Message| async move { log::info!("Message received."); + bot.send_chat_action(msg.chat.id, teloxide::types::ChatAction::Typing).await?; let location_button = KeyboardButton { - text: "Nearby Articles".to_string(), + text: "Send Location".to_string(), request: Some(ButtonRequest::Location), }; @@ -59,44 +38,49 @@ async fn main() { .one_time_keyboard(true) .resize_keyboard(true); - match msg.location() { - Some(user_location) => { - log::info!("Location received."); - bot.send_message(msg.chat.id, "Searching for nearby locations...").await?; - - let nearby_locations = ureq::get(&format!( - "https://en.wikipedia.org/w/api.php?action=query&format=json&list=geosearch&formatversion=2&gscoord={}|{}&gsradius=10000&gslimit=5", - user_location.latitude, user_location.longitude - )) - .call() - .unwrap() - .into_json::() - .unwrap() - .query - .geosearch - .unwrap(); - - for location in nearby_locations { - bot.send_location(msg.chat.id, location.lat, location.lon).await?; - - let url = teloxide::utils::markdown::link(&format!("http://en.wikipedia.org/?curid={}", location.pageid), &location.title); - let bold_url = teloxide::utils::markdown::bold(&url); - bot.send_message(msg.chat.id, bold_url) - .parse_mode(teloxide::types::ParseMode::MarkdownV2) - .await?; + match msg.kind { + MessageKind::Common(ref common) => match &common.media_kind { + MediaKind::Location(media_location) => { + let user_location = media_location.location; + log::info!("Location received."); + wikipedia::send_wikipedia_pages(user_location.latitude, user_location.longitude, bot, msg).await; } - bot.send_message(msg.chat.id, "Send a location to see nearby places that have a wikipedia page!") - .reply_markup(location_button_markup) - .await?; + MediaKind::Text(media_text) => { + let text = &media_text.text; + if text.contains("/help") { + bot.send_message(msg.chat.id, help_text()) + .parse_mode(teloxide::types::ParseMode::MarkdownV2) + .await?; + } else { + match openstreetmap::geocode_text(text) { + Ok(location) => { + bot.send_location(msg.chat.id, location.lat, location.lon).await?; + bot.send_message(msg.chat.id, format!("Location found: {}", location.name)) + .await?; + wikipedia::send_wikipedia_pages(location.lat, location.lon, bot, msg).await; + } + Err(error) => { + println!("Geocoding failed: {}", error); + bot.send_message( + msg.chat.id, + format!("Location query returned no matches:\n\t{}\nTry /help", text), + ) + .await?; + } + } + } + } + _ => {} + }, + _ => { + bot.send_message( + msg.chat.id, + "Send a location or address to see nearby places that have a wikipedia page!", + ) + .reply_markup(location_button_markup) + .await?; } - None => { - log::info!("Something other than a location received."); - bot.send_message(msg.chat.id, "Send a location to see nearby places that have a wikipedia page!") - .reply_markup(location_button_markup) - .await?; - } - }; - + } Ok(()) }) .await; diff --git a/src/openstreetmap.rs b/src/openstreetmap.rs new file mode 100644 index 0000000..09f8f78 --- /dev/null +++ b/src/openstreetmap.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Place { + place_id: i64, + licence: String, + osm_type: String, + osm_id: i64, + lat: String, + lon: String, + category: String, + #[serde(rename = "type")] + place_type: String, + place_rank: i32, + importance: f64, + addresstype: String, + name: String, + display_name: String, + boundingbox: Vec, +} + +pub struct Location { + pub name: String, + pub lat: f64, + pub lon: f64, +} + +pub fn geocode_text(search_query: &str) -> Result { + // "https://nominatim.openstreetmap.org/search.php?q=1650+wewatta+world&format=jsonv2&limit=1" + let base_url = "https://nominatim.openstreetmap.org/search.php"; + + let url = Url::parse_with_params(base_url, &[("q", search_query), ("format", "jsonv2"), ("limit", "1")]) + .expect("Failed to construct OSM URL"); + + let response = ureq::get(url.as_str()) + .set("User-Agent", "Wiki Location Telegram Bot") + .call() + .expect("Request failed") + .into_string() + .expect("Failed to read response from OSM"); + + let places: Vec = serde_json::from_str(&response).expect("Failed to parse JSON"); + + if let Some(place) = places.first() { + let name = place.name.clone(); + let lat: f64 = place.lat.parse().expect("Failed to parse latitude"); + let lon: f64 = place.lon.parse().expect("Failed to parse longitude"); + Ok(Location { name, lat, lon }) + } else { + Err("Latitude and Longitude could not be found") + } +} diff --git a/src/wikipedia.rs b/src/wikipedia.rs new file mode 100644 index 0000000..e33c13b --- /dev/null +++ b/src/wikipedia.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use teloxide::prelude::*; +use teloxide::types::Message; + +#[derive(Debug, Deserialize, Serialize)] +pub struct PageInfo { + pageid: usize, + ns: usize, + title: String, + contentmodel: String, + pagelanguage: String, + pagelanguagehtmlcode: String, + pagelanguagedir: String, + touched: String, + lastrevid: usize, + length: usize, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GeoSearch { + pageid: usize, + ns: usize, + title: String, + lat: f64, + lon: f64, + dist: f32, + primary: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Query { + geosearch: Option>, + pages: Option>, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Root { + batchcomplete: bool, + query: Query, +} + +fn escape_markdown_v2(text: String) -> String { + text.chars().fold(String::new(), |mut acc, c| { + match c { + '_' | '*' | '[' | ']' | '(' | ')' | '~' | '`' | '>' | '#' | '+' | '-' | '=' | '|' | '{' | '}' | '.' | '!' => { + acc.push('\\'); + } + _ => {} + } + acc.push(c); + acc + }) +} + +pub async fn send_wikipedia_pages(latitude: f64, longitude: f64, bot: Bot, msg: Message) { + let nearby_locations = ureq::get(&format!( + concat!( + "https://en.wikipedia.org/w/api.php", + "?action=query&format=json&list=geosearch&formatversion=2&gscoord={}|{}&gsradius=10000&gslimit=5" + ), + latitude, longitude + )) + .set("User-Agent", "Wiki Location Telegram Bot") + .call() + .unwrap() + .into_json::() + .unwrap() + .query + .geosearch + .unwrap(); + + for location in nearby_locations { + bot.send_location(msg.chat.id, location.lat, location.lon).await.unwrap(); + + let url = teloxide::utils::markdown::link( + &format!("http://en.wikipedia.org/?curid={}", location.pageid), + &escape_markdown_v2(location.title), + ); + let bold_url = teloxide::utils::markdown::bold(&url); + bot.send_message(msg.chat.id, bold_url) + .parse_mode(teloxide::types::ParseMode::MarkdownV2) + .await + .unwrap(); + } +}