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::borrow::Cow;

use modio::games::ApiAccessOptions;
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::guild::Permissions;
use twilight_util::builder::command::{CommandBuilder, StringBuilder, SubCommandBuilder};
use twilight_util::builder::embed::{
    EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder,
};
use twilight_util::builder::InteractionResponseDataBuilder;

use super::{create_response, defer_ephemeral, update_response_content};
use crate::bot::Context;
use crate::commands::autocomplete_games;
use crate::db::types::{GameId, GuildId};
use crate::error::Error;
use crate::util::IntoFilter;

pub fn commands() -> Vec<Command> {
    vec![
        CommandBuilder::new("about", "Get bot info", CommandType::ChatInput).build(),
        CommandBuilder::new(
            "settings",
            "Guild specific settings",
            CommandType::ChatInput,
        )
        .dm_permission(false)
        .default_member_permissions(Permissions::MANAGE_GUILD)
        .option(
            SubCommandBuilder::new("default-game", "Set the default game for `/mods` command")
                .option(
                    StringBuilder::new("value", "ID or search")
                        .required(true)
                        .autocomplete(true),
                ),
        )
        .build(),
    ]
}
pub async fn about(ctx: &Context, interaction: &Interaction) -> Result<(), Error> {
    let user = ctx.cache.current_user().unwrap();

    let version = if env!("GIT_SHA") == "UNKNOWN" {
        env!("CARGO_PKG_VERSION").to_string()
    } else {
        format!(
            "{} ([{}](https://github.com/nickelc/modio-bot/commit/{}))",
            env!("CARGO_PKG_VERSION"),
            env!("GIT_SHA_SHORT"),
            env!("GIT_SHA"),
        )
    };

    let embed = EmbedBuilder::new()
        .author(EmbedAuthorBuilder::new(user.name))
        .footer(EmbedFooterBuilder::new(format!(
            "Servers: {}",
            ctx.metrics.guilds.get()
        )))
        .field(
            EmbedFieldBuilder::new(
                "Invite to server",
                "[discordbot.mod.io](https://discordbot.mod.io)",
            )
            .inline(),
        )
        .field(
            EmbedFieldBuilder::new("mod.io Discord", "[discord.mod.io](https://discord.mod.io)")
                .inline(),
        )
        .field(
            EmbedFieldBuilder::new(
                "modbot Discord",
                "[discord.gg/XNX9665](https://discord.gg/XNX9665)",
            )
            .inline(),
        )
        .field(
            EmbedFieldBuilder::new(
                "Website/Blog",
                "[ModBot for Discord](https://blog.mod.io/modbot-for-discord-blog-52c07be0d828)",
            )
            .inline(),
        )
        .field(
            EmbedFieldBuilder::new(
                "Github",
                "[nickelc/modio-bot](https://github.com/nickelc/modio-bot)",
            )
            .inline(),
        )
        .field(EmbedFieldBuilder::new("Version", version).inline());

    let data = InteractionResponseDataBuilder::new().embeds([embed.build()]);

    create_response(ctx, interaction, data.build()).await?;

    Ok(())
}

pub async fn settings(
    ctx: &Context,
    interaction: &Interaction,
    command: &CommandData,
) -> Result<(), Error> {
    let filter = match command.options.as_slice() {
        [CommandDataOption {
            value: CommandOptionValue::SubCommand(commands),
            ..
        }] => match commands.as_slice() {
            [CommandDataOption {
                value: CommandOptionValue::Focused(value, _),
                ..
            }] => {
                return autocomplete_games(ctx, interaction, value).await;
            }
            [CommandDataOption {
                value: CommandOptionValue::String(s),
                ..
            }] => s.into_filter(),
            _ => unreachable!(),
        },

        _ => unreachable!(),
    };

    defer_ephemeral(ctx, interaction).await?;

    let game = ctx.modio.games().search(filter).first().await?;
    let guild_id = interaction.guild_id.expect("guild only command");

    let content: Cow<'_, str> = if let Some(game) = game {
        if game
            .api_access_options
            .contains(ApiAccessOptions::ALLOW_THIRD_PARTY)
        {
            ctx.settings.set_game(GuildId(guild_id), GameId(game.id))?;
            format!("Game is set to '{}'.", game.name).into()
        } else {
            let msg = format!(
                ":no_entry: Third party API access is disabled for '{}' but is required for the commands.",
                game.name
            );
            msg.into()
        }
    } else {
        "Game not found.".into()
    };

    update_response_content(ctx, interaction, &content).await
}