~phate/rsPhate

7f0e4f5c023dd487da7e35fafb73c6976729c0c6 — Ash 1 year, 3 months ago
Initial commit
A  => .env +1 -0
@@ 1,1 @@
RUST_LOG=info
\ No newline at end of file

A  => .gitignore +1 -0
@@ 1,1 @@
/target

A  => Cargo.toml +18 -0
@@ 1,18 @@
[package]
name = "rsPhate"
version = "0.1.0"
authors = ["Ash <valleyknight@protonmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
date_time = "2.1.0"
env_logger = "0.6"
kankyo = "0.2"
log = "0.4"
mpd = "0.0.12"
owoify = "0.1.5"
rand = "0.7"
rofl = "0.0.1"
serenity = "0.8"
\ No newline at end of file

A  => README.md +55 -0
@@ 1,55 @@
# rsPhate

[![made-with-emacs](https://img.shields.io/badge/made%20with-emacs-993399.svg)](https://www.gnu.org/software/emacs/) ![lines of code](https://tokei.rs/b1/github/Phate6660/rsPhate?category=code)

![bot](images/bot.png?raw=true "bot")

A Discord bot written in Rust.

Anything with a "*" denotes an entry in the notes section.

How to use:

- `export DISCORD_TOKEN=token` (obtain your token from the bot section of the developers part of discord's website).
- `cargo run` or `cargo run --release` from the root dir of the repo, or at least, the `.env` and `scripts/` must be in the CWD.
- Args are delimited with `,` so make sure commands are ran like this: `^command arg,arg`.

## Current Commands

- `^about`: Bot will reply with pretty embed containing title and description of bot, as well as where to find the author (me).
- `^date`: Bot will reply with current date and time in this format -- `06:30 AM | Thu 21, May of 2020`.
- `^embed title,desc,<image_link>`: Bot will generate an embed based on input and send it.*
- `^help`: Without args, bot will give a generalized help. Specifying a command or function will explain it.
- `^fortune`: Bot will reply with random fortune from `fortune-mod-mythical-linux` (repo is pinned to my profile if anyone is interested).*
- `^git site,owner/repo`: Bot will reply with full link to repo.
- `^hmm`: How much music does Phate have?
- `^iv SEARCH`: Bot will reply with an invidio link of the search query.*
- `^math operation,num,num`: Bot will do math for you (basic add/sub/div/mul) and reply with the result.*
- `^meme position,text`: Bot will generate a meme based on the input and send it as an image.
- `^owo input`: Bot will reply with OwO-ified input.
- `^projects`: Bot willk reply with pretty embed containing links to other projects by the author.
- `^rng min,max`: Bot will generate a random number between min and max and reply with the result.
- `^rr`: Bot will reply with a link (without a link preview) to Rick Astley's "Never Gonna Give You Up".
- `^wipltrn`: Bot will reply with pretty embed containing music info and cover art of Phate's currently playing music.*
- `^ww {apple,steam,systemd}`: Bot will reply with pretty embed explaining why the topic is bad.

## Automated functions

- If a message contains "youtube.com", react with disappointed face and send pretty embed full of info on invidio.us.
- If a message contains "twitter.com", do the same as with messages containing "youtube.com" except the embed pertains to nitter.net.

## Bot structure

- `.env`: For environmental variables.
- `scripts/`: Contains scripts to be ran for certain commands.
- `src/main.rs`: The main file for the bot, it's responsible for everything except for defining the commands.
- `src/commands/*` (except `mod.rs`): Define the commands to be used.
- `src/commands/mod.rs`: For making the command files in `src/commands/` public for use in `src/main.rs`.

## Notes

- `embed`: Image link is optional, it will be replaced with a 1px transparent image if not supplied, so it'll be like it's not even there. Also, don't forget to place your image link in brackets (<>) to disable the preview of them beforehand.
- `fortune`: Potentially NSFW and offensive fortunes are *enabled*, use at your own risk.
- `math`: Operation refers to `multiply`/`divide`/`add`/`subtract`. Example: `^math add,1,1` will make the bot reply with `2`.
- `iv`: Requires `youtube-dl`. Example: `^iv type o negative dead again` will cause the bot to reply with an invidio link to the full album.
- `wipltrn`: Requires `mpc`, and for the cover art to be present at `/tmp/cover.png`.

A  => images/bot.png +0 -0
A  => scripts/hmm +11 -0
@@ 1,11 @@
#!/bin/bash

MUSIC="/mnt/ehdd2/Music/"
case "$1" in
    artists) echo "$(mpc list Artist | sed '/^\s*$/d' | wc -l)";;
    albums) echo "$(mpc list Album | sed '/^\s*$/d' | wc -l)";;
    songs) echo "$(mpc list Title | sed '/^\s*$/d' | wc -l)";;
    files) echo "$(tree -a $MUSIC | tail -n1 | awk -F\  '{print $3}')";;
    size) echo "$(du -hs $MUSIC | cut -c 1-4)B";;
    amount) echo "$(w3m -dump https://libre.fm/user/phate6660/stats | grep Total | sed "s/[^0-9]//g")";;
esac

A  => src/#main.rs# +228 -0
@@ 1,228 @@
use log::{error, info};
use serenity::{
    client::bridge::gateway::ShardManager,
    framework::standard::{
        help_commands,
        macros::{check, group, help},
        Args, CheckResult, CommandGroup, CommandOptions, CommandResult,
        DispatchError::CheckFailed,
        HelpOptions, StandardFramework,
    },
    model::{gateway::Ready, id::UserId, prelude::Message},
    prelude::*,
};
use std::{
    collections::{HashMap, HashSet},
    env,
    sync::Arc,
};

// Load and use commands from src/commands/
mod commands;
use commands::{
    about::*, date::*, embed::*, fortune::*, git::*, hmm::*, iv::*, math::*, meme::*, owo::*,
    projects::*, quit::*, rng::*, rr::*, star::*, wipltrn::*, ww::*,
};

// Load and use extra functions from src/functions/
mod functions;
use functions::{prefix_space_check::prefix_space_check, yt_tw_check::yt_tw_check};

// A container type is created for inserting into the Client's `data`, which
// allows for data to be accessible across all events and framework commands, or
// anywhere else that has a copy of the `data` Arc.
struct ShardManagerContainer;

