Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add skeleton documentation and add clippy warning to workflow #4

Merged
merged 7 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 54 additions & 11 deletions src/bot.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! Bot services.
//!
//! The shuttle_runtime for the BotService is defined here, which
//! binds itself to the `SocketAddr` provided by shuttle.
use crate::{
database::*,
message::{display_tally, list_runs, list_users},
Expand All @@ -6,11 +10,15 @@ use sqlx::PgPool;
use teloxide::{prelude::*, utils::command::BotCommands};
use tracing::error;

/// Encapsulate the BotService.
pub struct BotService {
/// Teloxide Bot.
pub bot: Bot,
/// Database connection.
pub postgres: PgPool,
}

/// Required implementation of the `shuttle_runtime::Service` trait for `BotService`.
#[shuttle_runtime::async_trait]
impl shuttle_runtime::Service for BotService {
async fn bind(self, _addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> {
Expand All @@ -23,7 +31,11 @@ impl shuttle_runtime::Service for BotService {
}
}

/// impl block for `BotService`.
impl BotService {
/// Clones `bot` and `db_connection` before passing these over to `Command`, also
/// defined within teloxide. It parses incoming commands, matches them and
/// hands them over to the `answer` methold.
async fn start(&self) -> Result<(), shuttle_runtime::CustomError> {
let bot = self.bot.clone();
let db_connection = self.postgres.clone();
Expand All @@ -37,31 +49,62 @@ impl BotService {
}
}

/// Enumeration of commands accepted by the bot.
#[derive(BotCommands, Clone)]
#[command(
rename_rule = "lowercase",
description = "The following commands are supported:"
)]
enum Command {
#[command(description = "display this text")]
/// Matched to `/help` -> displays commands and their documentation.
#[command(description = "Display this text. Usage: /help")]
Help,
#[command(description = "Show users registered on telerun within the chat.")]
#[command(description = "Show users registered on telerun within the chat. Usage: /show")]
/// Matched to `/show` -> displays users within chat.
Show,
/// Matched to `/add <distance> <user_name>` -> creates users in db if not present,
/// then adds run data to runs table.
#[command(
description = "Add run data to database. Format is /add %distance (km)% %username%",
description = "Add run data to database. Usage: /add <distance> <user_name>",
parse_with = "split"
)]
Add { distance: f32, user_name: String },
#[command(description = "Edit data for a run.", parse_with = "split")]
Edit { run_id: i32, distance: f32 },
#[command(description = "Remove a run from database.")]
Delete { run_id: i32 },
#[command(description = "Tallies current medals and distances.")]
Add {
/// Distance run in km
distance: f32,
/// Name of user to tie the run to. Must be unique.
user_name: String,
},
/// Matched to `/edit <run_id> <distance>` -> edits stored run data.
#[command(
description = "Edit data for a run. Usage: /edit <run_id> <distance>",
parse_with = "split"
)]
Edit {
/// Id of run as stored in runs table.
run_id: i32,
/// Corrected distance run in km.
distance: f32,
},
/// Matched to `/delete <run_id>` -> removes a certain run from database.
#[command(description = "Remove a run from database. Usage: /delete <run_id>")]
Delete {
/// Id of run to remove from table.
run_id: i32,
},
/// Matched to `/tally` -> sends score board as message through Telegram.
#[command(description = "Tallies current medals and distances. Usage: /tally")]
Tally,
#[command(description = "Lists recent runs. Number of runs to display must be specified.")]
List { limit: u32 },
/// Matched to `/list <limit>` -> displays runs registered by the group chat, subject to a limit.
#[command(
description = "Lists recent runs. Number of runs to display must be specified. Usage: /list <num_runs_to_show>"
)]
List {
/// Limit to query from db.
limit: u32,
},
}

