Polls persist restarts now.
This commit is contained in:
		| @@ -10,6 +10,10 @@ namespace NadekoBot.Common.Collections | |||||||
|         public List<T> Source { get; } |         public List<T> Source { get; } | ||||||
|         private readonly object _locker = new object(); |         private readonly object _locker = new object(); | ||||||
|  |  | ||||||
|  |         public IndexedCollection() | ||||||
|  |         { | ||||||
|  |             Source = new List<T>(); | ||||||
|  |         } | ||||||
|         public IndexedCollection(IEnumerable<T> source) |         public IndexedCollection(IEnumerable<T> source) | ||||||
|         { |         { | ||||||
|             lock (_locker) |             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"); |                     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 => |             modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.Quote", b => | ||||||
|                 { |                 { | ||||||
|                     b.Property<int>("Id") |                     b.Property<int>("Id") | ||||||
| @@ -1771,6 +1832,20 @@ namespace NadekoBot.Migrations | |||||||
|                         .OnDelete(DeleteBehavior.Cascade); |                         .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 => |             modelBuilder.Entity("NadekoBot.Core.Services.Database.Models.RaceAnimal", b => | ||||||
|                 { |                 { | ||||||
|                     b.HasOne("NadekoBot.Core.Services.Database.Models.BotConfig") |                     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 System.Threading.Tasks; | ||||||
| using NadekoBot.Common.Attributes; | using NadekoBot.Common.Attributes; | ||||||
| using NadekoBot.Modules.Games.Services; | using NadekoBot.Modules.Games.Services; | ||||||
|  | using NadekoBot.Core.Services.Database.Models; | ||||||
|  | using System.Text; | ||||||
|  | using System.Linq; | ||||||
|  |  | ||||||
| namespace NadekoBot.Modules.Games | namespace NadekoBot.Modules.Games | ||||||
| { | { | ||||||
| @@ -23,24 +26,33 @@ namespace NadekoBot.Modules.Games | |||||||
|             [NadekoCommand, Usage, Description, Aliases] |             [NadekoCommand, Usage, Description, Aliases] | ||||||
|             [RequireUserPermission(GuildPermission.ManageMessages)] |             [RequireUserPermission(GuildPermission.ManageMessages)] | ||||||
|             [RequireContext(ContextType.Guild)] |             [RequireContext(ContextType.Guild)] | ||||||
|             public Task Poll([Remainder] string arg = null) |             public async Task Poll([Remainder] string arg) | ||||||
|                 => InternalStartPoll(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] |             [NadekoCommand, Usage, Description, Aliases] | ||||||
|             [RequireUserPermission(GuildPermission.ManageMessages)] |             [RequireUserPermission(GuildPermission.ManageMessages)] | ||||||
|             [RequireContext(ContextType.Guild)] |             [RequireContext(ContextType.Guild)] | ||||||
|             public async Task PollStats() |             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; |                     return; | ||||||
|  |  | ||||||
|                 await Context.Channel.EmbedAsync(poll.GetStats(GetText("current_poll_results"))); |                 await Context.Channel.EmbedAsync(GetStats(pr.Poll, 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); |  | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             [NadekoCommand, Usage, Description, Aliases] |             [NadekoCommand, Usage, Description, Aliases] | ||||||
| @@ -50,9 +62,50 @@ namespace NadekoBot.Modules.Games | |||||||
|             { |             { | ||||||
|                 var channel = (ITextChannel)Context.Channel; |                 var channel = (ITextChannel)Context.Channel; | ||||||
|  |  | ||||||
|                 if(_service.ActivePolls.TryRemove(channel.Guild.Id, out var poll)) |                 Poll p; | ||||||
|                     await poll.StopPoll().ConfigureAwait(false); |                 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; | ||||||
| using NadekoBot.Core.Services.Impl; | using NadekoBot.Core.Services.Impl; | ||||||
| using NLog; | 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 | namespace NadekoBot.Modules.Games.Services | ||||||
| { | { | ||||||
|     public class PollService : IEarlyBlockingExecutor, INService |     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 Logger _log; | ||||||
|         private readonly DiscordSocketClient _client; |         private readonly DiscordSocketClient _client; | ||||||
|         private readonly NadekoStrings _strings; |         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(); |             _log = LogManager.GetCurrentClassLogger(); | ||||||
|             _client = client; |             _client = client; | ||||||
|             _strings = strings; |             _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; |                 return null; | ||||||
|             var data = arg.Split(';'); |             var data = input.Split(';'); | ||||||
|             if (data.Length < 3) |             if (data.Length < 3) | ||||||
|                 return null; |                 return null; | ||||||
|  |  | ||||||
|             var poll = new Poll(_client, _strings, msg, data[0], data.Skip(1)); |             var col = new IndexedCollection<PollAnswer>(data.Skip(1) | ||||||
|             if (ActivePolls.TryAdd(guildId, poll)) |                 .Select(x => new PollAnswer() { Text = x })); | ||||||
|             { |  | ||||||
|                 poll.OnEnded += (gid) => |  | ||||||
|                 { |  | ||||||
|                     ActivePolls.TryRemove(gid, out _); |  | ||||||
|                 }; |  | ||||||
|  |  | ||||||
|                 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 true; | ||||||
|             } |             } | ||||||
|             return false; |             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) |         public async Task<bool> TryExecuteEarly(DiscordSocketClient client, IGuild guild, IUserMessage msg) | ||||||
|         { |         { | ||||||
|             if (guild == null) |             if (guild == null) | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ namespace NadekoBot.Core.Services.Database | |||||||
|         IWarningsRepository Warnings { get; } |         IWarningsRepository Warnings { get; } | ||||||
|         IXpRepository Xp { get; } |         IXpRepository Xp { get; } | ||||||
|         IClubRepository Clubs { get; } |         IClubRepository Clubs { get; } | ||||||
|  |         IPollsRepository Polls { get; } | ||||||
|  |  | ||||||
|         int Complete(); |         int Complete(); | ||||||
|         Task<int> CompleteAsync(); |         Task<int> CompleteAsync(); | ||||||
|   | |||||||
| @@ -3,15 +3,16 @@ using System.Collections.Generic; | |||||||
|  |  | ||||||
| namespace NadekoBot.Core.Services.Database.Models | namespace NadekoBot.Core.Services.Database.Models | ||||||
| { | { | ||||||
|     public class Poll |     public class Poll : DbEntity | ||||||
|     { |     { | ||||||
|         public ulong GuildId { get; set; } |         public ulong GuildId { get; set; } | ||||||
|  |         public ulong ChannelId { get; set; } | ||||||
|         public string Question { get; set; } |         public string Question { get; set; } | ||||||
|         public IndexedCollection<PollAnswer> Answers { 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 int Index { get; set; } | ||||||
|         public string Text { get; set; } |         public string Text { get; set; } | ||||||
|   | |||||||
| @@ -1,13 +1,20 @@ | |||||||
| using System; | namespace NadekoBot.Core.Services.Database.Models | ||||||
| using System.Collections.Generic; |  | ||||||
| using System.Linq; |  | ||||||
| using System.Text; |  | ||||||
| using System.Threading.Tasks; |  | ||||||
|  |  | ||||||
| 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); |                 .WithMany(x => x.Bans); | ||||||
|  |  | ||||||
|             #endregion |             #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; |         private IClubRepository _clubs; | ||||||
|         public IClubRepository Clubs => _clubs ?? (_clubs = new ClubRepository(_context)); |         public IClubRepository Clubs => _clubs ?? (_clubs = new ClubRepository(_context)); | ||||||
|  |  | ||||||
|  |         private IPollsRepository _polls; | ||||||
|  |         public IPollsRepository Polls => _polls ?? (_polls = new PollsRepository(_context)); | ||||||
|  |  | ||||||
|         public UnitOfWork(NadekoContext context) |         public UnitOfWork(NadekoContext context) | ||||||
|         { |         { | ||||||
|             _context = context; |             _context = context; | ||||||
|   | |||||||
| @@ -713,7 +713,8 @@ | |||||||
|   "games_current_poll_results": "Current poll results", |   "games_current_poll_results": "Current poll results", | ||||||
|   "games_no_votes_cast": "No votes cast.", |   "games_no_votes_cast": "No votes cast.", | ||||||
|   "games_poll_already_running": "Poll is already running on this server.", |   "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_result": "`{0}.` {1} with {2} votes.", | ||||||
|   "games_poll_voted": "{0} voted.", |   "games_poll_voted": "{0} voted.", | ||||||
|   "games_poll_vote_private": "Private Message me with the corresponding number of the answer.", |   "games_poll_vote_private": "Private Message me with the corresponding number of the answer.", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user