Polls persist restarts now.
This commit is contained in:
		@@ -10,6 +10,10 @@ namespace NadekoBot.Common.Collections
 | 
			
		||||
        public List<T> Source { get; }
 | 
			
		||||
        private readonly object _locker = new object();
 | 
			
		||||
 | 
			
		||||
        public IndexedCollection()
 | 
			
		||||
        {
 | 
			
		||||
            Source = new List<T>();
 | 
			
		||||
        }
 | 
			
		||||
        public IndexedCollection(IEnumerable<T> source)
 | 
			
		||||
        {
 | 
			
		||||
            lock (_locker)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1992
									
								
								NadekoBot.Core/Migrations/20171027155001_poll-rewrite.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1992
									
								
								NadekoBot.Core/Migrations/20171027155001_poll-rewrite.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										100
									
								
								NadekoBot.Core/Migrations/20171027155001_poll-rewrite.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								NadekoBot.Core/Migrations/20171027155001_poll-rewrite.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Migrations
 | 
			
		||||
{
 | 
			
		||||
    public partial class pollrewrite : Migration
 | 
			
		||||
    {
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "Poll",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    Id = table.Column<int>(type: "INTEGER", nullable: false)
 | 
			
		||||
                        .Annotation("Sqlite:Autoincrement", true),
 | 
			
		||||
                    ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
 | 
			
		||||
                    GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    Question = table.Column<string>(type: "TEXT", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("PK_Poll", x => x.Id);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "PollAnswer",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    Id = table.Column<int>(type: "INTEGER", nullable: false)
 | 
			
		||||
                        .Annotation("Sqlite:Autoincrement", true),
 | 
			
		||||
                    DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
 | 
			
		||||
                    Index = table.Column<int>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    PollId = table.Column<int>(type: "INTEGER", nullable: true),
 | 
			
		||||
                    Text = table.Column<string>(type: "TEXT", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("PK_PollAnswer", x => x.Id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "FK_PollAnswer_Poll_PollId",
 | 
			
		||||
                        column: x => x.PollId,
 | 
			
		||||
                        principalTable: "Poll",
 | 
			
		||||
                        principalColumn: "Id",
 | 
			
		||||
                        onDelete: ReferentialAction.Restrict);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "PollVote",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    Id = table.Column<int>(type: "INTEGER", nullable: false)
 | 
			
		||||
                        .Annotation("Sqlite:Autoincrement", true),
 | 
			
		||||
                    DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
 | 
			
		||||
                    PollId = table.Column<int>(type: "INTEGER", nullable: true),
 | 
			
		||||
                    UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    VoteIndex = table.Column<int>(type: "INTEGER", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("PK_PollVote", x => x.Id);
 | 
			
		||||
                    table.ForeignKey(
 | 
			
		||||
                        name: "FK_PollVote_Poll_PollId",
 | 
			
		||||
                        column: x => x.PollId,
 | 
			
		||||
                        principalTable: "Poll",
 | 
			
		||||
                        principalColumn: "Id",
 | 
			
		||||
                        onDelete: ReferentialAction.Restrict);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "IX_Poll_GuildId",
 | 
			
		||||
                table: "Poll",
 | 
			
		||||
                column: "GuildId",
 | 
			
		||||
                unique: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "IX_PollAnswer_PollId",
 | 
			
		||||
                table: "PollAnswer",
 | 
			
		||||
                column: "PollId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "IX_PollVote_PollId",
 | 
			
		||||
                table: "PollVote",
 | 
			
		||||
                column: "PollId");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "PollAnswer");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "PollVote");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "Poll");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -983,6 +983,67 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.ToTable("PlaylistSong");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.Poll", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd();
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("ChannelId");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded");
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("GuildId");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Question");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("GuildId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Poll");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.PollAnswer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd();
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Index");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("PollId");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Text");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PollId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("PollAnswer");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.PollVote", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd();
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int?>("PollId");
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("VoteIndex");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PollId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("PollVote");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.Quote", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
@@ -1771,6 +1832,20 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                        .OnDelete(DeleteBehavior.Cascade);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.PollAnswer", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("NadekoBot.Core.Services.Database.Models.Poll")
 | 
			
		||||
                        .WithMany("Answers")
 | 
			
		||||
                        .HasForeignKey("PollId");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.PollVote", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("NadekoBot.Core.Services.Database.Models.Poll")
 | 
			
		||||
                        .WithMany("Votes")
 | 
			
		||||
                        .HasForeignKey("PollId");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.RaceAnimal", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.HasOne("NadekoBot.Core.Services.Database.Models.BotConfig")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,129 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services.Impl;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common
 | 
			
		||||
{
 | 
			
		||||
    public class Poll
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IUserMessage _originalMessage;
 | 
			
		||||
        private readonly IGuild _guild;
 | 
			
		||||
        private readonly string[] answers;
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, int> _participants = new ConcurrentDictionary<ulong, int>();
 | 
			
		||||
        private readonly string _question;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly NadekoStrings _strings;
 | 
			
		||||
        private bool running = false;
 | 
			
		||||
 | 
			
		||||
        public event Action<ulong> OnEnded = delegate { };
 | 
			
		||||
 | 
			
		||||
        public Poll(DiscordSocketClient client, NadekoStrings strings, IUserMessage umsg, string question, IEnumerable<string> enumerable)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
 | 
			
		||||
            _originalMessage = umsg;
 | 
			
		||||
            _guild = ((ITextChannel)umsg.Channel).Guild;
 | 
			
		||||
            _question = question;
 | 
			
		||||
            answers = enumerable as string[] ?? enumerable.ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public EmbedBuilder GetStats(string title)
 | 
			
		||||
        {
 | 
			
		||||
            var results = _participants.GroupBy(kvp => kvp.Value)
 | 
			
		||||
                                .ToDictionary(x => x.Key, x => x.Sum(kvp => 1))
 | 
			
		||||
                                .OrderByDescending(kvp => kvp.Value)
 | 
			
		||||
                                .ToArray();
 | 
			
		||||
 | 
			
		||||
            var eb = new EmbedBuilder().WithTitle(title);
 | 
			
		||||
 | 
			
		||||
            var sb = new StringBuilder()
 | 
			
		||||
                .AppendLine(Format.Bold(_question))
 | 
			
		||||
                .AppendLine();
 | 
			
		||||
 | 
			
		||||
            var totalVotesCast = 0;
 | 
			
		||||
            if (results.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                sb.AppendLine(GetText("no_votes_cast"));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                for (int i = 0; i < results.Length; i++)
 | 
			
		||||
                {
 | 
			
		||||
                    var result = results[i];
 | 
			
		||||
                    sb.AppendLine(GetText("poll_result",
 | 
			
		||||
                        result.Key,
 | 
			
		||||
                        Format.Bold(answers[result.Key - 1]),
 | 
			
		||||
                        Format.Bold(result.Value.ToString())));
 | 
			
		||||
                    totalVotesCast += result.Value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            eb.WithDescription(sb.ToString())
 | 
			
		||||
              .WithFooter(efb => efb.WithText(GetText("x_votes_cast", totalVotesCast)));
 | 
			
		||||
 | 
			
		||||
            return eb;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task StartPoll()
 | 
			
		||||
        {
 | 
			
		||||
            var msgToSend = GetText("poll_created", Format.Bold(_originalMessage.Author.Username)) + "\n\n" + Format.Bold(_question) + "\n";
 | 
			
		||||
            var num = 1;
 | 
			
		||||
            msgToSend = answers.Aggregate(msgToSend, (current, answ) => current + $"`{num++}.` **{answ}**\n");
 | 
			
		||||
            msgToSend += "\n" + Format.Bold(GetText("poll_vote_public"));
 | 
			
		||||
 | 
			
		||||
            await _originalMessage.Channel.SendConfirmAsync(msgToSend).ConfigureAwait(false);
 | 
			
		||||
            running = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task StopPoll()
 | 
			
		||||
        {
 | 
			
		||||
            running = false;
 | 
			
		||||
            OnEnded(_guild.Id);
 | 
			
		||||
            await _originalMessage.Channel.EmbedAsync(GetStats("POLL CLOSED")).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> TryVote(IUserMessage msg)
 | 
			
		||||
        {
 | 
			
		||||
            // has to be a user message
 | 
			
		||||
            if (msg == null || msg.Author.IsBot || !running)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            // has to be an integer
 | 
			
		||||
            if (!int.TryParse(msg.Content, out int vote))
 | 
			
		||||
                return false;
 | 
			
		||||
            if (vote < 1 || vote > answers.Length)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            IMessageChannel ch;
 | 
			
		||||
            //if public, channel must be the same the poll started in
 | 
			
		||||
            if (_originalMessage.Channel.Id != msg.Channel.Id)
 | 
			
		||||
                return false;
 | 
			
		||||
            ch = msg.Channel;
 | 
			
		||||
 | 
			
		||||
            //user can vote only once
 | 
			
		||||
            if (_participants.TryAdd(msg.Author.Id, vote))
 | 
			
		||||
            {
 | 
			
		||||
                var toDelete = await ch.SendConfirmAsync(GetText("poll_voted", Format.Bold(msg.Author.ToString()))).ConfigureAwait(false);
 | 
			
		||||
                toDelete.DeleteAfter(5);
 | 
			
		||||
                try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private string GetText(string key, params object[] replacements)
 | 
			
		||||
            => _strings.GetText(key,
 | 
			
		||||
                _guild.Id,
 | 
			
		||||
                "Games".ToLowerInvariant(),
 | 
			
		||||
                replacements);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								NadekoBot.Core/Modules/Games/Common/PollRunner.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								NadekoBot.Core/Modules/Games/Common/PollRunner.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common
 | 
			
		||||
{
 | 
			
		||||
    public class PollRunner
 | 
			
		||||
    {
 | 
			
		||||
        public Poll Poll { get; }
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public event Func<IUserMessage, IGuildUser, Task> OnVoted;
 | 
			
		||||
 | 
			
		||||
        private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
 | 
			
		||||
 | 
			
		||||
        public PollRunner(DbService db, Poll poll)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            Poll = poll;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> TryVote(IUserMessage msg)
 | 
			
		||||
        {
 | 
			
		||||
            PollVote voteObj;
 | 
			
		||||
            await _locker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                // has to be a user message
 | 
			
		||||
                // channel must be the same the poll started in
 | 
			
		||||
                if (msg == null || msg.Author.IsBot || msg.Channel.Id != Poll.ChannelId)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                // has to be an integer
 | 
			
		||||
                if (!int.TryParse(msg.Content, out int vote))
 | 
			
		||||
                    return false;
 | 
			
		||||
                --vote;
 | 
			
		||||
                if (vote < 0 || vote >= Poll.Answers.Count)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                var usr = msg.Author as IGuildUser;
 | 
			
		||||
                if (usr == null)
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                voteObj = new PollVote()
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = msg.Author.Id,
 | 
			
		||||
                    VoteIndex = vote,
 | 
			
		||||
                };
 | 
			
		||||
                if (!Poll.Votes.Add(voteObj))
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                var _ = OnVoted?.Invoke(msg, usr);
 | 
			
		||||
            }
 | 
			
		||||
            finally { _locker.Release(); }
 | 
			
		||||
            using (var uow = _db.UnitOfWork)
 | 
			
		||||
            {
 | 
			
		||||
                var trackedPoll = uow.Polls.Get(Poll.Id);
 | 
			
		||||
                trackedPoll.Votes.Add(voteObj);
 | 
			
		||||
                uow.Complete();
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void End()
 | 
			
		||||
        {
 | 
			
		||||
            OnVoted = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,9 @@ using NadekoBot.Extensions;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Games.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games
 | 
			
		||||
{
 | 
			
		||||
@@ -23,24 +26,33 @@ namespace NadekoBot.Modules.Games
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireUserPermission(GuildPermission.ManageMessages)]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public Task Poll([Remainder] string arg = null)
 | 
			
		||||
                => InternalStartPoll(arg);
 | 
			
		||||
            public async Task Poll([Remainder] string arg)
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(arg))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var poll = _service.CreatePoll(Context.Guild.Id,
 | 
			
		||||
                    Context.Channel.Id, arg);
 | 
			
		||||
                if (_service.StartPoll(poll))
 | 
			
		||||
                    await Context.Channel
 | 
			
		||||
                        .EmbedAsync(new EmbedBuilder()
 | 
			
		||||
                            .WithTitle(GetText("poll_created", Context.User.ToString()))
 | 
			
		||||
                            .WithDescription(string.Join("\n", poll.Answers
 | 
			
		||||
                                .Select(x => $"`{x.Index + 1}.` {Format.Bold(x.Text)}"))))
 | 
			
		||||
                        .ConfigureAwait(false);
 | 
			
		||||
                else
 | 
			
		||||
                    await ReplyErrorLocalized("poll_already_running").ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [RequireUserPermission(GuildPermission.ManageMessages)]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            public async Task PollStats()
 | 
			
		||||
            {
 | 
			
		||||
                if (!_service.ActivePolls.TryGetValue(Context.Guild.Id, out var poll))
 | 
			
		||||
                if (!_service.ActivePolls.TryGetValue(Context.Guild.Id, out var pr))
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                await Context.Channel.EmbedAsync(poll.GetStats(GetText("current_poll_results")));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private async Task InternalStartPoll(string arg)
 | 
			
		||||
            {
 | 
			
		||||
                if(await _service.StartPoll(Context.Guild.Id, Context.Message, arg) == false)
 | 
			
		||||
                    await ReplyErrorLocalized("poll_already_running").ConfigureAwait(false);
 | 
			
		||||
                await Context.Channel.EmbedAsync(GetStats(pr.Poll, GetText("current_poll_results")));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
@@ -50,8 +62,49 @@ namespace NadekoBot.Modules.Games
 | 
			
		||||
            {
 | 
			
		||||
                var channel = (ITextChannel)Context.Channel;
 | 
			
		||||
 | 
			
		||||
                if(_service.ActivePolls.TryRemove(channel.Guild.Id, out var poll))
 | 
			
		||||
                    await poll.StopPoll().ConfigureAwait(false);
 | 
			
		||||
                Poll p;
 | 
			
		||||
                if ((p = _service.StopPoll(Context.Guild.Id)) == null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var embed = GetStats(p, GetText("poll_closed"));
 | 
			
		||||
                await Context.Channel.EmbedAsync(embed)
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public EmbedBuilder GetStats(Poll poll, string title)
 | 
			
		||||
            {
 | 
			
		||||
                var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex)
 | 
			
		||||
                                    .ToDictionary(x => x.Key, x => x.Sum(kvp => 1))
 | 
			
		||||
                                    .OrderByDescending(kvp => kvp.Value)
 | 
			
		||||
                                    .ToArray();
 | 
			
		||||
 | 
			
		||||
                var eb = new EmbedBuilder().WithTitle(title);
 | 
			
		||||
 | 
			
		||||
                var sb = new StringBuilder()
 | 
			
		||||
                    .AppendLine(Format.Bold(poll.Question))
 | 
			
		||||
                    .AppendLine();
 | 
			
		||||
 | 
			
		||||
                var totalVotesCast = 0;
 | 
			
		||||
                if (results.Length == 0)
 | 
			
		||||
                {
 | 
			
		||||
                    sb.AppendLine(GetText("no_votes_cast"));
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    for (int i = 0; i < results.Length; i++)
 | 
			
		||||
                    {
 | 
			
		||||
                        var result = results[i];
 | 
			
		||||
                        sb.AppendLine(GetText("poll_result",
 | 
			
		||||
                            result.Key + 1,
 | 
			
		||||
                            Format.Bold(poll.Answers[result.Key].Text),
 | 
			
		||||
                            Format.Bold(result.Value.ToString())));
 | 
			
		||||
                        totalVotesCast += result.Value;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return eb.WithDescription(sb.ToString())
 | 
			
		||||
                    .WithFooter(efb => efb.WithText(GetText("x_votes_cast", totalVotesCast)))
 | 
			
		||||
                    .WithOkColor();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,45 +9,102 @@ using NadekoBot.Modules.Games.Common;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Impl;
 | 
			
		||||
using NLog;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using NadekoBot.Core.Services.Database;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Services
 | 
			
		||||
{
 | 
			
		||||
    public class PollService : IEarlyBlockingExecutor, INService
 | 
			
		||||
    {
 | 
			
		||||
        public ConcurrentDictionary<ulong, Poll> ActivePolls = new ConcurrentDictionary<ulong, Poll>();
 | 
			
		||||
        public ConcurrentDictionary<ulong, PollRunner> ActivePolls { get; } = new ConcurrentDictionary<ulong, PollRunner>();
 | 
			
		||||
        private readonly Logger _log;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly NadekoStrings _strings;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly NadekoStrings _strs;
 | 
			
		||||
 | 
			
		||||
        public PollService(DiscordSocketClient client, NadekoStrings strings)
 | 
			
		||||
        public PollService(DiscordSocketClient client, NadekoStrings strings, DbService db,
 | 
			
		||||
            NadekoStrings strs, IUnitOfWork uow)
 | 
			
		||||
        {
 | 
			
		||||
            _log = LogManager.GetCurrentClassLogger();
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _strs = strs;
 | 
			
		||||
 | 
			
		||||
            ActivePolls = uow.Polls.GetAllPolls()
 | 
			
		||||
                .ToDictionary(x => x.GuildId, x =>
 | 
			
		||||
                {
 | 
			
		||||
                    var pr = new PollRunner(db, x);
 | 
			
		||||
                    pr.OnVoted += Pr_OnVoted;
 | 
			
		||||
                    return pr;
 | 
			
		||||
                })
 | 
			
		||||
                .ToConcurrent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool?> StartPoll(ulong guildId, IUserMessage msg, string arg)
 | 
			
		||||
        public Poll CreatePoll(ulong guildId, ulong channelId, string input)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(arg) || !arg.Contains(";"))
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(input) || !input.Contains(";"))
 | 
			
		||||
                return null;
 | 
			
		||||
            var data = arg.Split(';');
 | 
			
		||||
            var data = input.Split(';');
 | 
			
		||||
            if (data.Length < 3)
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            var poll = new Poll(_client, _strings, msg, data[0], data.Skip(1));
 | 
			
		||||
            if (ActivePolls.TryAdd(guildId, poll))
 | 
			
		||||
            {
 | 
			
		||||
                poll.OnEnded += (gid) =>
 | 
			
		||||
                {
 | 
			
		||||
                    ActivePolls.TryRemove(gid, out _);
 | 
			
		||||
                };
 | 
			
		||||
            var col = new IndexedCollection<PollAnswer>(data.Skip(1)
 | 
			
		||||
                .Select(x => new PollAnswer() { Text = x }));
 | 
			
		||||
 | 
			
		||||
                await poll.StartPoll().ConfigureAwait(false);
 | 
			
		||||
            return new Poll()
 | 
			
		||||
            {
 | 
			
		||||
                Answers = col,
 | 
			
		||||
                Question = data[0],
 | 
			
		||||
                ChannelId = channelId,
 | 
			
		||||
                GuildId = guildId,
 | 
			
		||||
                Votes = new System.Collections.Generic.HashSet<PollVote>()
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool StartPoll(Poll p)
 | 
			
		||||
        {
 | 
			
		||||
            var pr = new PollRunner(_db,  p);
 | 
			
		||||
            if (ActivePolls.TryAdd(p.GuildId, pr))
 | 
			
		||||
            {
 | 
			
		||||
                using (var uow = _db.UnitOfWork)
 | 
			
		||||
                {
 | 
			
		||||
                    uow.Polls.Add(p);
 | 
			
		||||
                    uow.Complete();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                pr.OnVoted += Pr_OnVoted;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Poll StopPoll(ulong guildId)
 | 
			
		||||
        {
 | 
			
		||||
            if (ActivePolls.TryRemove(guildId, out var pr))
 | 
			
		||||
            {
 | 
			
		||||
                pr.OnVoted -= Pr_OnVoted;
 | 
			
		||||
                using (var uow = _db.UnitOfWork)
 | 
			
		||||
                {
 | 
			
		||||
                    uow.Polls.RemovePoll(pr.Poll.Id);
 | 
			
		||||
                    uow.Complete();
 | 
			
		||||
                }
 | 
			
		||||
                return pr.Poll;
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task Pr_OnVoted(IUserMessage msg, IGuildUser usr)
 | 
			
		||||
        {
 | 
			
		||||
            var toDelete = await msg.Channel.SendConfirmAsync(_strs.GetText("poll_voted", usr.Guild.Id, "Games".ToLowerInvariant(), Format.Bold(usr.ToString())))
 | 
			
		||||
                .ConfigureAwait(false);
 | 
			
		||||
            toDelete.DeleteAfter(5);
 | 
			
		||||
            try { await msg.DeleteAsync().ConfigureAwait(false); } catch { }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<bool> TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg)
 | 
			
		||||
        {
 | 
			
		||||
            if (guild == null)
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ namespace NadekoBot.Core.Services.Database
 | 
			
		||||
        IWarningsRepository Warnings { get; }
 | 
			
		||||
        IXpRepository Xp { get; }
 | 
			
		||||
        IClubRepository Clubs { get; }
 | 
			
		||||
        IPollsRepository Polls { get; }
 | 
			
		||||
 | 
			
		||||
        int Complete();
 | 
			
		||||
        Task<int> CompleteAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,16 @@ using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Services.Database.Models
 | 
			
		||||
{
 | 
			
		||||
    public class Poll
 | 
			
		||||
    public class Poll : DbEntity
 | 
			
		||||
    {
 | 
			
		||||
        public ulong GuildId { get; set; }
 | 
			
		||||
        public ulong ChannelId { get; set; }
 | 
			
		||||
        public string Question { get; set; }
 | 
			
		||||
        public IndexedCollection<PollAnswer> Answers { get; set; }
 | 
			
		||||
        public HashSet<PollVote> Votes { get; set; }
 | 
			
		||||
        public HashSet<PollVote> Votes { get; set; } = new HashSet<PollVote>();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class PollAnswer : IIndexed
 | 
			
		||||
    public class PollAnswer : DbEntity, IIndexed
 | 
			
		||||
    {
 | 
			
		||||
        public int Index { get; set; }
 | 
			
		||||
        public string Text { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,20 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Services.Database.Models
 | 
			
		||||
namespace NadekoBot.Core.Services.Database.Models
 | 
			
		||||
{
 | 
			
		||||
    public class PollVote
 | 
			
		||||
    public class PollVote : DbEntity
 | 
			
		||||
    {
 | 
			
		||||
        public ulong UserId { get; set; }
 | 
			
		||||
        public int VoteIndex { get; set; }
 | 
			
		||||
 | 
			
		||||
        public override int GetHashCode()
 | 
			
		||||
        {
 | 
			
		||||
            return UserId.GetHashCode();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public override bool Equals(object obj)
 | 
			
		||||
        {
 | 
			
		||||
            return obj is PollVote p
 | 
			
		||||
                ? p.UserId == UserId
 | 
			
		||||
                : false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -341,6 +341,12 @@ namespace NadekoBot.Core.Services.Database
 | 
			
		||||
                .WithMany(x => x.Bans);
 | 
			
		||||
 | 
			
		||||
            #endregion
 | 
			
		||||
 | 
			
		||||
            #region Polls
 | 
			
		||||
            modelBuilder.Entity<Poll>()
 | 
			
		||||
                .HasIndex(x => x.GuildId)
 | 
			
		||||
                .IsUnique();
 | 
			
		||||
            #endregion
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Services.Database.Repositories
 | 
			
		||||
{
 | 
			
		||||
    public interface IPollsRepository : IRepository<Poll>
 | 
			
		||||
    {
 | 
			
		||||
        IEnumerable<Poll> GetAllPolls();
 | 
			
		||||
        void RemovePoll(int id);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Services.Database.Repositories.Impl
 | 
			
		||||
{
 | 
			
		||||
    public class PollsRepository : Repository<Poll>, IPollsRepository
 | 
			
		||||
    {
 | 
			
		||||
        public PollsRepository(DbContext context) : base(context)
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<Poll> GetAllPolls()
 | 
			
		||||
        {
 | 
			
		||||
            return _set.Include(x => x.Answers)
 | 
			
		||||
                .Include(x => x.Votes)
 | 
			
		||||
                .ToArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void RemovePoll(int id)
 | 
			
		||||
        {
 | 
			
		||||
            var p = _set
 | 
			
		||||
                .Include(x => x.Answers)
 | 
			
		||||
                .Include(x => x.Votes)
 | 
			
		||||
                .FirstOrDefault(x => x.Id == id);
 | 
			
		||||
            p.Votes.Clear();
 | 
			
		||||
            p.Answers.Clear();
 | 
			
		||||
            _set.Remove(p);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -57,6 +57,9 @@ namespace NadekoBot.Core.Services.Database
 | 
			
		||||
        private IClubRepository _clubs;
 | 
			
		||||
        public IClubRepository Clubs => _clubs ?? (_clubs = new ClubRepository(_context));
 | 
			
		||||
 | 
			
		||||
        private IPollsRepository _polls;
 | 
			
		||||
        public IPollsRepository Polls => _polls ?? (_polls = new PollsRepository(_context));
 | 
			
		||||
 | 
			
		||||
        public UnitOfWork(NadekoContext context)
 | 
			
		||||
        {
 | 
			
		||||
            _context = context;
 | 
			
		||||
 
 | 
			
		||||
@@ -713,7 +713,8 @@
 | 
			
		||||
  "games_current_poll_results": "Current poll results",
 | 
			
		||||
  "games_no_votes_cast": "No votes cast.",
 | 
			
		||||
  "games_poll_already_running": "Poll is already running on this server.",
 | 
			
		||||
  "games_poll_created": "📃 {0} has created a poll which requires your attention:",
 | 
			
		||||
  "games_poll_created": "📃 {0} has created a poll",
 | 
			
		||||
  "games_poll_closed": "Poll Closed!",
 | 
			
		||||
  "games_poll_result": "`{0}.` {1} with {2} votes.",
 | 
			
		||||
  "games_poll_voted": "{0} voted.",
 | 
			
		||||
  "games_poll_vote_private": "Private Message me with the corresponding number of the answer.",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user