concord 2.4.4

A terminal user interface client for Discord
Documentation
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BuiltinSlashCommand {
    Gif,
    Tenor,
    Tts,
    Me,
    Tableflip,
    Unflip,
    Shrug,
    Spoiler,
    Nick,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BuiltinSlashCommandInfo {
    pub kind: BuiltinSlashCommand,
    pub name: &'static str,
    pub description: &'static str,
    pub replacement: &'static str,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BuiltinSlashCommandSubmit {
    Message { content: String, tts: bool },
    Nickname { nickname: String },
    Unsupported { message: String },
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BuiltinSlashCommandParse {
    Ready(BuiltinSlashCommandSubmit),
    Incomplete,
    NotBuiltin,
}

const BUILTIN_SLASH_COMMANDS: &[BuiltinSlashCommandInfo] = &[
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Gif,
        name: "gif",
        description: "Search for a GIF",
        replacement: "/gif ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Tenor,
        name: "tenor",
        description: "Search Tenor GIFs",
        replacement: "/tenor ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Tts,
        name: "tts",
        description: "Send a text-to-speech message",
        replacement: "/tts ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Me,
        name: "me",
        description: "Send an italic action message",
        replacement: "/me ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Tableflip,
        name: "tableflip",
        description: "Add a table flip face ((╯°□°)╯︵ ┻━┻)",
        replacement: "/tableflip ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Unflip,
        name: "unflip",
        description: "Add a table unflip face (┬─┬ ノ( ゜-゜ノ))",
        replacement: "/unflip ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Shrug,
        name: "shrug",
        description: r"Add a shrug face (¯\_(ツ)_/¯)",
        replacement: "/shrug ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Spoiler,
        name: "spoiler",
        description: "Send spoiler text",
        replacement: "/spoiler ",
    },
    BuiltinSlashCommandInfo {
        kind: BuiltinSlashCommand::Nick,
        name: "nick",
        description: "Change or clear your server nickname",
        replacement: "/nick ",
    },
];

pub fn builtin_slash_commands() -> &'static [BuiltinSlashCommandInfo] {
    BUILTIN_SLASH_COMMANDS
}

pub fn parse_builtin_slash_command(content: &str) -> BuiltinSlashCommandParse {
    let Some(rest) = content.strip_prefix('/') else {
        return BuiltinSlashCommandParse::NotBuiltin;
    };
    let Some((name, argument)) = split_command_name(rest) else {
        return BuiltinSlashCommandParse::NotBuiltin;
    };
    let Some(command) = BUILTIN_SLASH_COMMANDS
        .iter()
        .find(|command| command.name == name)
    else {
        return BuiltinSlashCommandParse::NotBuiltin;
    };

    match command.kind {
        BuiltinSlashCommand::Gif | BuiltinSlashCommand::Tenor => required_argument(argument)
            .map_or(BuiltinSlashCommandParse::Incomplete, |_| {
                BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Unsupported {
                    message: "GIF slash commands are not supported in Concord yet".to_owned(),
                })
            }),
        BuiltinSlashCommand::Tts => {
            required_argument(argument).map_or(BuiltinSlashCommandParse::Incomplete, |message| {
                BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Message {
                    content: message.to_owned(),
                    tts: true,
                })
            })
        }
        BuiltinSlashCommand::Me => {
            required_argument(argument).map_or(BuiltinSlashCommandParse::Incomplete, |message| {
                BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Message {
                    content: format!("_{message}_"),
                    tts: false,
                })
            })
        }
        BuiltinSlashCommand::Tableflip => {
            BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Message {
                content: append_optional_message(argument, "(╯°□°)╯︵ ┻━┻"),
                tts: false,
            })
        }
        BuiltinSlashCommand::Unflip => {
            BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Message {
                content: append_optional_message(argument, "┬─┬ ノ( ゜-゜ノ)"),
                tts: false,
            })
        }
        BuiltinSlashCommand::Shrug => {
            BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Message {
                content: append_optional_message(argument, r"¯\_(ツ)_/¯"),
                tts: false,
            })
        }
        BuiltinSlashCommand::Spoiler => {
            required_argument(argument).map_or(BuiltinSlashCommandParse::Incomplete, |message| {
                BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Message {
                    content: format!("||{message}||"),
                    tts: false,
                })
            })
        }
        BuiltinSlashCommand::Nick => {
            BuiltinSlashCommandParse::Ready(BuiltinSlashCommandSubmit::Nickname {
                nickname: argument.to_owned(),
            })
        }
    }
}

fn split_command_name(input: &str) -> Option<(&str, &str)> {
    let trimmed = input.trim_start();
    if trimmed.is_empty() {
        return None;
    }
    let name_end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
    let name = &trimmed[..name_end];
    let argument = trimmed[name_end..].trim_start();
    Some((name, argument))
}

fn required_argument(argument: &str) -> Option<&str> {
    (!argument.trim().is_empty()).then_some(argument)
}

fn append_optional_message(message: &str, suffix: &str) -> String {
    if message.is_empty() {
        suffix.to_owned()
    } else {
        format!("{message} {suffix}")
    }
}