Skip to content

Commit

Permalink
🔖 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
eigenein committed Sep 1, 2020
1 parent cb848d3 commit cef4746
Show file tree
Hide file tree
Showing 14 changed files with 469 additions and 172 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `0.1.0`

- 🔖 Initial release
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
# `mrktpltsbot`

[Marktplaats](https://www.marktplaats.nl/) search notifications in [Telegram](https://telegram.org/).
Periodically polls [Marktplaats](https://www.marktplaats.nl/) for the specified search queries and notifies the user about new items via [Telegram](https://telegram.org/).

[![Crates.io](https://img.shields.io/crates/v/mrktpltsbot?logo=rust)](https://crates.io/crates/mrktpltsbot)
[![Crates.io](https://img.shields.io/crates/l/mrktpltsbot)](https://crates.io/crates/mrktpltsbot)
[![GitHub last commit](https://img.shields.io/github/last-commit/eigenein/mrktpltsbot?logo=github)](https://github.com/eigenein/mrktpltsbot/commits/master)

TODO: user manual
## Usage

```shell script
mrktpltsbot <bot-token> -c <allowed-chat-id> ...
```

### Supported commands

- `/subscribe <query>`
- `/unsubscribe <subscription ID>`
- `/search <query>` – preview the top 1 result
- For a plain text message the bot will suggest you to subscribe to the search query

### Allow list

The bot allows only the specified chat IDs to interact with itself. To find out a chat ID you can run the bot without a `-c` option and send it a message. It will respond with the chat ID that you have to add to the parameters.

### Monitoring

The bot supports the `--sentry-dsn` option to integrate with [Sentry](https://sentry.io).
88 changes: 27 additions & 61 deletions src/chat_bot.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
//! Implements the Telegram chat bot.

use crate::marktplaats::{search, SearchListing};
use crate::math::div_rem;
use crate::marktplaats::search;
use crate::prelude::*;
use crate::redis::{get_subscription_details, unsubscribe_from};
use crate::search_bot;
use crate::telegram::format::escape_markdown_v2;
use crate::telegram::{types::*, *};

const OFFSET_KEY: &str = "telegram::offset";
const ALLOWED_UPDATES: &[&str] = &["message", "callback_query"];
const MARKDOWN_V2: Option<&str> = Some("MarkdownV2");

pub struct ChatBot {
telegram: Telegram,
Expand All @@ -26,7 +27,7 @@ impl ChatBot {
}
}

pub async fn spawn(mut self) -> Result {
pub async fn run(mut self) -> Result {
self.set_my_commands().await?;
info!("Running the chat bot…");
loop {
Expand Down Expand Up @@ -63,13 +64,15 @@ impl ChatBot {
.await?;
} else if let Some(callback_query) = update.callback_query {
info!("Callback query #{}.", callback_query.id);
// TODO: https://core.telegram.org/bots/api#answercallbackquery
if let Some(message) = callback_query.message {
self.handle_text_message(message.chat.id, callback_query.data)
.await?;
} else {
warn!("No message in the callback query.");
}
self.telegram
.answer_callback_query(&callback_query.id)
.await?;
} else {
warn!("Unhandled update #{}.", update.id);
}
Expand Down Expand Up @@ -104,12 +107,10 @@ impl ChatBot {
if let Some(query) = text.strip_prefix("/subscribe ") {
self.handle_subscribe_command(chat_id, query).await?;
} else if let Some(subscription_id) = text.strip_prefix("/unsubscribe ") {
self.handle_unsubscribe_command(chat_id, subscription_id)
self.handle_unsubscribe_command(chat_id, subscription_id.parse()?)
.await?;
} else if let Some(query) = text.strip_prefix("/search ") {
self.handle_search_preview_command(chat_id, query).await?;
} else if text == "/list" {
self.handle_list_command(chat_id).await?;
} else {
self.handle_search_query(chat_id, text).await?;
}
Expand All @@ -124,7 +125,7 @@ impl ChatBot {
.send_message(
chat_id,
&format!(
"✅ Subscribed to *{}*\n\nThere\\'re *{}* active subscriptions now.",
"✅ Subscribed to *{}*\n\nThere\\'re *{}* active subscriptions now\\.",
escape_markdown_v2(query),
subscription_count,
),
Expand All @@ -137,20 +138,28 @@ impl ChatBot {
Ok(())
}

async fn handle_unsubscribe_command(&self, _chat_id: i64, _subscription_id: &str) -> Result {
Ok(())
}

async fn handle_list_command(&self, _chat_id: i64) -> Result {
async fn handle_unsubscribe_command(&mut self, chat_id: i64, subscription_id: i64) -> Result {
let (_, query) = get_subscription_details(&mut self.redis, subscription_id).await?;
let subscription_count =
unsubscribe_from(&mut self.redis, chat_id, subscription_id).await?;
self.telegram
.send_message(
chat_id,
&format!(
"☑️ Unsubscribed\\!\n\nThere\\'re *{}* active subscriptions now\\.",
subscription_count
),
MARKDOWN_V2,
Into::<ReplyMarkup>::into(InlineKeyboardButton::new_subscribe_button(&query)),
)
.await?;
Ok(())
}

async fn handle_search_preview_command(&self, chat_id: i64, query: &str) -> Result {
async fn handle_search_preview_command(&mut self, chat_id: i64, query: &str) -> Result {
let search_response = search(query, "1").await?;
for listing in search_response.listings.iter() {
self.telegram
.send_message(chat_id, &format_listing(listing), MARKDOWN_V2, None)
.await?;
search_bot::push_notification(&mut self.redis, None, chat_id, listing).await?;
}
Ok(())
}
Expand All @@ -171,55 +180,12 @@ impl ChatBot {
}
}

impl InlineKeyboardButton {
fn new_search_preview_button(query: &str) -> Self {
Self {
text: "🔎 Preview".into(),
callback_data: Some(format!("/search {}", query)),
url: None,
}
}

fn new_subscribe_button(query: &str) -> Self {
Self {
text: "✅ Subscribe".into(),
callback_data: Some(format!("/subscribe {}", query)),
url: None,
}
}

fn new_unsubscribe_button(subscription_id: i64) -> Self {
Self {
text: "❌ Unsubscribe".into(),
callback_data: Some(format!("/unsubscribe {}", subscription_id)),
url: None,
}
}
}

fn format_listing(listing: &SearchListing) -> String {
let (euros, cents) = div_rem(listing.price.cents, 100);

format!(
"*{}*\n\n💰 {}\\.{:02} {:?}\n\n{}",
escape_markdown_v2(&listing.title),
euros,
cents,
listing.price.type_,
escape_markdown_v2(&listing.description),
)
}

impl ChatBot {
/// Set the bot commands.
async fn set_my_commands(&self) -> Result {
info!("Setting the chat bot commands…");
self.telegram
.set_my_commands(vec![
BotCommand {
command: "/list".into(),
description: "Show the saved searches".into(),
},
BotCommand {
command: "/subscribe".into(),
description: "Subscribe to the search query".into(),
Expand Down
22 changes: 17 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ pub mod result;
pub mod search_bot;
pub mod telegram;

use crate::chat_bot::ChatBot;
use crate::prelude::*;
use crate::telegram::notifier::Notifier;
use crate::telegram::Telegram;
use std::iter::FromIterator;

#[async_std::main]
Expand All @@ -24,14 +27,23 @@ async fn main() -> Result {
logging::init()?;
let redis = redis::open(opts.redis_db).await?;

futures::future::try_join(
search_bot::Bot::new(redis.get_async_std_connection().await?).spawn(),
chat_bot::ChatBot::new(
telegram::Telegram::new(&opts.telegram_token),
futures::future::try_join3(
search_bot::Bot::new(
redis.get_async_std_connection().await?,
opts.polling_interval_secs,
)
.run(),
ChatBot::new(
Telegram::new(&opts.telegram_token),
redis.get_async_std_connection().await?,
HashSet::from_iter(opts.allowed_chat_ids),
)
.spawn(),
.run(),
Notifier::new(
redis.get_async_std_connection().await?,
Telegram::new(&opts.telegram_token),
)
.run(),
)
.await?;

Expand Down
2 changes: 1 addition & 1 deletion src/marktplaats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pub enum PriorityProduct {

/// Search Marktplaats.
pub async fn search(query: &str, limit: &str) -> Result<SearchResponse> {
info!("Searching `{}` in Marktplaats…", query);
info!("Searching `{}` on Marktplaats…", query);
Ok(CLIENT.get(Url::parse_with_params("https://www.marktplaats.nl/lrp/api/search?offset=0&sortBy=SORT_INDEX&sortOrder=DECREASING", &[("query", query), ("limit", limit)])?).send().await?.json().await?)
}

Expand Down
9 changes: 9 additions & 0 deletions src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ pub struct Opts {
env = "MRKTPLTS_BOT_ALLOWED_CHAT_IDS"
)]
pub allowed_chat_ids: Vec<i64>,

/// Polling interval in seconds
#[structopt(
default_value = "180",
short = "i",
long = "poll-interval",
env = "MRKTPLTS_BOT_POLLING_INTERVAL"
)]
pub polling_interval_secs: u64,
}
2 changes: 1 addition & 1 deletion src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pub use std::error::Error;
pub use std::sync::Arc;
pub use std::time::Duration;

pub use anyhow::Context;
pub use anyhow::{anyhow, Context};
pub use async_std::task;
pub use chrono::{DateTime, Local};
pub use futures::stream::{self, StreamExt, TryStreamExt};
Expand Down
Loading

0 comments on commit cef4746

Please sign in to comment.