impl TypeMapKey for ShardManagerContainer {
    type Value = Arc<Mutex<ShardManager>>;
}

struct CommandCounter;

impl TypeMapKey for CommandCounter {
    type Value = HashMap<String, u64>;
}

struct Handler;

impl EventHandler for Handler {
    // Set a handler for the `message` event - so that whenever a new message
    // is received - the closure (or function) passed will be called.
    //
    // Event handlers are dispatched through a threadpool, and so multiple
    // events can be dispatched simultaneously.
    //
    // Set a handler to be called on the `ready` event. This is called when a
    // shard is booted, and a READY payload is sent by Discord. This payload
    // contains data like the current user's guild Ids, current user data,
    // private channels, and more.
    //
    // In this case, just print what the current user's username is.
    fn ready(&self, ctx: Context, ready: Ready) {
        use serenity::model::gateway::Activity;
        use serenity::model::user::OnlineStatus;

        let activity = Activity::playing("^help for help");
        let status = OnlineStatus::Online;

        ctx.set_presence(Some(activity), status);
        info!("Connected as {}", ready.user.name);
    }
}

// Groups
#[group]
#[description = "Functions for the bot that do not belong in any specific category."]
#[commands(date, hmm, iv, fortune, meme, rr, wipltrn, ww)]
struct Functions;

#[group]
#[description = "Generalized functions for the bot."]
#[commands(about, projects, quit)]
struct General;

#[group]
#[description = "Functions that are related to number operations."]
#[commands(math, rng)]
struct Numbers;

#[group]
#[description = "Functions that are related to message operations."]
#[commands(embed, git, owo, star)]
struct Messages;

