Skip to content

Commit

Permalink
Merge branch 'data-provider/yandex' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Leonid Kozarin committed Sep 7, 2023
2 parents fad868f + c796cb2 commit 254e886
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
TELOXIDE_TOKEN=0123456789:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
GOOGLE_MAPS_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
YANDEX_MAPS_GEOCODER_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
YANDEX_MAPS_PLACES_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

RUST_LOG=info
CACHE_TIME=3600
GAPI_MODE=GeoPlace
YAPI_MODE=Place
MSG_LOC_LIMIT=10
WEBHOOK_URL=
18 changes: 12 additions & 6 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use regex::Regex;
use once_cell::sync::Lazy;
use rust_i18n::t;
use crate::help;
use crate::loc::{Location, SearchChain, google::GoogleLocFinder, osm::OpenStreetMapLocFinder};
use crate::loc::{Location, SearchChain, google, yandex, osm};
use crate::metrics::{MESSAGE_COUNTER, INLINE_COUNTER, INLINE_CHOSEN_COUNTER, CMD_HELP_COUNTER, CMD_START_COUNTER, CMD_LOC_COUNTER};
use crate::utils::ensure_lang_code;
use teloxide::prelude::*;
Expand All @@ -13,8 +13,6 @@ use teloxide::types::Me;
use teloxide::types::ParseMode::MarkdownV2;
use teloxide::utils::command::BotCommands;

pub use crate::loc::google::preload_env_vars;

