public async Task UnscheduleGuildEvent(CommandContext context)
        {
            using IBotAccessProvider provider = this.accessBuilder.Build();
            InteractivityExtension interactivity = context.Client.GetInteractivity();

            DiscordMessage msg = await context.RespondAsync(
                $":wave: Hi, {context.User.Mention}! You want to unschedule an event for your guild?");

            Reaction reaction = await interactivity.AddAndWaitForYesNoReaction(msg, context.User);

            if (reaction != Reaction.Yes)
            {
                return;
            }

            await context.Message.DeleteAsync();

            DateTimeZone memberTimeZone = this.timeZoneProvider[provider.GetUsersTimeZone(context.User.Id).TimeZoneId];

            List <GuildBackgroundJob> guildEventJobs = provider.GetAllAssociatedGuildBackgroundJobs(context.Guild.Id)
                                                       .Where(x => x.GuildJobType == GuildJobType.SCHEDULED_EVENT)
                                                       .OrderBy(x => x.ScheduledTime)
                                                       .ToList();

            DiscordEmbedBuilder removeEventEmbed = new DiscordEmbedBuilder()
                                                   .WithTitle("Select an event to unschedule by typing: <event number>")
                                                   .WithColor(context.Member.Color);

            CustomResult <int> result = await context.WaitForMessageAndPaginateOnMsg(
                GetScheduledEventsPages(guildEventJobs.Select(x => x.WithTimeZoneConvertedTo(memberTimeZone)), interactivity, removeEventEmbed),
                PaginationMessageFunction.CreateWaitForMessageWithIntInRange(context.User, context.Channel, 1, guildEventJobs.Count + 1),
                msg : msg);

            if (result.TimedOut || result.Cancelled)
            {
                DiscordMessage snark = await context.RespondAsync("You never gave me a valid input. Thanks for wasting my time. :triumph:");

                await Task.Delay(5000);

                await context.Channel.DeleteMessagesAsync(new List <DiscordMessage> {
                    msg, snark
                });

                return;
            }

            GuildBackgroundJob job = guildEventJobs[result.Result - 1];

            msg = await msg.ModifyAsync($"{context.User.Mention}, are you sure you want to unschedule this event?", embed : null);

            reaction = await interactivity.AddAndWaitForYesNoReaction(msg, context.User);

            BackgroundJob.Delete(job.HangfireJobId);
            provider.DeleteGuildBackgroundJob(job);
            await msg.DeleteAllReactionsAsync();

            await msg.ModifyAsync("Ok, I've unscheduled that event!", embed : null);
        }
        public async Task UnsuggestAsync(CommandContext context)
        {
            DbResult <IEnumerable <GuildMovieSuggestion> > suggestionsResult = await GetSuggestionsResult(context);

            if (!suggestionsResult.TryGetValue(out IEnumerable <GuildMovieSuggestion>?result))
            {
                await context.RespondAsync("Something went wrong while attempting to get the suggestions. Please contact the developer.");

                return;
            }

            if (!result.Any())
            {
                await context.RespondAsync("You do not have any suggestions to delete.");

                return;
            }

            List <GuildMovieSuggestion> suggestions   = result.ToList();
            InteractivityExtension      interactivity = context.Client.GetInteractivity();
            IEnumerable <Page>          pages         = GetGuildMovieSuggestionsPages(suggestions.ToList(), interactivity);

            CustomResult <int> waitResult = await context.WaitForMessageAndPaginateOnMsg(pages,
                                                                                         PaginationMessageFunction.CreateWaitForMessageWithIntInRange(context.User, context.Channel, 1, suggestions.Count + 1)
                                                                                         );

            if (waitResult.Cancelled)
            {
                await context.RespondAsync("Ok, I won't delete any suggestion. Please try again if so desired.");

                return;
            }

            if (waitResult.TimedOut)
            {
                await context.RespondAsync("You never gave me a valid input. Please try again if so desired.");

                return;
            }

            Reaction reaction = await interactivity.AddAndWaitForYesNoReaction(
                await context.Channel.SendMessageAsync($"You want me to do delete the suggestion `{suggestions[waitResult.Result - 1].Title}`?"),
                context.Member
                );

            if (reaction != Reaction.Yes)
            {
                await context.Channel.SendMessageAsync("Ok!");

                return;
            }

            GuildMovieSuggestion chosen = suggestions[waitResult.Result - 1];

            await this.mediator.Send(new GuildMovieSuggestions.Delete(chosen));

            await context.Channel.SendMessageAsync($"{context.Member.Mention}, I have deleted the suggestion `{suggestions[waitResult.Result - 1].Title}`");
        }
        public async Task ScheduleGuildEvent(
            CommandContext context,
            [Description("The channel to announce the event in")]
            DiscordChannel announcementChannel,
            [Description("The role to announce the event to")]
            DiscordRole role,
            [Description("The date to schedule the event for")]
            [RemainingText]
            string datetimeString
            )
        {
            using IBotAccessProvider provider = this.accessBuilder.Build();

            if (!context.User.TryGetDateTimeZone(provider, this.timeZoneProvider, out DateTimeZone schedulerTimeZone))
            {
                await context.RespondAsync(StringConstants.NoTimeZoneErrorMessage);

                return;
            }

            DiscordMember botMember = await context.Guild.GetMemberAsync(context.Client.CurrentUser.Id);

            if (!announcementChannel.PermissionsFor(botMember).HasPermission(Permissions.SendMessages | Permissions.MentionEveryone))
            {
                await context.RespondAsync($"{context.Member.Mention}, I don't have permission to send messages and mention `@everyone` in that channel.");

                return;
            }

            LocalDateTime datetime = Recognizers.RecognizeDateTime(datetimeString, DateTimeV2Type.DateTime)
                                     .First().Values.Select(value => (LocalDateTime)value.Value).OrderBy(key => key).First();
            DiscordMessage msg = await context.RespondAsync($":wave: Hi, {context.User.Mention}! You want to schedule an event for {datetime:g} in your timezone?");

            InteractivityExtension interactivity = context.Client.GetInteractivity();
            Reaction reaction = await interactivity.AddAndWaitForYesNoReaction(msg, context.User);

            if (reaction != Reaction.Yes)
            {
                return;
            }

            DiscordEmbedBuilder scheduleEmbedBase = new DiscordEmbedBuilder()
                                                    .WithTitle("Select an event by typing: <event number>")
                                                    .WithColor(context.Member.Color);

            GuildEvent selectedEvent = await SelectPredefinedEvent(context, provider, msg, interactivity, scheduleEmbedBase);

            Instant      eventDateTime = datetime.InZoneStrictly(schedulerTimeZone).ToInstant();
            DiscordEmbed embed         = new DiscordEmbedBuilder()
                                         .WithAuthor(context.Member.DisplayName, iconUrl: context.Member.AvatarUrl)
                                         .WithDescription(selectedEvent.EventDesc)
                                         .WithTitle(selectedEvent.EventName)
                                         .Build();
            await msg.ModifyAsync($"You have scheduled the following event for {datetime:g} in your time zone to be output in the {announcementChannel.Mention} channel.", embed : embed);

            this.ScheduleEventsForRole(context, announcementChannel, provider, selectedEvent, eventDateTime, role);
        }
        public async Task DeleteAsync(CommandContext context)
        {
            DbResult <IEnumerable <GuildMovieNight> > getMovieNightsResult = await this.mediator.Send(new GuildMovieNights.GetAllGuildsMovieNights(context.Guild.Id));

            if (!getMovieNightsResult.TryGetValue(out IEnumerable <GuildMovieNight>?guildMovieNights))
            {
                throw new Exception("An error occured while retrieving guild movie nights");
            }
            bool hasManageServer = context.Member.Roles.Select(x => x.CheckPermission(Permissions.ManageGuild)).Any();

            if (!hasManageServer)
            {
                guildMovieNights = guildMovieNights.Where(mn => mn.HostId == context.Member.Id);
            }
            List <GuildMovieNight> movieNights   = guildMovieNights.ToList();
            InteractivityExtension interactivity = context.Client.GetInteractivity();
            IEnumerable <Page>     pages         = await GetGuildMovieNightsPages(context.Guild, movieNights, interactivity, hasManageServer);

            CustomResult <int> result = await context.WaitForMessageAndPaginateOnMsg(pages,
                                                                                     PaginationMessageFunction.CreateWaitForMessageWithIntInRange(context.User, context.Channel, 1, movieNights.Count + 1)
                                                                                     );

            if (result.TimedOut || result.Cancelled)
            {
                await context.RespondAsync("You never gave me a valid input. Please try again if so desired.");

                return;
            }

            Reaction reaction = await interactivity.AddAndWaitForYesNoReaction(await context.Channel.SendMessageAsync($"You want me to do delete movie night {result.Result}?"), context.Member);

            if (reaction != Reaction.Yes)
            {
                await context.Channel.SendMessageAsync("Ok!");

                return;
            }

            GuildMovieNight chosen = movieNights[result.Result - 1];

            RecurringJob.RemoveIfExists(chosen.MovieNightStartHangfireId);
            RecurringJob.RemoveIfExists(chosen.VotingStartHangfireId);
            RecurringJob.RemoveIfExists(chosen.VotingEndHangfireId);
            await this.mediator.Send(new GuildMovieNights.Delete(chosen));

            await context.Channel.SendMessageAsync($"{context.Member.Mention}, I have deleted movie night {result.Result}");
        }
        public async Task CreateAsync(CommandContext context, [Description("The channel in which to announce the movie night")] DiscordChannel announcementChannel)
        {
            TimeZoneInfo hostTimeZoneInfo = await GetUserTimeZoneInfoAsync(context);

            DiscordMessage confirmationMessage = await context.RespondAsync("Hi, you'd like to schedule a recurring movie night?");

            InteractivityExtension interactivity = context.Client.GetInteractivity();
            Reaction reaction = await interactivity.AddAndWaitForYesNoReaction(confirmationMessage, context.Member);

            if (reaction != Reaction.Yes)
            {
                await context.Channel.SendMessageAsync("Ok. I will end the process.");

                return;
            }

            DayOfWeek movieStartDayOfWeek = await GetMovieStartDayOfWeek(context, interactivity);

            TimeSpan movieStartTimeOfDay = await GetMovieStartTimeOfDay(context, interactivity);

            await context.Channel.SendMessageAsync("How many days and hours before the movie starts do you want voting to end? Format: 0d0h");

            (int voteEndDays, int voteEndHours) = await GetDaysAndHours(context, interactivity);

            (DayOfWeek voteEndDayOfWeek, TimeSpan voteEndTimeSpan) = GenerateCronEspressoVariables(movieStartDayOfWeek, movieStartTimeOfDay, voteEndDays, voteEndHours);

            await context.Channel.SendMessageAsync("How many days and hours before the movie starts do you want voting to start? Format: 0d0h");

            (int voteStartDays, int voteStartHours) = await GetDaysAndHours(context, interactivity);

            (DayOfWeek voteStartDayOfWeek, TimeSpan voteStartTimeSpan) = GenerateCronEspressoVariables(movieStartDayOfWeek, movieStartTimeOfDay, voteStartDays, voteStartHours);

            OmdbParentalRating maxParentalRating = await GetMaxParentalRating(context, interactivity);

            int maximumNumberOfSuggestions = await GetMaximumNumberOfSuggestions(context, interactivity);

            string voteStartCron  = GenerateCronExpression(voteStartTimeSpan, voteStartDayOfWeek);
            string voteEndCron    = GenerateCronExpression(voteEndTimeSpan, voteEndDayOfWeek);
            string movieStartCron = GenerateCronExpression(movieStartTimeOfDay, movieStartDayOfWeek);

            string guid            = Guid.NewGuid().ToString();
            string voteStartJobId  = $"{context.Guild.Id}-{context.Member.Id}-{guid}-voting-start";
            string voteEndJobId    = $"{context.Guild.Id}-{context.Member.Id}-{guid}-voting-end";
            string startMovieJobId = $"{context.Guild.Id}-{context.Member.Id}-{guid}-start-movie";

            DbResult <GuildMovieNight> addMovieNightResult = await this.mediator.Send(new GuildMovieNights.Add(voteStartJobId, voteEndJobId, startMovieJobId, maximumNumberOfSuggestions, maxParentalRating, context.Guild, announcementChannel, context.Member));

            if (!addMovieNightResult.TryGetValue(out GuildMovieNight? movieNight))
            {
                await context.Channel.SendMessageAsync("I failed in adding the movie night to the database. Logs have been sent to the developer.");

                throw new Exception(
                          $@"voteStartJobId: {voteStartJobId}
voteEndJobId: {voteEndJobId}
startMovieJobId: {startMovieJobId}
maximumNumberOfSuggestions: {maximumNumberOfSuggestions}
maxParentalRating: {maxParentalRating}
guild: {context.Guild.Id}
announcementChannel: {announcementChannel.Id}
host: {context.Member.Id}");
            }

            RecurringJob.AddOrUpdate <MovieNightService>(voteStartJobId, mns => mns.StartVoting(movieNight.Id), voteStartCron, new RecurringJobOptions()
            {
                TimeZone = hostTimeZoneInfo
            });
            RecurringJob.AddOrUpdate <MovieNightService>(voteEndJobId, mns => mns.CalculateVotes(movieNight.Id), voteEndCron, new RecurringJobOptions()
            {
                TimeZone = hostTimeZoneInfo
            });
            RecurringJob.AddOrUpdate <MovieNightService>(startMovieJobId, mns => mns.StartMovie(movieNight.Id), movieStartCron, new RecurringJobOptions()
            {
                TimeZone = hostTimeZoneInfo
            });
            Dictionary <string, RecurringJobDto>?rJobDtos = JobStorage.Current
                                                            .GetConnection()
                                                            .GetRecurringJobs(new List <string>()
            {
                voteStartJobId, voteEndJobId, startMovieJobId
            })
                                                            .ToDictionary(x => x.Id);

            await context.Channel.SendMessageAsync("Your movie night has been scheduled.");

            if (rJobDtos[voteStartJobId].NextExecution !.Value > rJobDtos[startMovieJobId].NextExecution !.Value)
            {
                await context.Channel.SendMessageAsync($"{context.Member.Mention}, the next scheduled voting will happen after the movie night is supposed to happen. To handle this, we are going to open voting now and close voting at the normal scheduled time. If the normal scheduled time to close voting can't be used, we will cancel the next movie night altogether and will not open voting.");

                if (rJobDtos[voteEndJobId].NextExecution !.Value > rJobDtos[startMovieJobId].NextExecution !.Value)
                {
                    JobStorage.Current.GetConnection().SetRangeInHash(
                        $"recurring-job:{startMovieJobId}",
                        new[] { new KeyValuePair <string, string>("skip", "true") }
                        );
                }

                BackgroundJob.Enqueue <MovieNightService>(mns => mns.StartVoting(movieNight.Id));
            }
        }