/// Function used for handling various commands matched.
async fn answer(bot: Bot, msg: Message, cmd: Command, db_connection: PgPool) -> ResponseResult<()> {
match cmd {
Command::Help => {
Expand Down
39 changes: 38 additions & 1 deletion src/database.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
//! Database operations.
//!
//! [sqlx](https://docs.rs/sqlx/latest/sqlx/) is used to interact with the
//! Postgresql database. Macros are used to check queries against the
//! database at compile time.
use crate::models::{Run, Score, User};
use sqlx::PgPool;
use teloxide::types::ChatId;
use tracing::error;

/// Convenience type to wrap a generic `Ok` and `sqlx::Error`.
type DBResult<T> = Result<T, sqlx::Error>;

/// Creates a user in users table.
///
/// Users are tied to the `chat_id` that the message came from
/// and the `user_name` input. This combination must be unique.
pub async fn create_user(user_name: &str, chat_id: ChatId, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"INSERT INTO users (chat_id, user_name)
Expand All @@ -19,6 +29,9 @@ pub async fn create_user(user_name: &str, chat_id: ChatId, connection: &PgPool)
Ok(())
}

/// Retrieves a user.
///
/// Fetches user information based on `(user_name, chat_id)`.
async fn get_user(user_name: &str, chat_id: ChatId, connection: &PgPool) -> DBResult<Option<User>> {
let user: Option<User> = sqlx::query_as!(
User,
Expand All @@ -34,6 +47,9 @@ async fn get_user(user_name: &str, chat_id: ChatId, connection: &PgPool) -> DBRe
Ok(user)
}

/// Fetchers users in a chat.
///
/// Retrieves users in a chat from `ChatId`.
pub async fn get_users_in_chat(
chat_id: ChatId,
connection: &PgPool,
Expand Down Expand Up @@ -61,7 +77,19 @@ pub async fn get_users_in_chat(
}
}

// TODO: refactor in the future
/// Wrapper for adding run data.
///
/// # Arguments
/// * `distance` - Distance run in km
/// * `user_name` - Name user wishes to tie the run to.
/// * `chat_id` - Unique ID identifying the chat, this comes from Telegram.
///
/// # Remarks
///
/// Due to design flaws, we first check whether that user has
/// added a run before. If not, we need to first create that user.
/// Afterwards, we run `get_user` again to retrieve its `user_id`.
/// Following which, we then actually add the run to the database.
pub async fn add_run_wrapper(
distance: f32,
user_name: &str,
Expand All @@ -85,6 +113,9 @@ pub async fn add_run_wrapper(
Ok(())
}

/// Adds run data.
///
/// Performs the actual database update for adding run data.
async fn add_run(distance: f32, user_id: i32, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"INSERT INTO runs (distance, user_id)
Expand All @@ -99,6 +130,9 @@ async fn add_run(distance: f32, user_id: i32, connection: &PgPool) -> DBResult<(
Ok(())
}

/// Fetches runs fromt the chat.
///
/// `limit` must be specified or the `answer` cannot match the enum.
pub async fn get_runs(
chat_id: ChatId,
limit: i64,
Expand Down Expand Up @@ -137,6 +171,7 @@ pub async fn get_runs(
}
}

/// Updates a certain run by id.
pub async fn update_run(run_id: i32, distance: f32, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"UPDATE runs
Expand All @@ -151,6 +186,7 @@ pub async fn update_run(run_id: i32, distance: f32, connection: &PgPool) -> DBRe
Ok(())
}

/// Deletes a run by id.
pub async fn delete_run(run_id: i32, connection: &PgPool) -> DBResult<()> {
sqlx::query!(
"DELETE FROM runs
Expand All @@ -163,6 +199,7 @@ pub async fn delete_run(run_id: i32, connection: &PgPool) -> DBResult<()> {
Ok(())
}

/// Aggregates runs into a tally (`Vec<Score>`)
pub async fn get_tally(chat_id: ChatId, connection: &PgPool) -> DBResult<Option<Vec<Score>>> {
let users = get_users_in_chat(chat_id, connection).await?;

Expand Down
18 changes: 18 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]

//! Telegram bot for a running contest between friends.
//!
//! Implements a telegram bot hosted on shuttle.rs (subject to change).
//! Shuttle provisions infrastructure from our infrastructure as code
//! that is used in this codebase.

mod bot;
mod database;
mod message;
Expand All @@ -8,6 +17,15 @@ use shuttle_secrets::SecretStore;
use sqlx::PgPool;
use teloxide::prelude::*;

/// Entry point to the telegram bot service.
///
/// We pass in the resources that we wish to provision as arguments to `shuttle_main()`.
/// As a requirement of shuttle.rs for provisioning a database, we run sqlx migrations
/// as the first step as well.
///
/// Next, we load in our telegram bot's key and as it is a requirement for teloxide.
///
/// Finally, we start our service.
#[shuttle_runtime::main]
async fn shuttle_main(
#[shuttle_secrets::Secrets] secrets: SecretStore,
Expand Down
29 changes: 29 additions & 0 deletions src/message.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
//! Templating and message preparation.
//!
//! This module reads in Rust-native objects and renders
//! them as `String`s using [askama](https://crates.io/crates/askama/0.7.2)
//! as the templating engine.

use crate::models::{Run, Score, User};
use askama::Template;
use std::fmt;
use std::ops;

/// NewType implementation so that Display can be implemented for it.
struct RunDisplay(Run);

/// Deref is implemented so that accessing `Run`'s contents is easier.
impl ops::Deref for RunDisplay {
type Target = Run;

Expand Down Expand Up @@ -42,12 +50,19 @@ impl fmt::Display for Score {
}
}

/// Struct Run display.
#[derive(Template)]
#[template(path = "list_runs.j2")]
struct ListRunTemplate<'a> {
/// Reference to `runs` for askama to access.
runs: &'a Vec<RunDisplay>,
}

/// Displays runs fetched from database.
///
/// Function takes in an `Option` and will check if any records have
/// been retrieved, else it will output that there are no runs
/// stored in the database.
pub fn list_runs(runs: Option<Vec<Run>>) -> String {
if let Some(runs) = runs {
let run_displays: Vec<RunDisplay> = runs.into_iter().map(RunDisplay).collect();
Expand All @@ -61,12 +76,19 @@ pub fn list_runs(runs: Option<Vec<Run>>) -> String {
}
}

/// Struct User display.
#[derive(Template)]
#[template(path = "list_users.j2")]
struct ListUserTemplate<'a> {
/// Reference to `users` for askama to access.
users: &'a Vec<User>,
}

/// Displays users fetched from database.
///
/// Function takes in an `Option` and will check if any records have
/// been retrieved, else it will output that there are no users
/// stored in the database.
pub fn list_users(users: Option<Vec<User>>) -> String {
if let Some(users) = users {
let user_template = ListUserTemplate { users: &users };
Expand All @@ -77,12 +99,19 @@ pub fn list_users(users: Option<Vec<User>>) -> String {
}
}

/// Struct Tally display.
#[derive(Template)]
#[template(path = "list_tally.j2")]
struct ListTallyTemplate<'a> {
/// Reference to `scores` for askama to access.
scores: &'a Vec<Score>,
}

/// Displays score aggregates fetched from database.
///
/// Function takes in an `Option` and will check if any records have
/// been retrieved, else it will output that there the tally
/// cannot be generated.
pub fn display_tally(scores: Option<Vec<Score>>) -> String {
if let Some(scores) = scores {
let tally_template = ListTallyTemplate { scores: &scores };
Expand Down
21 changes: 21 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
//! Struct models for database tables.
//!
//! Contains structs for an "ORM-like" approach to
//! database interactions.

use sqlx::types::chrono;

/// Represents a user row in the `users` table.
#[derive(sqlx::FromRow)]
pub struct User {
/// User id
pub id: i32,
/// Id of telegram chat
pub chat_id: String,
/// Self-specified username
pub user_name: String,
}

/// Represents a run row in the `runs` table.
#[derive(sqlx::FromRow)]
pub struct Run {
/// Run id
pub id: i32,
/// Distance ran for a particular run
pub distance: f32,
/// Datetime when the run was submitted to the database
pub run_datetime: Option<chrono::NaiveDateTime>,
/// User_id of the user who submitted the run
pub user_id: i32,
}

/// Represents a score that appears in the tally.
///
/// While this struct those not correspond direclty to a database
/// table, it is built directly from results retrieved.
pub struct Score {
/// Self-specified username
pub user_name: String,
/// Number of runs for the user, or in this case, medals
pub medals: u32,
/// Total distance run by the user
pub distance: f32,
}