#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase")]
pub enum Command {
Expand All @@ -29,18 +27,25 @@ static COORDS_REGEXP: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?P<latitude>-?\d{
.expect("Invalid regex!"));
static FINDER: Lazy<SearchChain> = Lazy::new(|| {
SearchChain::new(vec![
Box::new(OpenStreetMapLocFinder::new()),
Box::new(GoogleLocFinder::from_env())
Box::new(osm::OpenStreetMapLocFinder::new()),
Box::new(google::GoogleLocFinder::from_env())
]).for_lang_code("ru", vec![
Box::new(yandex::YandexLocFinder::from_env())
])
});

pub fn preload_env_vars() {
google::preload_env_vars();
yandex::preload_env_vars();
}

pub async fn inline_handler(bot: Bot, q: InlineQuery) -> HandlerResult {
if q.query.is_empty() {
bot.answer_inline_query(q.id, vec![]).await?;
return Ok(());
}

log::info!("Got query: {}", q.query);
log::info!("Got inline query: {}", q.query);
INLINE_COUNTER.inc();

let lang_code = &ensure_lang_code(q.from.id, q.from.language_code.clone());
Expand Down Expand Up @@ -122,6 +127,7 @@ async fn cmd_loc_handler(bot: Bot, msg: Message) -> HandlerResult {
async fn resolve_locations_for_message(msg: &Message) -> Result<Vec<Location>, Box<dyn std::error::Error + Send + Sync>> {
let text = msg.text().ok_or("no text")?.to_string();
let from = msg.from().ok_or("no from")?;
log::info!("Got message query: {}", text);

let lang_code = &ensure_lang_code(from.id, from.language_code.clone());
resolve_locations(text, lang_code).await
Expand Down
7 changes: 6 additions & 1 deletion src/help/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ The same is available via inline queries: `@{{bot_name}} Statue of Liberty`

These commands are supported:
/help — print a help message
/loc — use this command to search for a place in a group chat since the bot has no access to usual messages
/loc — use this command to search for a place in a group chat since the bot has no access to usual messages\.

This bot uses information from the following data sources:
[OpenStreetMap Nominatim](https://nominatim.openstreetmap.org/)
[Yandex Maps](https://yandex.ru/maps/)
[Google Maps](https://www.google.com/maps)
7 changes: 6 additions & 1 deletion src/help/ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@

Бот поддерживает следующие команды:
/help — напечатает это сообщение снова;
/loc — используй эту команду для поиска мест в групповых чатах, так как там бот не имеет доступа к обычным сообщениям
/loc — используй эту команду для поиска мест в групповых чатах, так как там бот не имеет доступа к обычным сообщениям\.

Для работы используются данные из следующих источников:
[OpenStreetMap Nominatim](https://nominatim.openstreetmap.org/)
[Yandex Maps](https://yandex.ru/maps/)
[Google Maps](https://www.google.com/maps)
1 change: 1 addition & 0 deletions src/loc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;
use async_trait::async_trait;

pub mod google;
pub mod yandex;
pub mod osm;

#[cfg(test)]
Expand Down
158 changes: 158 additions & 0 deletions src/loc/yandex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use std::str::FromStr;
use anyhow::anyhow;
use async_trait::async_trait;
use once_cell::sync::Lazy;
use strum_macros::EnumString;
use crate::loc::{Location, LocFinder, LocResult};
use crate::metrics;

const GEOCODER_ENV_API_KEY: &str = "YANDEX_MAPS_GEOCODER_API_KEY";
const PLACES_ENV_API_KEY: &str = "YANDEX_MAPS_PLACES_API_KEY";

pub static YAPI_MODE: Lazy<YandexAPIMode> = Lazy::new(|| {
let val = std::env::var("YAPI_MODE").expect("YAPI_MODE must be set!");
log::info!("YAPI_MODE is {val}");
YandexAPIMode::from_str(val.as_str()).expect("Invalid value of YAPI_MODE")
});

#[derive(EnumString)]
pub enum YandexAPIMode {
Geocode, // HTTP Geocoder request
Place, // Places API request
GeoPlace, // Geocoder request first, Places if nothing was found
}

/// Load and check required parameters at startup
pub fn preload_env_vars() {
let _ = *YAPI_MODE;
}

pub struct YandexLocFinder {
geocode_api_key: String,
places_api_key: Option<String>,

geocode_req_counter: prometheus::Counter,
place_req_counter: prometheus::Counter,
}

impl YandexLocFinder {
pub fn init(geocode_api_key: String, places_api_key: Option<String>) -> YandexLocFinder {
let base_opts = prometheus::Opts::new("yandex_maps_api_requests_total", "count of requests to the Yandex Maps API");
let geocode_opts = base_opts.clone().const_label("API", "geocode");
let place_opts = base_opts.clone().const_label("API", "place");

YandexLocFinder {
geocode_api_key,
places_api_key,

geocode_req_counter: metrics::REGISTRY.register_counter("Yandex Maps API (geocode) requests", geocode_opts),
place_req_counter: metrics::REGISTRY.register_counter("Yandex Maps API (place) requests", place_opts),
}
}

pub fn from_env() -> YandexLocFinder {
let geocode_api_key = std::env::var(GEOCODER_ENV_API_KEY).expect("Yandex Maps Geocoder API key is required!");
let places_api_key = match *YAPI_MODE {
YandexAPIMode::Place | YandexAPIMode::GeoPlace => {
let api_key = std::env::var(PLACES_ENV_API_KEY).expect("Yandex Maps Places API key is required!");
Some(api_key)
}
YandexAPIMode::Geocode => None
};
Self::init(geocode_api_key, places_api_key)
}

pub async fn find_geo_place(&self, address: &str, lang_code: &str) -> LocResult {
let mut results = self.find_geo(address, lang_code).await?;
if results.is_empty() {
results = self.find_place(address, lang_code).await?;
}
Ok(results)
}

pub async fn find_geo(&self, address: &str, lang_code: &str) -> LocResult {
self.geocode_req_counter.inc();

let url = format!("https://geocode-maps.yandex.ru/1.x?apikey={}&lang={}&geocode={}&format=json",
self.geocode_api_key, lang_code, address);
let resp = reqwest::get(url).await?.json::<serde_json::Value>().await?;

log::info!("response from Yandex Maps Geocoder: {}", resp);

let empty: Vec<serde_json::Value> = Vec::new();
let result = resp["response"]["GeoObjectCollection"]["featureMember"]
.as_array()
.unwrap_or(&empty)
.iter()
.filter_map(geocode_elem_mapper)
.collect();
Ok(result)
}

pub async fn find_place(&self, address: &str, lang_code: &str) -> LocResult {
self.place_req_counter.inc();

let api_key = self.places_api_key.clone()
.ok_or(anyhow!("unexpected absence of a key for Yandex Maps Places API"))?;

let url = format!("https://search-maps.yandex.ru/v1/?apikey={}&lang={}&text={}",
api_key, lang_code, address);
let resp = reqwest::get(url).await?.json::<serde_json::Value>().await?;

log::info!("response from Yandex Maps Places API: {}", resp);

let empty: Vec<serde_json::Value> = Vec::new();
let result = resp["features"]
.as_array()
.unwrap_or(&empty)
.iter()
.filter_map(places_elem_mapper)
.collect();
Ok(result)
}
}

#[async_trait]
impl LocFinder for YandexLocFinder {
async fn find(&self, query: &str, lang_code: &str) -> LocResult {
match *YAPI_MODE {
YandexAPIMode::Geocode => self.find_geo(query, lang_code).await,
YandexAPIMode::Place => self.find_place(query, lang_code).await,
YandexAPIMode::GeoPlace => self.find_geo_place(query, lang_code).await,
}
}
}

fn geocode_elem_mapper(v: &serde_json::Value) -> Option<Location> {
let obj = &v["GeoObject"];
let metadata = &obj["metaDataProperty"]["GeocoderMetaData"];
let address = Some(metadata["text"].as_str()?.to_string());

let pos = &obj["Point"]["pos"].as_str()?
.split(' ')
.collect::<Vec<&str>>();
if pos.len() < 2 {
log::error!("pos length < 2: {pos:?}");
return None
}
let longitude: f64 = pos[0].parse().ok()?;
let latitude: f64 = pos[1].parse().ok()?;

Some(Location {
address, latitude, longitude
})
}

fn places_elem_mapper(v: &serde_json::Value) -> Option<Location> {
let name = v["properties"]["name"].as_str()?;
let description = v["properties"]["description"].as_str()?;
let address = Some(format!("{}, {}", name, description));

let loc = &v["geometry"]["coordinates"];
let longitude: f64 = loc[0].as_f64()?;
let latitude: f64 = loc[1].as_f64()?;

Some(Location {
address, latitude, longitude
})
}

0 comments on commit 254e886

Please sign in to comment.