#[help]
#[individual_command_tip = "`^help` | `^help command` | `^help group`\nNOTE: Args are delimited via `,`."]
fn my_help(
    context: &mut Context,
    msg: &Message,
    args: Args,
    help_options: &'static HelpOptions,
    groups: &[&'static CommandGroup],
    owners: HashSet<UserId>,
) -> CommandResult {
    help_commands::with_embeds(context, msg, args, help_options, groups, owners)
}

fn main() {
    // This will load the environment variables located at `./.env`, relative to the CWD.
    kankyo::load().expect("Failed to load .env file");

    // Initialize the logger to use environment variables.
    env_logger::init();

    // Configure the client with your Discord bot token in the environment.
    let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");

    // Create a new instance of the Client, logging in as a bot. This will
    // automatically prepend your bot token with "Bot ", which is a requirement
    // by Discord for bot users.
    let mut client = Client::new(&token, Handler).expect("Err creating client");

    {
        let mut data = client.data.write();
        data.insert::<CommandCounter>(HashMap::default());
        data.insert::<ShardManagerContainer>(Arc::clone(&client.shard_manager));
    }

    client.with_framework(
        // Configures the client, allowing for options to mutate how the
        // framework functions.
        //
        // Refer to the documentation for
        // `serenity::ext::framework::Configuration` for all available
        // configurations.
        StandardFramework::new()
            .configure(|c| {
                c.with_whitespace(true)
                    .prefix("^")
                    // Delimiters are: " ", ", ", and ",".
                    .delimiters(vec![","])
            })
            // Set a function to be called prior to each command execution. This
            // provides the context of the command, the message that was received,
            // and the full name of the command that will be called.
            //
            // You can not use this to determine whether a command should be
            // executed. Instead, the `#[check]` macro gives you this functionality.
            .before(|ctx, msg, command_name| {
                info!(
                    "command: '{}', user: '{}'",
                    command_name, msg.author.name
                );

                // Increment the number of times this command has been run once. If
                // the command's name does not exist in the counter, add a default
                // value of 0.
                let mut data = ctx.data.write();
                let counter = data
                    .get_mut::<CommandCounter>()
                    .expect("Expected CommandCounter in ShareMap.");
                let entry = counter.entry(command_name.to_string()).or_insert(0);
                *entry += 1;

                true // if `before` returns false, command processing doesn't happen.
            })
            // Similar to `before`, except will be called directly _after_
            // command execution.
            .after(|_, _, command_name, error| match error {
                Ok(()) => info!("Processed command '{}'", command_name),
                Err(why) => info!("Command '{}' returned error {:?}", command_name, why),
            })
            // Set a function that's called whenever an attempted command-call's
            // command could not be found.
            .unrecognised_command(|ctx, msg, unknown_command_name| {
                prefix_space_check(ctx, msg, unknown_command_name);
            })
            .normal_message(|ctx, msg| {
                yt_tw_check(ctx, msg);
            })
            // Set a function that's called whenever a command's execution didn't complete for one
            // reason or another. For example, when a user has exceeded a rate-limit or a command
            // can only be performed by the bot owner.
            .on_dispatch_error(|ctx, msg, error| match error {
                CheckFailed(Owner, owner_check) => {
                    msg.reply(
                        &ctx.http,
                        "Owner check failed! I will ping you a hundredfold if you do that again! <:sadgry:676458405342216195>",
                    );
                }
                _ => {
                    error!("Unhandled dispatch error!");
                }
            })
            // Set the help function
            .help(&MY_HELP)
            // The `#[group]` macro generates `static` instances of the options set for the group.
            // They're made in the pattern: `#name_GROUP` for the group instance and `#name_GROUP_OPTIONS`.
            // #name is turned all uppercase
            .group(&FUNCTIONS_GROUP)
            .group(&GENERAL_GROUP)
            .group(&NUMBERS_GROUP)
            .group(&MESSAGES_GROUP),
    );

    if let Err(why) = client.start() {
        error!("Client error: {:?}", why);
    }
}

#[check]
#[name = "Owner"]
fn owner_check(_: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> CheckResult {
    // Replace 7 with your ID to make this check pass.
    //
    // `true` will convert into `CheckResult::Success`,
    //
    // `false` will convert into `CheckResult::Failure(Reason::Unknown)`,
    //
    // and if you want to pass a reason alongside failure you can do:
    // `CheckResult::new_user("Lacked admin permission.")`,
    //
    // if you want to mark it as something you want to log only:
    // `CheckResult::new_log("User lacked admin permission.")`,
    //
    // and if the check's failure origin is unknown you can mark it as such (same as using `false.into`):
    // `CheckResult::new_unknown()`
    (msg.author.id == 534379378432540675).into()
}

A  => src/commands/about.rs +48 -0
@@ 1,48 @@
use serenity::{
    framework::standard::{macros::command, CommandError, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will reply with pretty embed containing title and description of bot, as well as where to find the author."]
fn about(ctx: &mut Context, msg: &Message) -> CommandResult {
    // Obtain Bot's profile pic: cache -> current info -> bot user -> bot icon
    let cache_http = &ctx.http;
    let current_info = match cache_http.get_current_application_info() {
        Ok(c) => c,
        Err(err) => return Err(CommandError(err.to_string())),
    };
    let bot_user = match current_info.id.to_user(cache_http) {
        Ok(u) => u,
        Err(err) => return Err(CommandError(err.to_string())),
    };
    let bot_icon = match bot_user.avatar_url() {
        Some(u) => u,
        None => bot_user.default_avatar_url(),
    };

    let msg = msg.channel_id.send_message(&ctx.http, |m| {
        m.embed(|e| {
            e.title("`rsPhate`");
            e.description("A bot with random and probably nonsensical features.\nWhere is the author, Phate6660?");
            e.thumbnail(bot_icon);
            // false = not inline
            e.fields(vec![
                ("Scripting/Programming", "[Codeberg](https://codeberg.org/Phate6660), [Github](https://github.com/Phate6660)", false),
                ("Social", "[Reddit](https://reddit.com/u/Valley6660), [Lobsters](https://lobste.rs/u/Phate6660)", false),
                ("Personal Site", "https://pages.codeberg.org/Phate6660/", false),
                ("Discord", "Phate6660#6270", false),
                ("Source Code", "[Phate6660/rsPhate](https://codeberg.org/Phate6660/rsPhate)", false),
            ]);
            e
        });
        m
    });

    if let Err(why) = msg {
        println!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/date.rs +21 -0
@@ 1,21 @@
use date_time::{date_tuple::DateTuple, time_tuple::TimeTuple};
use log::error;
use serenity::{
    framework::standard::{macros::command, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Display the date in format: `14:47 | 28 May 2020`."]
fn date(ctx: &mut Context, msg: &Message) -> CommandResult {
    let date = DateTuple::today().to_readable_string(); // dd mmm yyyy
    let time = TimeTuple::now().to_hhmm_string(); // 00:00, 24-hour time
    let date = time + &" | ".to_string() + &date; // example output: 14:47 | 28 May 2020

    if let Err(why) = msg.channel_id.say(&ctx.http, date) {
        error!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/embed.rs +36 -0
@@ 1,36 @@
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will generate an embed based on input."]
#[usage = "title description <image_link>"]
#[example = "^embed,rsPhate can generate embeds!,<https://website.com/path/to/image.ext>"]
fn embed(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
    let title = args.single::<String>()?;
    let description = args.single::<String>()?;
    let mut image = args.single::<String>().unwrap_or("false".to_string());
    
    let link = if image == "false" {
        "https://i.imgur.com/pMBcpoq.png".to_string()
    } else {
        image.replace("<", "").replace(">", "")
    };
    
    let msg = msg.channel_id.send_message(&ctx.http, |m| {
        m.embed(|e| {
            e.title(title);
            e.description(description);
            e.image(link)
        });
        m
    });

    if let Err(why) = msg {
        println!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/fortune.rs +29 -0
@@ 1,29 @@
use serenity::{
    framework::standard::{macros::command, CommandResult},
    model::channel::Message,
    prelude::*,
    utils::MessageBuilder,
};
use std::process::Command;

#[command]
#[description = "Bot will reply with random fortune from `fortune-mod-mythical-linux` (repo is pinned to my profile if anyone is interested). (Note: Potentially NSFW and offensive fortunes are *enabled*, use at your own risk.)"]
fn fortune(ctx: &mut Context, msg: &Message) -> CommandResult {
    let fortune = Command::new("fortune")
        .arg("mythical_linux")
        .arg("off/mythical_linux")
        .output()
        .expect("Could not obtain fortune.");

    let response = MessageBuilder::new()
        .push("```\n")
        .push(String::from_utf8_lossy(&fortune.stdout))
        .push("\n```")
        .build();

    if let Err(why) = msg.channel_id.say(&ctx.http, response) {
        println!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/git.rs +46 -0
@@ 1,46 @@
use log::{error, info};
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    model::channel::Message,
    prelude::*,
    utils::MessageBuilder,
};

#[command]
#[description = "Bot will parse the input and output the correct full link to the repo."]
#[usage = "site,user/repo"]
#[example = "github,rsfetch/rsfetch"]
#[example = "gitlab,ArcticTheRogue/asgl"]
#[example = "codeberg,Phate6660/rsPhate"]
#[num_args(2)]
fn git(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
    let site = args.single::<String>()?;
    let repo = args.single::<String>()?;
    let match_site = site.as_str();

    // Log what was supplied
    info!("site: {}, owner/repo: {}", site, repo);

    // Match for site to create message.
    let message: String = match match_site {
        "github" => MessageBuilder::new()
            .push("https://github.com/")
            .push(repo)
            .build(),
        "gitlab" => MessageBuilder::new()
            .push("https://gitlab.com/")
            .push(repo)
            .build(),
        "codeberg" => MessageBuilder::new()
            .push("https://codeberg.org/")
            .push(repo)
            .build(),
        _ => "Could not generate a full link, please try again.".to_string(),
    };

    if let Err(why) = msg.channel_id.say(&ctx.http, &message) {
        error!("Could not push full Git repo link because: {}", why);
    }

    Ok(())
}

A  => src/commands/hmm.rs +56 -0
@@ 1,56 @@
use serenity::{
    framework::standard::{macros::command, CommandResult},
    model::channel::Message,
    prelude::*,
};
use std::process::Command;

#[command]
#[description = "How much music does Phate have?"]
fn hmm(ctx: &mut Context, msg: &Message) -> CommandResult {
    let artists = Command::new("scripts/hmm")
        .arg("artists")
        .output()
        .expect("Could not obtain amount of artists.");
    let albums = Command::new("scripts/hmm")
        .arg("albums")
        .output()
        .expect("Could not obtain amount of albums.");
    let songs = Command::new("scripts/hmm")
        .arg("songs")
        .output()
        .expect("Could not obtain amount of songs.");
    let files = Command::new("scripts/hmm")
        .arg("files")
        .output()
        .expect("Could not obtain amount of files.");
    let size = Command::new("scripts/hmm")
        .arg("size")
        .output()
        .expect("Could not obtain size of music collection.");
    let amount = Command::new("scripts/hmm")
        .arg("amount")
        .output()
        .expect("Could not obtain amount of times played.");
    let msg = msg.channel_id.send_message(&ctx.http, |m| {
        m.embed(|e| {
            e.title("`^hmm`");
            e.description("How Much Music (Does Phate Have?)");
            e.fields(vec![
                ("Artists", String::from_utf8_lossy(&artists.stdout), true),
                ("Albums", String::from_utf8_lossy(&albums.stdout), true),
                ("Songs", String::from_utf8_lossy(&songs.stdout), true),
                ("Files", String::from_utf8_lossy(&files.stdout), true),
                ("Size of Collection", String::from_utf8_lossy(&size.stdout), true),
                ("Amount of Songs Played", String::from_utf8_lossy(&amount.stdout), true),
            ]);
            e
        });
        m
    });

    if let Err(why) = msg {
        println!("Error sending message: {:?}", why);
    }
    Ok(())
}

A  => src/commands/iv.rs +37 -0
@@ 1,37 @@
use log::{error, info};
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    model::channel::Message,
    prelude::*,
};
use std::process::Command;

#[command]
#[description = "Bot will reply with an invidio link of the search query."]
#[usage = "search"]
#[example = "mindless self indulgence unsociable"]
fn iv(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult {
    let mut args = args.rest();
    info!("search query: {}", args);
    let args = str::replace(args, " ", "+");
    info!("ytdl search query: {}", args);
    let args: String = "ytsearch:".to_string() + &args.to_string();

    // todo: somehow obtain video id through a crate or something, maybe somebody made a youtube-dl crate?
    let id = Command::new("youtube-dl")
        .arg("--get-id")
        .arg(args)
        .output()
        .expect("Could not generate link.");
    info!("video id: {}", String::from_utf8_lossy(&id.stdout));

    let link: String =
        "https://invidio.us/watch?v=".to_string() + &String::from_utf8_lossy(&id.stdout);
    info!("invidio link: {}", link);

    if let Err(why) = msg.channel_id.say(&ctx.http, link) {
        error!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/math.rs +84 -0
@@ 1,84 @@
use log::{error, info};
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will do math for you (basic add/sub/div/mul) and reply with the result."]
#[usage = "operation num num"]
#[example = "multiply 2 1"]
#[example = "divide 2 1"]
#[example = "add 1 1"]
#[example = "subtract 3 1"]
#[num_args(3)]
fn math(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
    let pre_operation = args.single::<String>()?; // First argument, ensure it's a string
    let post_operation = pre_operation.as_str();

    match post_operation {
        "multiply" => {
            info!("Got operation: {}", pre_operation);
            let first_number = args.single::<i64>()?; // Second argument
            let second_number = args.single::<i64>()?; // Third argument

            let output = first_number * second_number;
            if let Err(why) = msg.channel_id.say(&ctx.http, &output.to_string()) {
                error!(
                    "Err sending product of {:?} and {:?}: {:?}",
                    first_number, second_number, why
                );
            }
        }
        "divide" => {
            info!("Got operation: {}", pre_operation);
            let first_number = args.single::<i64>()?; // Second argument
            let second_number = args.single::<i64>()?; // Third argument

            let output = first_number / second_number;
            if let Err(why) = msg.channel_id.say(&ctx.http, &output.to_string()) {
                error!(
                    "Err sending quotient of {:?} divided by {:?}: {:?}",
                    first_number, second_number, why
                );
            }
        }
        "add" => {
            info!("Got operation: {}", pre_operation);
            let first_number = args.single::<i64>()?; // Second argument
            let second_number = args.single::<i64>()?; // Third argument

            let output = first_number + second_number;
            if let Err(why) = msg.channel_id.say(&ctx.http, &output.to_string()) {
                error!(
                    "Err sending addition of {:?} by {:?}: {:?}",
                    first_number, second_number, why
                );
            }
        }
        "subtract" => {
            info!("Got operation: {}", pre_operation);
            let first_number = args.single::<i64>()?; // Second argument
            let second_number = args.single::<i64>()?; // Third argument

            let output = first_number - second_number;
            if let Err(why) = msg.channel_id.say(&ctx.http, &output.to_string()) {
                error!(
                    "Err sending subtraction of {:?} by {:?}: {:?}",
                    first_number, second_number, why
                );
            }
        }
        _ => {
            let message: String =
                "Unknown math operation or user error has occured, please try again.".to_string();
            error!("{}", message);
            if let Err(why) = msg.channel_id.say(&ctx.http, message) {
                error!("Could not push error message because: {}", why);
            }
        }
    }

    Ok(())
}

A  => src/commands/meme.rs +65 -0
@@ 1,65 @@
use log::{error, info};
use rofl;
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    http::AttachmentType,
    model::channel::Message,
    prelude::*,
};
use std::{fs, io::Write, path::Path};

#[command]
#[description = "Bot will generate a meme based on input."]
#[usage = "top bottom template"]
#[example = "ah yes"]
#[example = "ah yes,enslaved meme generator"]
#[example = "ah yes,enslaved meme generator,anditsgone"]
fn meme(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
    let meme = args.single::<String>()?;
    let meme2 = args.single::<String>().unwrap_or("empty".to_string());
    let template = args.single::<String>().unwrap_or("zoidberg".to_string());

    let pos = if meme2 == "empty" {
        vec![rofl::Caption::text_at(rofl::VAlign::Top, meme)]
    } else {
        vec![
            rofl::Caption::text_at(rofl::VAlign::Top, meme),
            rofl::Caption::text_at(rofl::VAlign::Bottom, meme2),
        ]
    };
    let engine = rofl::Engine::new(
        "/home/valley/downloads/git/rofld/data/templates",
        "/home/valley/downloads/git/rofld/data/fonts",
    );
    let image_macro = rofl::ImageMacro {
        template: template.into(),
        captions: pos,
        ..rofl::ImageMacro::default()
    };
    let output = engine.caption(image_macro)?;
    info!("meme has been generated");

    let mut file = fs::OpenOptions::new()
        .write(true)
        .open("/tmp/meme.png")?;
    file.write_all(&*output)?;
    info!("meme has been written to /tmp/meme.png");

    let msg = msg.channel_id.send_message(&ctx.http, |m| {
        m.embed(|e| {
            e.title("rofl meme generator");
            e.image("attachment://meme.png");
            e
        });
        m.add_file(AttachmentType::Path(Path::new(
            "/tmp/meme.png",
        )));
        m
    });

    if let Err(why) = msg {
        error!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/mod.rs +20 -0
@@ 1,20 @@
// These are for making everything in `src/commands/*`
// available in `src/main.rs` via `mod commands;`.
// (Pssst, don't forget to `use` them as well.)
pub mod about;
pub mod date;
pub mod embed;
pub mod fortune;
pub mod git;
pub mod hmm;
pub mod iv;
pub mod math;
pub mod meme;
pub mod owo;
pub mod projects;
pub mod quit;
pub mod rng;
pub mod rr;
pub mod star;
pub mod wipltrn;
pub mod ww;

A  => src/commands/owo.rs +22 -0
@@ 1,22 @@
use log::error;
use owoify::OwOifiable;
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will reply with OwO-ified version of input."]
#[usage = "input"]
#[example = "// kill me, please -- phate"]
fn owo(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult {
    let input = String::from(args.rest());
    let owo_text = input.owoify();

    if let Err(why) = msg.channel_id.say(&ctx.http, owo_text) {
        error!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/projects.rs +36 -0
@@ 1,36 @@
use serenity::{
    framework::standard::{macros::command, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot willk reply with pretty embed containing links to other projects by the author."]
fn projects(ctx: &mut Context, msg: &Message) -> CommandResult {
    let msg = msg.channel_id.send_message(&ctx.http, |m| {
        m.embed(|e| {
            e.title("Other Projects Created/Co-Created by The Author");
            e.fields(vec![
                ("rsfetch", "https://github.com/rsfetch/rsfetch", false),
                ("pkg", "https://github.com/Phate6660/pkg", false),
                ("p6nc-overlay", "https://github.com/p6nc/overlay", false),
                ("bindings", "https://github.com/Phate6660/bindings", false),
                ("sxhkd-bindings", "https://github.com/Phate6660/sxhkd-bindings", false),
                ("rust-server", "https://github.com/Phate6660/rust-server", false),
				("WBMPFMPD", "https://github.com/Phate6660/WBMPFMPD", false),
				("valleyTERM", "https://github.com/Phate6660/term", false),
				("FFNIFLFYTU", "https://github.com/Phate6660/FFNIFDBDFYTU", false),
				("cfg", "https://github.com/Phate6660/cfg", false),
				("fet", "https://github.com/Phate6660/fet", false),
            ]);
            e
        });
        m
    });

    if let Err(why) = msg {
        println!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/quit.rs +30 -0
@@ 1,30 @@
use crate::ShardManagerContainer;
use crate::OWNER_CHECK;
use serenity::{
    framework::standard::{macros::command, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[checks(Owner)]
#[aliases(exit, foad)]
fn quit(ctx: &mut Context, msg: &Message) -> CommandResult {
    let data = ctx.data.read();
    let message = &msg.content;

    if let Some(manager) = data.get::<ShardManagerContainer>() {
        // Shush, it'll make me feel better when I can't get the bot to work how I want.
        if message.contains("^foad") {
            let _ = msg.reply(&ctx, "I'm terribly sorry for being a failure. Expunging myself to robot hell as we speak.");
        } else {
            let _ = msg.reply(&ctx, "Hai, Phate-senpai~");
        }
        manager.lock().shutdown_all();
    } else {
        let _ = msg.reply(&ctx, "There was a problem getting the shard manager");
        return Ok(());
    }

    Ok(())
}

A  => src/commands/rng.rs +61 -0
@@ 1,61 @@
use log::error;
use rand::Rng;
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will generate a random number between min and max and reply with the result."]
#[usage = "min max"]
#[example = "-999 999"]
fn rng(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
    fn min_and_max(mut a: Args) -> (i64, i64) {
        let min = a.single::<i64>().unwrap_or(9223372036854775807);
        let max = a.single::<i64>().unwrap_or(9223372036854775807);
        (min, max)
    }

    let (min, max) = min_and_max(args);
    let message: String = "Could not generate a random number, please try again.".to_string();
    if min == 9223372036854775807 {
        let message_specific: String = "The min value supplied is invalid.".to_string();
        error!("{}", message_specific);
        msg.channel_id.say(&ctx.http, message_specific);
        msg.channel_id.say(&ctx.http, message);
        return Ok(());
    } else if max == 9223372036854775807 {
        let message_specific: String = "The max value supplied is invalid.".to_string();
        error!("{}", message_specific);
        msg.channel_id.say(&ctx.http, message_specific);
        msg.channel_id.say(&ctx.http, message);
        return Ok(());
    } else if min == max {
        let message_specific: String =
            "The min value must not be equal to, or greater than, the max value.".to_string();
        error!("{}", message_specific);
        msg.channel_id.say(&ctx.http, message_specific);
        msg.channel_id.say(&ctx.http, message);
        return Ok(());
    } else if min > max {
        let message_specific: String =
            "The min value must be lower than the max value.".to_string();
        error!("{}", message_specific);
        msg.channel_id.say(&ctx.http, message_specific);
        msg.channel_id.say(&ctx.http, message);
        return Ok(());
    }

    let random_number = rand::thread_rng().gen_range(min, max);

    if let Err(why) = msg.channel_id.say(&ctx.http, &random_number.to_string()) {
        error!("Could not send randomly generated number because: {}", why);
        msg.channel_id.say(
            &ctx.http,
            "Could not generate a random number, please try again.",
        );
    }

    Ok(())
}

A  => src/commands/rr.rs +18 -0
@@ 1,18 @@
use serenity::{
    framework::standard::{macros::command, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will reply with a link (without a link preview) to Rick Astley's \"Never Gonna Give You Up\"."]
fn rr(ctx: &mut Context, msg: &Message) -> CommandResult {
    if let Err(why) = msg.channel_id.say(
        &ctx.http,
        "<https://invidio.us/watch?v=dQw4w9WgXcQ&local=1>",
    ) {
        println!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/star.rs +22 -0
@@ 1,22 @@
use log::{error, info};
use serenity::{
    framework::standard::{macros::command, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will star the user's message. Useful for starring your own messages with Hoshi bot."]
#[usage = "message"]
#[example = "message goes here"]
fn star(ctx: &mut Context, msg: &Message) -> CommandResult {
    info!(
        "starring the message: {}, sent by user: {}",
        msg.content, msg.author.name
    );
    if let Err(why) = msg.react(&ctx.http, '⭐') {
        error!("could not star the message");
    }

    Ok(())
}

A  => src/commands/wipltrn.rs +37 -0
@@ 1,37 @@
use mpd::{Client, Song};
use serenity::{
    framework::standard::{macros::command, CommandResult},
    http::AttachmentType,
    model::channel::Message,
    prelude::*,
};
use std::path::Path;

#[command]
#[description = "Bot will reply with pretty embed containing current music info and cover art of what Phate is listening to."]
fn wipltrn(ctx: &mut Context, msg: &Message) -> CommandResult {
    let mut c = Client::connect("127.0.0.1:6600").unwrap();
    let song: Song = c.currentsong().unwrap().unwrap();
    let tit = song.title.as_ref().unwrap();
    let art = song.tags.get("Artist").unwrap();
    let alb = song.tags.get("Album").unwrap();
    let dat = song.tags.get("Date").unwrap();
    let mus_title = art.to_owned() + &" - ".to_string() + tit;
    let mus_album = alb.to_owned() + &" (".to_string() + &dat + &")".to_string();
    let msg = msg.channel_id.send_message(&ctx.http, |m| {
        m.embed(|e| {
            e.title(&mus_title);
            e.description(&mus_album);
            e.image("attachment://cover.png");
            e
        });
        m.add_file(AttachmentType::Path(Path::new("/tmp/cover.png")));
        m
    });

    if let Err(why) = msg {
        println!("Error sending message: {:?}", why);
    }

    Ok(())
}

A  => src/commands/ww.rs +78 -0
@@ 1,78 @@
use serenity::{
    framework::standard::{macros::command, Args, CommandResult},
    model::channel::Message,
    prelude::*,
};

#[command]
#[description = "Bot will reply with pretty embed explaining why the topic is bad."]
#[usage = "topic"]
#[example = "apple"]
#[example = "steam"]
#[example = "systemd"]
fn ww(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult {
    let arg = args.rest();

	if arg == "apple" {
		let msg = msg.channel_id.send_message(&ctx.http, |m| {
            m.embed(|e| {
                e.title("Why Phate6660 hates Apple:");
                e.fields(vec![
                    ("They are Evil", "They are a despicable, evil, and disgusting company. I find them to be even worse than Google, and probably even Amazon. They've done some truly terrible things. Some examples: mass censorship, worker abuse (manipulation/brainwashing, sweatshops), repairing your own device is not allowed, they fully support DRM, they exploit developers.", false),
                    ("They Pretend", "They like to pretend that they are the good guys of tech. While companies like Google are extremely terrible for your privacy, at least they aren't pretending like they aren't. Apple likes to give people the illusion that you can pay for your privacy, which to put frankly, is not true at all. They still spy on you just as much, or even more than, Google does.", false),
                    ("They are Restrictive and Controlling", "They limit and control what you are allowed to do with your own device. Want to repair your Mac? Nope, can't do that. Want to install a different OS? Nope, they'll do as much as they can to stop you from doing that. The reason why I prefer Google more (not that I like them, this is more about choosing the lesser evil), is because you are allowed to do something about it. Don't want Android spying on you? Most of the time (depending on the phone brand) you can easily unlock your phone, install a custom recovery, and install a custom ROM like LineageOS without installing GApps. With iPhones, try as you may, you will never have that same amount of control that you can have on an Android device.", false),
                ]);
                e
            });
            m
        });

        if let Err(why) = msg {
            println!("Error sending message: {:?}", why);
        }
	}

    if arg == "steam" {
        let msg = msg.channel_id.send_message(&ctx.http, |m| {
            m.embed(|e| {
                e.title("Why Phate6660 hates Steam:");
                e.fields(vec![
                    ("DRM", "You don't own the games you buy, you own the right to play the game off of Steam.", false),
                    ("Mistreatment of Developers", "Steam mistreats game devs and publishers (this is why you see more and more games using GOG or even their own launchers/stores).", false),
                    ("It is Forced Onto You", "Steam is *forced* onto you by various games. Imagine my surprise when I buy the Elder Scrolls anthology (as a physical collector's set complete with DISC COPIES of the game), and every game works... except for Skyrim. Skyrim requires you to use Steam. It's a shame that I saved up 50 whole dollars for shit. I can tell you Skyrim went straight into the trash, right after being broken into 600 little pieces.", false),
                    ("Privacy Violations", "I shouldn't have to explain this one right?", false),
                    ("Centralization", "Having all of your games centralized into one place is stupid, and this ties into the DRM point. If Steam were to shut down right now, I guarantee that you would lose access to at least 80% of your games.", false),
                ]);
                e
            });
            m
        });

        if let Err(why) = msg {
            println!("Error sending message: {:?}", why);
        }
    }

    if arg == "systemd" {
        let msg = msg.channel_id.send_message(&ctx.http, |m| {
            m.embed(|e| {
                e.title("Why Phate6660 hates SystemD:");
                e.fields(vec![
                    ("Feature Creep", "The undeniable feature creep. While some people actually enjoy the features brought in from it (boot manager, network manager, login manager?, etc), I find them to be nothing but bloat. An init system should be just that. An init system. Not <insert an exaggerated amount of functions here>.", false),
                    ("Slow", "It is slow. Slow to shutdown, slow to boot up, etc. Here are actual timed reboots from my machine using 3 init systems. SystemD (17s), OpenRC (11s), Runit (7s). 17s vs 7s, which would you choose?", false),
                    ("Bugs and Insecurities", "Due to the feature creep, there is a larger attack service for bugs and security vulnerabilities. And there are security issues with SystemD.", false),
                    ("Devs don't Care", "This is the one that bothers me the most. It's almost as if the dev(s) are completely oblivious or at least ignorant to the feature creep and security issues. Hell, Poettering even got awarded by Red Hat for making lame excuses for not fixing important bugs.", false),
                    ("Hard Requirement", "It is a hard dependency for a large and *still growing* list of programs. Which forces users to use tools such as consolekit or elogind or eudev, or even patch the program themselves not to use systemd. This is a trivial thing when using distros like Gentoo, but I feel sincerely sorry for those using distros like Debian where it's near impossible to escape.", false),
                ]);
                e
            });
            m
        });

        if let Err(why) = msg {
            println!("Error sending message: {:?}", why);
        }
    }

    Ok(())
}

A  => src/functions/mod.rs +2 -0
@@ 1,2 @@
pub mod prefix_space_check;
pub mod yt_tw_check;

A  => src/functions/prefix_space_check.rs +21 -0
@@ 1,21 @@
use log::{error, info};
use serenity::{model::channel::Message, prelude::*};

// Users commonly message things like "^^^ what he said", this will check if there's a "^ " in the message and will ignore it if it's found.
pub fn prefix_space_check(ctx: &mut Context, msg: &Message, unknown_command_name: &str) {
    if msg.content.contains("^ ") {
        info!("There was a space after the prefix, assuming the bot was not intended to be used.");
    } else {
        error!("Invalid command: '{}'", unknown_command_name);
        let msg = msg.channel_id.send_message(&ctx.http, |m| {
            m.embed(|e| {
                e.description("Invalid command, please use `^help` to check for valid commands.");
                e
            })
        });

        if let Err(why) = msg {
            error!("Error sending message: {:?}", why);
        }
    }
}

A  => src/functions/yt_tw_check.rs +57 -0
@@ 1,57 @@
use log::{error, info};
use serenity::{model::channel::Message, prelude::*};

// If "youtube.com" is found, react with disappointed and message a pretty embed about invidio.
// Do the same for "twitter.com", except the embed pertains to Nitter.
pub fn yt_tw_check(ctx: &mut Context, msg: &Message) {
    let sent_message = &msg.content;
    if sent_message.contains("youtube.com") {
        info!("YouTube link was found! It was sent by: {}", msg.author.name);
        msg.react(&ctx.http, '😞');
        let message = msg.channel_id.send_message(&ctx.http, |m| {
            m.embed(|e| {
                e.title("`rsPhate`");
                e.description("YouTube link discovered! Please link to invidio instead. Why:");
                // false = not inline
                e.fields(vec![
                    ("FOSS", "Invidio is a [FOSS](https://github.com/omarroth/invidious) frontend to YouTube.", false),
                    ("Proxy", "Invidio has the ability to proxy YouTube videos through their servers, allowing you to watch videos without ever touching Google's servers. Add `&local=1` to an invidio URL to proxy.", false),
                    ("Lightweight", "The entire site is very small, and works without JavaScript or XHR.", false),
                    ("Self-Hostable", "You can self-host your own instance if you really wanted to. And if you don't trust the [main instance](https://invidio.us) there are a [variety of other instances](https://github.com/omarroth/invidious/wiki/Invidious-Instances) to choose from.", false),
                    ("I'm interested, how do I convert URLs?", "Replace `youtube.com` with `invidio.us`. For `youtu.be` URLs, take the string after the last slash and append it to `https://invidio.us/watch?v=`.", false),
                ]);
                e
            });
            m
        });

        if let Err(why) = message {
            error!("Error sending message: {:?}", why);
        }
    }
    if sent_message.contains("twitter.com") {
        info!("Twitter link was found! It was sent by: {}", msg.author.name);
        msg.react(&ctx.http, '😞');
        let message = msg.channel_id.send_message(&ctx.http, |m| {
            m.embed(|e| {
                e.title("`rsPhate`");
                e.description("Twitter link discovered! Please link to Nitter instead. Why:");
                // false = not inline
                e.fields(vec![
                    ("FOSS", "Nitter is a [FOSS](https://github.com/zedeus/nitter) frontend to Twitter inspired by invidio.", false),
                    ("Proxy", "Everything is proxied through Nitter, Twitter won't even know your IP, much less be able to fingerprint or track you.", false),
                    ("API", "Contains an unoffical twitter api without any keys required or any rate limits enforced.", false),
                    ("Lightweight", "Pages are very small, and work without JS and XHR.", false),
                    ("Self-Hostable", "As with invidio, it is self-hostable and there are plenty of other [instances](https://github.com/zedeus/nitter/wiki/Instances) to choose from.", false),
                    ("I'm interested, how do I convert URLs?", "Replace `twitter.com` with `nitter.net`.", false),
                ]);
                e
            });
            m
        });

        if let Err(why) = message {
            error!("Error sending message: {:?}", why);
        }
    }
}

A  => src/main.rs +228 -0
@@ 1,228 @@
use log::{error, info};
use serenity::{
    client::bridge::gateway::ShardManager,
    framework::standard::{
        help_commands,
        macros::{check, group, help},
        Args, CheckResult, CommandGroup, CommandOptions, CommandResult,
        DispatchError::CheckFailed,
        HelpOptions, StandardFramework,
    },
    model::{gateway::Ready, id::UserId, prelude::Message},
    prelude::*,
};
use std::{
    collections::{HashMap, HashSet},
    env,
    sync::Arc,
};

// Load and use commands from src/commands/
mod commands;
use commands::{
    about::*, date::*, embed::*, fortune::*, git::*, hmm::*, iv::*, math::*, meme::*, owo::*,
    projects::*, quit::*, rng::*, rr::*, star::*, wipltrn::*, ww::*,
};

// Load and use extra functions from src/functions/
mod functions;
use functions::{prefix_space_check::prefix_space_check, yt_tw_check::yt_tw_check};

// A container type is created for inserting into the Client's `data`, which
// allows for data to be accessible across all events and framework commands, or
// anywhere else that has a copy of the `data` Arc.
struct ShardManagerContainer;

impl TypeMapKey for ShardManagerContainer {
    type Value = Arc<Mutex<ShardManager>>;
}

struct CommandCounter;

impl TypeMapKey for CommandCounter {
    type Value = HashMap<String, u64>;
}

struct Handler;

impl EventHandler for Handler {
    // Set a handler for the `message` event - so that whenever a new message
    // is received - the closure (or function) passed will be called.
    //
    // Event handlers are dispatched through a threadpool, and so multiple
    // events can be dispatched simultaneously.
    //
    // Set a handler to be called on the `ready` event. This is called when a
    // shard is booted, and a READY payload is sent by Discord. This payload
    // contains data like the current user's guild Ids, current user data,
    // private channels, and more.
    //
    // In this case, just print what the current user's username is.
    fn ready(&self, ctx: Context, ready: Ready) {
        use serenity::model::gateway::Activity;
        use serenity::model::user::OnlineStatus;

        let activity = Activity::playing("^help for help");
        let status = OnlineStatus::Online;

        ctx.set_presence(Some(activity), status);
        info!("Connected as {}", ready.user.name);
    }
}

// Groups
#[group]
#[description = "Functions for the bot that do not belong in any specific category."]
#[commands(date, hmm, iv, fortune, meme, rr, wipltrn, ww)]
struct Functions;

#[group]
#[description = "Generalized functions for the bot."]
#[commands(about, projects, quit)]
struct General;

#[group]
#[description = "Functions that are related to number operations."]
#[commands(math, rng)]
struct Numbers;

#[group]
#[description = "Functions that are related to message operations."]
#[commands(embed, git, owo, star)]
struct Messages;

#[help]
#[individual_command_tip = "`^help` | `^help command` | `^help group`\nNOTE: Args are delimited via `,`."]
fn my_help(
    context: &mut Context,
    msg: &Message,
    args: Args,
    help_options: &'static HelpOptions,
    groups: &[&'static CommandGroup],
    owners: HashSet<UserId>,
) -> CommandResult {
    help_commands::with_embeds(context, msg, args, help_options, groups, owners)
}

fn main() {
    // This will load the environment variables located at `./.env`, relative to the CWD.
    kankyo::load().expect("Failed to load .env file");

    // Initialize the logger to use environment variables.
    env_logger::init();

    // Configure the client with your Discord bot token in the environment.
    let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");

    // Create a new instance of the Client, logging in as a bot. This will
    // automatically prepend your bot token with "Bot ", which is a requirement
    // by Discord for bot users.
    let mut client = Client::new(&token, Handler).expect("Err creating client");

    {
        let mut data = client.data.write();
        data.insert::<CommandCounter>(HashMap::default());
        data.insert::<ShardManagerContainer>(Arc::clone(&client.shard_manager));
    }

    client.with_framework(
        // Configures the client, allowing for options to mutate how the
        // framework functions.
        //
        // Refer to the documentation for
        // `serenity::ext::framework::Configuration` for all available
        // configurations.
        StandardFramework::new()
            .configure(|c| {
                c.with_whitespace(true)
                    .prefix("^")
                    // Delimiters are: " ", ", ", and ",".
                    .delimiters(vec![","])
            })
            // Set a function to be called prior to each command execution. This
            // provides the context of the command, the message that was received,
            // and the full name of the command that will be called.
            //
            // You can not use this to determine whether a command should be
            // executed. Instead, the `#[check]` macro gives you this functionality.
            .before(|ctx, msg, command_name| {
                info!(
                    "command: '{}', user: '{}'",
                    command_name, msg.author.name
                );

                // Increment the number of times this command has been run once. If
                // the command's name does not exist in the counter, add a default
                // value of 0.
                let mut data = ctx.data.write();
                let counter = data
                    .get_mut::<CommandCounter>()
                    .expect("Expected CommandCounter in ShareMap.");
                let entry = counter.entry(command_name.to_string()).or_insert(0);
                *entry += 1;

                true // if `before` returns false, command processing doesn't happen.
            })
            // Similar to `before`, except will be called directly _after_
            // command execution.
            .after(|_, _, command_name, error| match error {
                Ok(()) => info!("Processed command '{}'", command_name),
                Err(why) => info!("Command '{}' returned error {:?}", command_name, why),
            })
            // Set a function that's called whenever an attempted command-call's
            // command could not be found.
            .unrecognised_command(|ctx, msg, unknown_command_name| {
                prefix_space_check(ctx, msg, unknown_command_name);
            })
            .normal_message(|ctx, msg| {
                yt_tw_check(ctx, msg);
            })
            // Set a function that's called whenever a command's execution didn't complete for one
            // reason or another. For example, when a user has exceeded a rate-limit or a command
            // can only be performed by the bot owner.
            .on_dispatch_error(|ctx, msg, error| match error {
                CheckFailed(Owner, owner_check) => {
                    msg.reply(
                        &ctx.http,
                        "Owner check failed! I will ping you a hundredfold if you do that again! <:sadgry:676458405342216195>",
                    );
                }
                _ => {
                    error!("Unhandled dispatch error!");
                }
            })
            // Set the help function
            .help(&MY_HELP)
            // The `#[group]` macro generates `static` instances of the options set for the group.
            // They're made in the pattern: `#name_GROUP` for the group instance and `#name_GROUP_OPTIONS`.
            // #name is turned all uppercase
            .group(&FUNCTIONS_GROUP)
            .group(&GENERAL_GROUP)
            .group(&NUMBERS_GROUP)
            .group(&MESSAGES_GROUP),
    );

    if let Err(why) = client.start() {
        error!("Client error: {:?}", why);
    }
}

#[check]
#[name = "Owner"]
fn owner_check(_: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> CheckResult {
    // Replace 7 with your ID to make this check pass.
    //
    // `true` will convert into `CheckResult::Success`,
    //
    // `false` will convert into `CheckResult::Failure(Reason::Unknown)`,
    //
    // and if you want to pass a reason alongside failure you can do:
    // `CheckResult::new_user("Lacked admin permission.")`,
    //
    // if you want to mark it as something you want to log only:
    // `CheckResult::new_log("User lacked admin permission.")`,
    //
    // and if the check's failure origin is unknown you can mark it as such (same as using `false.into`):
    // `CheckResult::new_unknown()`
    (msg.author.id == 534379378432540675).into()
}