modbot 0.5.3

Discord bot for https://mod.io. ModBot provides commands to search for mods and notifications about added & edited mods.
use std::fmt::Write;

use futures_util::{Stream, TryStreamExt};
use modio::filter::prelude::*;
use modio::games::Game;
use twilight_model::application::command::{Command, CommandType};
use twilight_model::application::interaction::application_command::{
    CommandData, CommandDataOption, CommandOptionValue,
};
use twilight_model::application::interaction::Interaction;
use twilight_model::channel::message::embed::EmbedField;
use twilight_model::channel::message::Embed;
use twilight_util::builder::command::{CommandBuilder, StringBuilder};
use twilight_util::builder::embed::{EmbedBuilder, ImageSource};

use super::{
    create_response, defer_response, update_response_content, update_response_from_content,
    EphemeralMessage,
};
use crate::bot::Context;
use crate::db::types::GuildId;
use crate::error::Error;
use crate::util::{ContentBuilder, IntoFilter};

pub fn commands() -> Vec<Command> {
    vec![
        CommandBuilder::new(
            "games",
            "List all games on <https://mod.io>",
            CommandType::ChatInput,
        )
        .option(StringBuilder::new("search", "ID or search"))
        .build(),
        CommandBuilder::new("game", "Display the default game.", CommandType::ChatInput)
            .dm_permission(false)
            .build(),
    ]
}

pub async fn games(
    ctx: &Context,
    interaction: &Interaction,
    command: &CommandData,
) -> Result<(), Error> {
    let filter = match command.options.as_slice() {
        [CommandDataOption {
            value: CommandOptionValue::String(s),
            ..
        }] => s.into_filter(),
        _ => Filter::default(),
    };

    defer_response(ctx, interaction).await?;

    let mut games = ctx.modio.games().search(filter).iter().await?;

    match games.size_hint() {
        (0, _) => update_response_content(ctx, interaction, "No games found.").await,
        (1, _) => {
            let game = games.try_next().await?;
            if let Some(game) = game {
                let embed = create_embed(game);

                ctx.interaction()
                    .update_response(&interaction.token)
                    .content(Some("Found 1 game."))?
                    .embeds(Some(&[embed]))?
                    .await?;

                Ok(())
            } else {
                update_response_content(ctx, interaction, "No games found.").await
            }
        }
        _ => {
            let games = games
                .try_fold(ContentBuilder::new(4000), |mut buf, game| {
                    let _ = writeln!(&mut buf, "{}. {}", game.id, game.name);
                    async { Ok(buf) }
                })
                .await?;

            update_response_from_content(ctx, interaction, "Games", &games.content).await
        }
    }
}

pub async fn game(ctx: &Context, interaction: &Interaction) -> Result<(), Error> {
    let game_id = match interaction.guild_id.map(GuildId) {
        Some(guild_id) => ctx.settings.game(guild_id)?.map(|id| id.0),
        _ => None,
    };

    let Some(game_id) = game_id else {
        let data = "Default game is not set.".into_ephemeral();

        return create_response(ctx, interaction, data).await;
    };

    defer_response(ctx, interaction).await?;

    let game = ctx.modio.game(game_id).get().await?;

    let embed = create_embed(game);

    ctx.interaction()
        .update_response(&interaction.token)
        .embeds(Some(&[embed]))?
        .await?;

    Ok(())
}

fn create_embed(game: Game) -> Embed {
    let mut embed = EmbedBuilder::new()
        .title(game.name)
        .url(game.profile_url.to_string())
        .description(game.summary)
        .image(ImageSource::url(game.logo.thumb_640x360).unwrap())
        .field(EmbedField {
            name: "Info".into(),
            value: format!(
                r#"**Id:** {}
**Name-Id:** {}
**Profile:** {}"#,
                game.id, game.name_id, game.profile_url,
            ),
            inline: true,
        });

    if let Some(stats) = game.stats {
        embed = embed.field(EmbedField {
            name: "Stats".into(),
            value: format!(
                r#"**Mods:** {}
**Subscribers:** {}
**Downloads:** {}"#,
                stats.mods_total, stats.subscribers_total, stats.downloads.total,
            ),
            inline: true,
        });
    }
    embed.build()
}