Skip to content

Commit

Permalink
Adds maintenance task to clean up unused and empty lists
Browse files Browse the repository at this point in the history
  • Loading branch information
cuducos committed Oct 11, 2024
1 parent 0b6f91a commit 4c93736
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 50 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ Bot na Lista bot (Portuguese word play for _add to the list_) made with 💜 by

Send a message or add [`@bot_na_lista_bot`](https://t.me/bot_na_lista_bot) to a group to get started.

| Comando | Descrição |
| Command | Description |
|---|---|
| `<string>` | Adiciona `<string>` à lista de compras |
| `<number>` | Remove ítem número `<number>` da lista de compras |
| `/view` | Mostra a lista de compras |
| `text` | Adds `text` to the list |
| `number` | Removes item `number` from the list |
| `/view` | Shows the list |

> [!WARNING]
> Lists with no activity for more than one year will be deleted.

## Environment Variables

| Variable | Description |
|---|---|
| `DATABASE_URL` | Credentials for a PostgreSQL database |
| `TELOXIDE_TOKEN` | Telegram bot token |
| `TELOXIDE_TOKEN` | Telegram bot token |
| `PORT` | Bot webhook port (optional) |
| `HOST` | Bot webhook host (optional) |

Expand Down
2 changes: 2 additions & 0 deletions migrations/2024-10-11-145759_create_is_empty/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_list_is_empty;
ALTER TABLE list DROP COLUMN is_empty;
3 changes: 3 additions & 0 deletions migrations/2024-10-11-145759_create_is_empty/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE list ADD COLUMN is_empty BOOLEAN NOT NULL DEFAULT TRUE;
UPDATE list SET is_empty = (items = '{}');
CREATE INDEX idx_list_is_empty ON list(is_empty);
22 changes: 14 additions & 8 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
use anyhow::{Context, Result};
use chrono::Utc;
use diesel::{
r2d2::{ConnectionManager, PooledConnection},
r2d2::{ConnectionManager, Pool, PooledConnection},
update, ExpressionMethods, OptionalExtension, PgConnection, QueryDsl, RunQueryDsl,
};

Expand All @@ -15,8 +15,12 @@ pub struct Chat {
}

impl Chat {
pub fn new(chat_id: i64, conn: PooledConnection<ConnectionManager<PgConnection>>) -> Self {
Self { chat_id, conn }
pub fn new(chat_id: i64, pool: &Pool<ConnectionManager<PgConnection>>) -> Result<Self> {
let conn = pool.get().context(format!(
"[List#{}] Could not get database connection",
chat_id
))?;
Ok(Self { chat_id, conn })
}

fn create_list(&mut self) -> Result<List> {
Expand All @@ -36,6 +40,7 @@ impl Chat {
))?;
if !list.items.contains(&new_item) {
list.items.push(new_item);
list.is_empty = false;
list.updated_at = Utc::now();
update(list::table)
.filter(list::dsl::chat_id.eq(self.chat_id))
Expand All @@ -46,13 +51,14 @@ impl Chat {
Ok(list)
}

fn remove_item(&mut self, item: usize) -> Result<List> {
fn remove_item(&mut self, index: usize) -> Result<List> {
let mut list = self.list().context(format!(
"[List#{}] Error looking for list to remove item",
self.chat_id
))?;
if item < list.items.len() {
list.items.remove(item);
if index < list.items.len() {
list.items.remove(index);
list.is_empty = list.items.is_empty();
list.updated_at = Utc::now();
update(list::table)
.filter(list::dsl::chat_id.eq(self.chat_id))
Expand Down Expand Up @@ -81,8 +87,8 @@ impl Chat {
}

pub fn process_input(&mut self, item: &str) -> Result<List> {
if let Ok(index) = item.parse::<usize>() {
self.remove_item(index - 1)
if let Ok(number) = item.parse::<usize>() {
self.remove_item(number - 1)
} else {
self.add_item(item)
}
Expand Down
14 changes: 9 additions & 5 deletions src/db.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
Connection, PgConnection,
};
use std::{env, time::Duration};

Expand All @@ -16,14 +16,18 @@ pub fn max_connections() -> u32 {
DEFAULT_MAX_CONNECTIONS
}

pub fn from_env() -> Result<Pool<ConnectionManager<PgConnection>>> {
pub fn pool_from_env() -> Result<Pool<ConnectionManager<PgConnection>>> {
let url = env::var("DATABASE_URL").context("Missing `DATABASE_URL` environment variable")?;
let manager = ConnectionManager::<PgConnection>::new(url);
let pool = Pool::builder()
Pool::builder()
.max_size(max_connections())
.max_lifetime(Some(Duration::from_secs(300)))
.idle_timeout(Some(Duration::from_secs(60)))
.build(manager)
.context("Error creating connection pool")?;
Ok(pool)
.context("Error creating connection pool")
}

pub fn from_env() -> Result<PgConnection> {
let url = env::var("DATABASE_URL").context("Missing `DATABASE_URL` environment variable")?;
PgConnection::establish(&url).context("Error connecting to the database")
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use diesel_migrations::{embed_migrations, EmbeddedMigrations};
pub mod chat;

pub mod db;
pub mod maintenance;
pub mod models;
pub mod schema;
pub mod telegram;
Expand Down
17 changes: 15 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
use std::time::Duration;

use anyhow::{Context, Result};
use bot_na_lista::{db, telegram, MIGRATIONS};
use bot_na_lista::{db, maintenance, telegram, MIGRATIONS};
use diesel_migrations::{HarnessWithOutput, MigrationHarness};
use tokio::{spawn, time::sleep};

const ONE_DAY: Duration = Duration::from_secs(60 * 60 * 24);

#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
pretty_env_logger::init();
log::info!("Starting Bot na Lista");
let pool = db::from_env().context("Could not get a database connection pool")?;
let pool = db::pool_from_env().context("Could not get a database connection pool")?;
let mut conn = pool.get().context("Could not get a database connection")?;
let mut harness = HarnessWithOutput::write_to_stdout(&mut conn);
harness
.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow::anyhow!("Failed to run pending migrations: {}", e))?;
spawn(async {
loop {
if let Err(e) = maintenance::clean_up() {
log::error!("Error cleaning up: {:?}", e);
}
sleep(ONE_DAY).await;
}
});
telegram::run(pool).await
}
18 changes: 18 additions & 0 deletions src/maintenance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use anyhow::Result;
use chrono::{Duration, Utc};
use diesel::{delete, BoolExpressionMethods, ExpressionMethods, RunQueryDsl};

use crate::{db, schema::list};

pub fn clean_up() -> Result<()> {
let mut conn = db::from_env()?;
let one_year_ago = Utc::now() - Duration::days(366); // considering leap years
delete(list::table)
.filter(
list::dsl::updated_at
.lt(one_year_ago)
.or(list::dsl::is_empty.eq(true)),
)
.execute(&mut conn)?;
Ok(())
}
15 changes: 15 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub struct List {
pub items: Vec<Option<String>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub is_empty: bool,
}

impl std::fmt::Display for List {
Expand All @@ -26,6 +27,18 @@ impl std::fmt::Display for List {
}
}

impl std::fmt::Debug for List {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("List")
.field("chat_id", &self.chat_id)
.field("is_empty", &self.is_empty)
.field("items", &self.items)
.field("created_at", &self.created_at)
.field("updated_at", &self.updated_at)
.finish()
}
}

#[derive(Insertable, AsChangeset)]
#[diesel(table_name = crate::schema::list)]
pub struct NewList {
Expand All @@ -40,6 +53,7 @@ mod tests {
fn test_list_display() {
let list = List {
chat_id: 42,
is_empty: false,
items: vec![Some("Foo".to_string()), Some("Bar".to_string()), None],
created_at: Utc::now(),
updated_at: Utc::now(),
Expand All @@ -53,6 +67,7 @@ mod tests {
fn test_empty_list_display() {
let list = List {
chat_id: 1,
is_empty: true,
items: vec![],
created_at: Utc::now(),
updated_at: Utc::now(),
Expand Down
1 change: 1 addition & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ diesel::table! {
items -> Array<Nullable<Text>>,
created_at -> Timestamptz,
updated_at -> Timestamptz,
is_empty -> Bool,
}
}
15 changes: 6 additions & 9 deletions src/telegram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ If the message is a number, I will delete the item from the list that has that n
If the message is not a number, I will add the item to the list.
Use /view to see the list.";
Use /view to see the list.
Lists with no activity for more than one year are automatically deleted.";

enum Response {
Text(String),
Expand Down Expand Up @@ -54,9 +56,8 @@ async fn process_message(pool: Pool<ConnectionManager<PgConnection>>, bot: &Bot,
if txt == "/help" || txt == "/start" {
return send_response(bot, msg.chat.id, Response::Text(HELP.to_string())).await;
}
match pool.get() {
Ok(conn) => {
let mut chat = Chat::new(msg.chat.id.0, conn);
match Chat::new(msg.chat.id.0, &pool) {
Ok(mut chat) => {
let reply = if txt == "/view" {
chat.list()
} else {
Expand All @@ -72,11 +73,7 @@ async fn process_message(pool: Pool<ConnectionManager<PgConnection>>, bot: &Bot,
),
}
}
Err(e) => log::error!(
"[List#{}] Error getting database connection: {}",
msg.chat.id.0,
e
),
Err(e) => log::error!("[List#{}] Error handling chat: {}", msg.chat.id.0, e),
}
}
}
Expand Down
10 changes: 6 additions & 4 deletions tests/chat_list_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ fn assert_list_table_count(
#[test]
fn test_new_chat_gets_a_new_list() {
run(|| {
let pool = db::from_env().expect("Could not connect to PostgreSQL");
let mut chat = Chat::new(42, pool.get().expect("Could not get database connection"));
let pool = db::pool_from_env().expect("Could not connect to PostgreSQL");
let mut chat = Chat::new(42, &pool).expect("Could not start chat");
let mut conn = pool.get().expect("Could not get database connection");
assert_list_table_count(&mut conn, 0);
let list = chat.list().expect("Error getting a list");
assert!(list.is_empty);
assert_eq!(42, list.chat_id);
assert_list_table_count(&mut conn, 1);
});
Expand All @@ -44,16 +45,17 @@ fn test_new_chat_gets_a_new_list() {
#[test]
fn test_known_chat_gets_existing_list() {
run(|| {
let pool = db::from_env().expect("Could not connect to PostgreSQL");
let pool = db::pool_from_env().expect("Could not connect to PostgreSQL");
let mut conn = pool.get().expect("Could not get database connection");
assert_list_table_count(&mut conn, 0);
insert_into(list::table)
.values(&NewList { chat_id: 42 })
.get_result::<List>(&mut conn)
.expect("Could not create existing list");
assert_list_table_count(&mut conn, 1);
let mut chat = Chat::new(42, conn);
let mut chat = Chat::new(42, &pool).expect("Could not start chatt");
let list = chat.list().expect("Error getting a list");
assert!(list.is_empty);
assert_eq!(42, list.chat_id);
});
}
Loading

0 comments on commit 4c93736

Please sign in to comment.