public async Task AddZone(string name, string type = "", string description = "")
        {
            if (string.IsNullOrEmpty(name))
            {
                await BotUtils.ReplyAsync_Error(Context, "Zone name cannot be empty.");
            }
            else
            {
                // Allow the user to specify zones with numbers (e.g., "1") or single letters (e.g., "A").
                // Otherwise, the name is taken as-is.
                name = ZoneUtils.FormatZoneName(name).ToLower();

                // If an invalid type was provided, assume the user meant it as a description instead.
                // i.e., "addzone <name> <description>"

                ZoneType zone_type = await ZoneUtils.GetZoneTypeAsync(type);

                if (zone_type is null || zone_type.Id == ZoneType.NullZoneTypeId)
                {
                    description = type;

                    // Attempt to determine the zone type automatically if one wasn't provided.
                    // Currently, this is only possible if users are using the default zone types (i.e. "aquatic" and "terrestrial").

                    zone_type = await ZoneUtils.GetDefaultZoneTypeAsync(name);
                }

                if (await ZoneUtils.GetZoneAsync(name) != null)
                {
                    // Don't attempt to create the zone if it already exists.

                    await BotUtils.ReplyAsync_Warning(Context, string.Format("A zone named \"{0}\" already exists.", ZoneUtils.FormatZoneName(name)));
                }
                else
                {
                    await ZoneUtils.AddZoneAsync(new Zone {
                        Name        = name,
                        Description = description,
                        ZoneTypeId  = zone_type.Id
                    });

                    await BotUtils.ReplyAsync_Success(Context, string.Format("Successfully created new {0} zone, **{1}**.",
                                                                             zone_type.Name.ToLower(),
                                                                             ZoneUtils.FormatZoneName(name)));
                }
            }
        }
        // Checks for roles that are unfulfilled for a given zone
        private async Task <string[]> _getMissingRolesInZoneIdeasAsync()
        {
            List <string> ideas = new List <string>();

            string query = @"SELECT Zones.id AS zone_id1, Zones.name AS zone_name, Roles.id AS role_id1, Roles.name AS role_name FROM Zones, Roles WHERE
	            NOT EXISTS(SELECT * FROM SpeciesRoles WHERE role_id = role_id1 AND species_id IN (SELECT species_id FROM SpeciesZones WHERE zone_id = zone_id1));"    ;

            using (SQLiteCommand cmd = new SQLiteCommand(query))
                using (DataTable table = await Database.GetRowsAsync(cmd))
                    foreach (DataRow row in table.Rows)
                    {
                        string zone_name = row.Field <string>("zone_name");
                        string role_name = row.Field <string>("role_name");

                        ideas.Add(string.Format("**{0}** does not have any **{1}s**. Why not fill this role?",
                                                ZoneUtils.FormatZoneName(zone_name),
                                                StringUtils.ToTitleCase(role_name)));
                    }

            return(ideas.ToArray());
        }
        // Private members

        public async Task _plusZone(Species species, string zoneList, string notes, bool onlyShowErrors = false)
        {
            // Get the zones from user input.
            ZoneListResult zones = await ZoneUtils.GetZonesByZoneListAsync(zoneList);

            // Add the zones to the species.
            await SpeciesUtils.AddZonesAsync(species, zones.Zones, notes);

            if (zones.InvalidZones.Count() > 0)
            {
                // Show a warning if the user provided any invalid zones.

                await BotUtils.ReplyAsync_Warning(Context, string.Format("{0} {1} not exist.",
                                                                         StringUtils.ConjunctiveJoin(", ", zones.InvalidZones.Select(x => string.Format("**{0}**", ZoneUtils.FormatZoneName(x))).ToArray()),
                                                                         zones.InvalidZones.Count() == 1 ? "does" : "do"));
            }

            if (zones.Zones.Count() > 0 && !onlyShowErrors)
            {
                // Show a confirmation of all valid zones.

                await BotUtils.ReplyAsync_Success(Context, string.Format("**{0}** now inhabits {1}.",
                                                                         species.ShortName,
                                                                         StringUtils.ConjunctiveJoin(", ", zones.Zones.Select(x => string.Format("**{0}**", x.GetFullName())).ToArray())));
            }
        }
        public async Task MinusZone(string genus, string species, string zoneList)
        {
            // Ensure that the user has necessary privileges to use this command.
            if (!await BotUtils.ReplyHasPrivilegeAsync(Context, PrivilegeLevel.ServerModerator))
            {
                return;
            }

            // Get the specified species.

            Species sp = await BotUtils.ReplyFindSpeciesAsync(Context, genus, species);

            if (sp is null)
            {
                return;
            }

            // Get the zones that the species currently resides in.
            // These will be used to show warning messages (e.g., doesn't exist in the given zone).

            long[] current_zone_ids = (await BotUtils.GetZonesFromDb(sp.Id)).Select(x => x.Id).ToArray();

            // Get the zones from user input.
            ZoneListResult zones = await ZoneUtils.GetZonesByZoneListAsync(zoneList);

            // Remove the zones from the species.
            await SpeciesUtils.RemoveZonesAsync(sp, zones.Zones);

            if (zones.InvalidZones.Count() > 0)
            {
                // Show a warning if the user provided any invalid zones.

                await BotUtils.ReplyAsync_Warning(Context, string.Format("{0} {1} not exist.",
                                                                         StringUtils.ConjunctiveJoin(", ", zones.InvalidZones.Select(x => string.Format("**{0}**", ZoneUtils.FormatZoneName(x))).ToArray()),
                                                                         zones.InvalidZones.Count() == 1 ? "does" : "do"));
            }

            if (zones.Zones.Any(x => !current_zone_ids.Contains(x.Id)))
            {
                // Show a warning if the species wasn't in one or more of the zones provided.

                await BotUtils.ReplyAsync_Warning(Context, string.Format("**{0}** is already absent from {1}.",
                                                                         sp.ShortName,
                                                                         StringUtils.ConjunctiveJoin(", ", zones.Zones.Where(x => !current_zone_ids.Contains(x.Id)).Select(x => string.Format("**{0}**", x.GetFullName())).ToArray())));
            }

            if (zones.Zones.Any(x => current_zone_ids.Contains(x.Id)))
            {
                // Show a confirmation of all valid zones.

                await BotUtils.ReplyAsync_Success(Context, string.Format("**{0}** no longer inhabits {1}.",
                                                                         sp.ShortName,
                                                                         StringUtils.DisjunctiveJoin(", ", zones.Zones.Where(x => current_zone_ids.Contains(x.Id)).Select(x => string.Format("**{0}**", x.GetFullName())).ToArray())));
            }
        }