modbot 0.5.3

Discord bot for https://mod.io. ModBot provides commands to search for mods and notifications about added & edited mods.
#![deny(rust_2018_idioms)]
#![deny(clippy::pedantic)]
#![allow(
    clippy::module_name_repetitions,
    clippy::similar_names,
    clippy::wildcard_imports
)]

use std::path::PathBuf;

use dotenv::dotenv;
use futures_util::StreamExt;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::EnvFilter;

mod bot;
mod commands;
mod config;
mod db;
mod error;
mod metrics;
mod tasks;
mod util;

use db::init_db;
use metrics::Metrics;
use util::*;

const HELP: &str = "\
🤖 modbot. modbot. modbot.

USAGE:
  modbot [-c <config>]

OPTIONS:
  -c <config>       Path to config file

ENV:
  MODBOT_DEBUG_TIMESTAMP        Start time as Unix timestamp for polling the mod events
";

#[tokio::main]
async fn main() {
    if let Err(e) = try_main().await {
        tracing::error!("{e}");
        std::process::exit(1);
    }
}

async fn try_main() -> CliResult {
    dotenv().ok();
    let filter = EnvFilter::builder()
        .with_default_directive(LevelFilter::INFO.into())
        .from_env_lossy();
    tracing_subscriber::fmt().with_env_filter(filter).init();

    let mut args = pico_args::Arguments::from_env();
    if args.contains(["-h", "--help"]) {
        println!("{HELP}");
        std::process::exit(0);
    }

    let path = args
        .opt_value_from_os_str("-c", |s| PathBuf::try_from(s))?
        .unwrap_or_else(|| PathBuf::from("bot.toml"));

    let config = config::load_from_file(&path)
        .map_err(|e| format!("Failed to load config {path:?}: {e}"))?;

    let metrics = Metrics::new()?;
    let pool = init_db(&config.bot.database_url)?;
    let modio = init_modio(&config)?;

    let (cluster, mut events, context) =
        bot::initialize(&config, modio, pool, metrics.clone()).await?;

    if let Some(cmd) = args.subcommand()? {
        match cmd {
            cmd if cmd == "check" => {
                check_subscriptions(&context).await?;
            }
            cmd => {
                eprintln!("unknown subcommand: {cmd:?}");
            }
        }
        std::process::exit(0);
    }

    tokio::spawn(metrics::serve(&config.metrics, metrics));
    tokio::spawn(tasks::events::task(context.clone()));

    if let Some(token) = config.bot.dbl_token {
        tracing::info!("Spawning DBL task");
        let bot = context.application.id.get();
        let metrics = context.metrics.clone();
        tokio::spawn(tasks::dbl::task(bot, metrics, &token)?);
    }

    cluster.up().await;

    tokio::spawn(async move {
        tokio::signal::ctrl_c()
            .await
            .expect("failed to listen to ctrlc event.");
        tracing::info!("Shutting down cluster");
        cluster.down();
    });

    while let Some((_, event)) = events.next().await {
        let context = context.clone();
        tokio::spawn(bot::handle_event(event, context));
    }

    Ok(())
}