using Discord; using Discord.Commands; using EventBot.Services; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using System.Linq; using EventBot.Entities; using Discord.WebSocket; using Discord.Addons.Interactive; namespace EventBot.Modules { [RequireContext(ContextType.Guild)] [Name("Event")] public class EventModule : ModuleBase { private readonly EventManagementService _events; private readonly DatabaseService _database; public EventModule(EventManagementService events, DatabaseService database) { _events = events; _database = database; } [Command("join")] [Alias("j")] [Name("Join event")] [Summary("Joins latest or specified event with specified event role.")] public async Task JoinEvent( [Summary("Role emote or role ID to join the most recent event.")] string emoteOrId, [Summary("Extra information that might be needed by organizers.")] string extraInformation = null, [Summary("Optional event ID, used to join an event that started before the most recent one.")] Event @event = null) { EventRole er; if (!(Context.User is SocketGuildUser guildUser)) throw new Exception("This command must be executed inside a discord server."); if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null & !(int.TryParse(emoteOrId, out int roleId))) throw new Exception("Unable to locate any events for this discord server."); else if (@event == null) er = _database.EventRoles.FirstOrDefault(r => r.Id == roleId); else er = @event.Roles.FirstOrDefault(r => r.Emote == emoteOrId); if (er == null) throw new ArgumentException("Invalid emote or event ID specified."); if (@event.MessageId == 0) throw new Exception("You cannot join an event that hasn't been opened!"); await _events.TryJoinEvent(guildUser, er, extraInformation); await Context.Message.DeleteAsync(); // Protect somewhat sensitive data. } [RequireUserPermission(GuildPermission.Administrator, Group = "Permission")] [RequireOwner(Group = "Permission")] [Group("event")] //[Alias("e")] [Name("Event management")] public class EventManagementModule : InteractiveBase { private readonly EventManagementService _events; private readonly DatabaseService _database; public EventManagementModule(EventManagementService events, DatabaseService database) { _events = events; _database = database; } [Priority(2)] [Command("config logchannel")] [Name("Configure logging channel")] [Summary("Sets logging channel for role changes.")] public async Task ConfigureEventLogChannel( [Summary("Channel to use for logging.")] IChannel channel) { var guild = _database.GuildConfigs.FirstOrDefault(g => g.GuildId == Context.Guild.Id); if (guild == null) throw new Exception("This command must be executed inside a discord server."); guild.EventRoleConfirmationChannelId = channel.Id; var s = _database.SaveChangesAsync(); await ReplyAsync($"Event role changes now will be logged to `{channel.Name}` channel."); await s; } [Priority(2)] [Command("config partrole")] [Name("Configure participant role")] [Summary("Sets discord role to assign when the user selects a role.")] public async Task ConfigureEventParticipantRole( [Summary("Role to assign.")] IRole role) { var guild = _database.GuildConfigs.FirstOrDefault(g => g.GuildId == Context.Guild.Id); if (guild == null) throw new Exception("This command must be executed inside a discord server."); guild.ParticipantRoleId = role.Id; var s = _database.SaveChangesAsync(); await ReplyAsync($"Event participants will be given `{role.Name}` role."); await s; } [Priority(2)] [Command("config category")] [Name("Configure auto channel category")] [Summary("Configures auto channel category, where new channels will be created for each role.")] public async Task ConfigureParticipantCategory( [Summary("Category to use when making channels for roles.")] ICategoryChannel category = null) { var guild = _database.GuildConfigs.FirstOrDefault(g => g.GuildId == Context.Guild.Id); if (guild == null) throw new Exception("This command must be executed inside a discord server."); if (category == null) { guild.AutoRoleChannelCategoryId = 0; await ReplyAsync("No channels and discord roles will be created for event roles."); } else { guild.AutoRoleChannelCategoryId = category.Id; await ReplyAsync($"Bot will create discord roles and channels for each new role inside `{category.Name}` category."); } await _database.SaveChangesAsync(); } [Priority(1)] [Command("new")] [Alias("add", "create")] [Name("Create event")] [Summary("Creates a new event.")] public async Task CreateEvent( [Summary("Title for the event.")] string title, [Summary("Description for the event.")] string description, [Summary("Type of event registration.")] Event.EventParticipactionType type = Event.EventParticipactionType.Quick) { var guild = _database.GuildConfigs.FirstOrDefault(g => g.GuildId == Context.Guild.Id); if (guild == null) throw new Exception("This command must be executed inside a discord server."); var @event = new Event() { Title = title, Description = description, Type = type, Guild = guild }; _database.Add(@event); await _database.SaveChangesAsync(); await ReplyAsync($"Created new {@event.Type} event `{title}`, with description: `{description}`. Its ID is `{@event.Id}`."); } [Priority(2)] [Command("update title")] [Name("Update event's title")] [Summary("Updates the event's title.")] public async Task UpdateEventTitle( [Summary("Title for the event.")] string title, [Summary("Event to update, if not specified, updates latest event.")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("Unable to locate any events for this discord server."); if (!@event.Active) throw new Exception("This event is finalized. Please make a new event."); @event.Title = title; await _database.SaveChangesAsync(); await ReplyAsync($"Updated event's (`{@event.Id}`) title to `{@event.Title}`"); await _events.UpdateEventMessage(@event); } [Priority(2)] [Command("update description")] [Alias("update desc")] [Name("Update event's description")] [Summary("Updates the event's description.")] public async Task UpdateEventDescription( [Summary("Description for the event.")] string description, [Summary("Event to update, if not specified, updates latest event.")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("Unable to locate any events for this discord server."); if (!@event.Active) throw new Exception("This event is finalized. Please make a new event."); @event.Description = description; await _database.SaveChangesAsync(); await ReplyAsync($"Updated event's (`{@event.Id}`) description to `{@event.Description}`"); await _events.UpdateEventMessage(@event); } [Priority(2)] [Command("update type")] [Name("Update event's type")] [Summary("Updates the event type.")] public async Task UpdateEventType( [Summary("Type of event registration.")] Event.EventParticipactionType type, [Summary("Event to update, if not specified, updates latest event.")] Event @event = null) { if (type == Event.EventParticipactionType.Unspecified) return; if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("Unable to locate any events for this discord server."); if (!@event.Active) throw new Exception("This event is finalized. Please make a new event."); if (@event.MessageId != 0 && @event.Type != type) throw new Exception("Can't change event registration type when it's open for registration. Maybe you meant to set type to `Unspecified` (-1)"); if(@event.Type != type) @event.Type = type; await _database.SaveChangesAsync(); await ReplyAsync($"Updated event(`{@event.Id}`) type to `{@event.Type}`"); } [Priority(2)] [Command("role new")] [Alias("role add", "role create")] [Name("Add role")] [Summary("Adds a new role to the event.")] public async Task NewEventRole( [Summary("Title of the role.")] string title, [Summary("Description of the role.")] string description, [Summary("Emote for the role.")] string emote, [Summary("Max openings, if number is negative, opening count is unlimited.")] int maxOpenings = -1, [Summary("Which event to assign the role to.")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("Unable to locate any events for this discord server."); if (!@event.Active) throw new Exception("This event is finalized. Please make a new event."); if (@event.Roles != null && @event.Roles.Count >= 20) throw new Exception("There are too many roles for this event."); if(@event.MessageId != 0) throw new Exception("Can't add new roles to event with open registration."); if (!EmoteHelper.TryParse(emote, out IEmote parsedEmote)) throw new ArgumentException("Invalid emote provided."); if(@event.Roles != null && @event.Roles.Count(r => r.Emote == parsedEmote.ToString()) > 0) throw new ArgumentException("This emote is already used by other role."); var er = new EventRole() { Title = title, Description = description, MaxParticipants = maxOpenings, Emote = parsedEmote.ToString(), Event = @event }; if(@event.Guild.AutoRoleChannelCategoryId != 0) { var channel = await Context.Guild.CreateTextChannelAsync(er.ChannelName, o => o.CategoryId = @event.Guild.AutoRoleChannelCategoryId); var role = await Context.Guild.CreateRoleAsync(er.Title); await channel.AddPermissionOverwriteAsync(role, new OverwritePermissions(viewChannel: PermValue.Allow, sendMessages: PermValue.Allow)); er.RoleId = role.Id; er.ChannelId = channel.Id; } _database.Add(er); await _database.SaveChangesAsync(); await ReplyAsync($"Added event role `{er.Id}` for event `{er.Event.Id}`, title: `{er.Title}`, description: `{er.Description}`, slots: `{er.MaxParticipants}`, emote: {er.Emote}"); } [Priority(2)] [Command("role update title")] [Name("Update role's title")] [Summary("Updates the role's title.")] public async Task UpdateEventRoleTitle( [Summary("Which role to update.")] EventRole eventRole, [Summary("The new title for the role.")][Remainder] string title) { if(eventRole == null) throw new Exception("Please provide the correct role info, this one does not exist."); if (!eventRole.Event.Active) throw new Exception("This event is finalized. Please make a new event."); eventRole.Title = title; var s = _database.SaveChangesAsync(); await ReplyAsync($"Updated event role `{eventRole.Id}`'s title to `{eventRole.Title}`"); await s; await _events.UpdateEventMessage(eventRole.Event); } [Priority(2)] [Command("role update description")] [Alias("role update desc")] [Name("Update role's description")] [Summary("Updates the role's description.")] public async Task UpdateEventRoleDescription( [Summary("Which role to update.")] EventRole eventRole, [Summary("New description for the role.")][Remainder] string description) { if (eventRole == null) throw new Exception("Please provide the correct role ID, this one does not exist."); if (!eventRole.Event.Active) throw new Exception("This event is finalized. Please make a new event."); eventRole.Description = description; var s = _database.SaveChangesAsync(); await ReplyAsync($"Updated event role's `{eventRole.Id}` description to `{eventRole.Description}`"); await s; await _events.UpdateEventMessage(eventRole.Event); } [Priority(2)] [Command("role update slots")] [Name("Update role's slots")] [Summary("Updates a role's maximum participants count.")] public async Task UpdateEventRoleMaxParticipants( [Summary("Which role to update.")] EventRole eventRole, [Summary("New maximum participant count for role.")] int maxParticipants) { if (eventRole == null) throw new Exception("Please provide the correct role info, this one does not exist."); if (!eventRole.Event.Active) throw new Exception("This event is finalized. Please make a new event."); eventRole.MaxParticipants = maxParticipants; var s = _database.SaveChangesAsync(); await ReplyAsync($"Updated event role `{eventRole.Id}`'s maximum participant count to `{eventRole.MaxParticipants}`"); await s; await _events.UpdateEventMessage(eventRole.Event); } [Priority(2)] [Command("role update emote")] [Name("Update role's role")] [Summary("Updates a role's emote.")] public async Task UpdateEventRoleEmote( [Summary("Which role to update.")] EventRole eventRole, [Summary("New emote for the role.")] string emote) { if (eventRole == null) throw new Exception("Please provide the correct role info, this one does not exist."); if (eventRole.Event.MessageId != 0) throw new Exception("A role's emote cannot be edited while the event is open for registration."); if (!eventRole.Event.Active) throw new Exception("This event is finalized. Please make a new event."); if (!EmoteHelper.TryParse(emote, out IEmote parsedEmote)) throw new ArgumentException("Invalid emote provided."); if (eventRole.Event.Roles.Count(r => r.Emote == parsedEmote.ToString()) > 0) throw new ArgumentException("This emote is already used by other role."); eventRole.Emote = parsedEmote.ToString(); var s = _database.SaveChangesAsync(); await ReplyAsync($"Updated event role `{eventRole.Id}` emote to {eventRole.Emote}"); await s; } [Command()] [Alias("info")] [Name("Event info")] [Summary("Get info about event.")] public async Task EventInfo( [Summary("Event ID of event you wish to know more of.")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("No events were found for this discord server."); var embed = new EmbedBuilder() .WithTitle(@event.Title) .WithDescription(@event.Description) .WithTimestamp(@event.Opened) .WithFooter($"EventId: {@event.Id}; MessageID: {@event.MessageId}; MessageChannelID: {@event.MessageChannelId}") .AddField("Active", @event.Active ? "Yes" : "No", true) .AddField("Type", @event.Type, true) .AddField("Participants", @event.ParticipantCount, true); if (@event.Roles != null) embed.WithFields(@event.Roles.OrderBy(e => e.SortNumber).Select(r => new EmbedFieldBuilder() .WithName($"Id: `{r.Id}` {r.Emote} `{r.Title}` - {r.ParticipantCount} participants; {(r.MaxParticipants < 0 ? "infinite" : r.MaxParticipants.ToString())} max slots.") .WithValue(r.ParticipantCount == 0 ? "There are no participants" : string.Join("\r\n", r.Participants .Select(p => new { Participant = p, User = Context.Guild.GetUser(p.UserId) }) .OrderBy(o => o.User.ToString()) .Select(o => $"{o.User}{(o.Participant.UserData != null ? $" - `{o.Participant.UserData}`" : "")}") )) )); await ReplyAsync(embed: embed.Build()); } [Priority(2)] [Command("role delete")] [Alias("role remove")] [Name("Delete role")] [Summary("Deletes role and all information about it.")] public async Task EventRoleDelete( [Summary("Role you wish to delete.")] EventRole eventRole) { if (!(Context.User is SocketGuildUser guildUser)) throw new Exception("This command must be executed inside a discord server."); if (eventRole == null) throw new Exception("Please provide the correct role, this one does not exist."); if (eventRole.Event.MessageId != 0) throw new Exception("Can't remove role from open event."); foreach (var p in eventRole.Participants) await _events.RemoveParticipant(p, guildUser); if(eventRole.ChannelId != 0) await Context.Guild.GetTextChannel(eventRole.ChannelId)?.DeleteAsync(); if (eventRole.RoleId != 0) await Context.Guild.GetRole(eventRole.RoleId)?.DeleteAsync(); _database.Remove(eventRole); await _database.SaveChangesAsync(); await ReplyAsync($"Role {eventRole.Title} has been deleted, and it's participants removed."); } [Priority(1)] [Command("role")] [Alias("role info")] [Name("Role info")] [Summary("Gets info about a role.")] public async Task EventRoleInfo( [Summary("Role you wish to have more info about.")] EventRole eventRole) { if (eventRole == null) throw new Exception("Please provide the correct role info, this one does not exist."); var embed = new EmbedBuilder() .WithTitle($"{eventRole.Emote} {eventRole.Title}") .WithDescription($"{eventRole.Description}") .WithFooter($"EventRoleId: {eventRole.Id}, EventId: {eventRole.Event.Id}") .AddField("Max participants", eventRole.MaxParticipants < 0 ? "infinite" : eventRole.MaxParticipants.ToString(), true) .AddField("Participants", eventRole.ParticipantCount, true); var msg = await ReplyAsync(embed: embed.Build()); if (eventRole.Participants != null && eventRole.Participants.Count > 0) { embed.AddField("Participants", string.Join("\r\n", eventRole.Participants.Select(p => $"id: `{p.Id}` <@{p.UserId}>{(p.UserData != null ? $" - {p.UserData}" : "")}"))); await msg.ModifyAsync(m => m.Embed = embed.Build()); } } [Priority(1)] [Command("open")] [Alias("start", "begin")] [Name("Open event")] [Summary("Open registration for event here.")] public async Task EventOpen( [Summary("Event to open")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("No events were found for this discord server."); if (!@event.Active) throw new Exception("This event is finalized. Please make a new event."); await Context.Message.DeleteAsync(); var message = await ReplyAsync(embed: _events.GenerateEventEmbed(@event).Build()); @event.MessageId = message.Id; @event.MessageChannelId = message.Channel.Id; await _database.SaveChangesAsync(); switch (@event.Type) { case Event.EventParticipactionType.Unspecified: throw new Exception("Event type was unspecified."); case Event.EventParticipactionType.Quick: await message.AddReactionsAsync(@event.Roles.OrderBy(e => e.SortNumber).Select(r => EmoteHelper.Parse(r.Emote)).ToArray()); break; case Event.EventParticipactionType.Detailed: break; default: throw new Exception("Event type in not implemented."); } } [Priority(1)] [Command("close")] [Alias("stop", "end")] [Name("Close event")] [Summary("Closes event registration.")] public async Task EventClose( [Summary("Event to close")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("No events were found for this discord server."); if (!@event.Active) throw new Exception("This event is finalized. Please make a new event."); if (@event.MessageId == 0) throw new Exception("This event is already closed for registration."); @event.MessageId = 0; @event.MessageChannelId = 0; await _database.SaveChangesAsync(); await ReplyAsync($"Event's `{@event.Id}` registration has been closed, its registration message will now be normal message."); } [Priority(1)] [Command("finalize")] [Alias("archive")] [Name("Archive event")] [Summary("Archives event and reverts all role additions. This is irreversable.")] public async Task EventFinilize( [Summary("Event to finilize")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("No events were found for this discord server."); if (!@event.Active) throw new Exception("This event is already finalized."); @event.Active = false; await _database.SaveChangesAsync(); await ReplyAsync($"Event `{@event.Id}` has been finalized. Removing participant roles.."); if (@event.Guild.ParticipantRoleId != 0) foreach (var participant in @event.Participants) { var user = Context.Guild.GetUser(participant.UserId); await user.RemoveRoleAsync(Context.Guild.GetRole(@event.Guild.ParticipantRoleId)); } foreach (var role in @event.Roles) { if (role.ChannelId != 0) await Context.Guild.GetTextChannel(role.ChannelId)?.DeleteAsync(); if (role.RoleId != 0) await Context.Guild.GetRole(role.RoleId)?.DeleteAsync(); } await ReplyAsync($"Everyone's roles have been removed. I hope it was fun!"); } [Priority(1)] [Command("list")] [Alias("all")] [Name("Lists events")] [Summary("Lists all prevous events that took on this server.")] public async Task EventArchive() { var guildEvents = _database.Events.Where(e => e.GuildId == Context.Guild.Id).OrderBy(e => e.Opened).ToList(); if (guildEvents.Count() == 0) throw new Exception("There are no events that have been run on this server."); var pagedEvents = guildEvents .Select((e, i) => new { Event = e, Index = i }) .GroupBy(o => o.Index / 6) .Select(g => g.Select(o => o.Event)); var pager = new PaginatedMessage() { Title = "Lists all previous events.", Color = Color.Blue, Options = new PaginatedAppearanceOptions() { Timeout = new TimeSpan(0, 3, 0), DisplayInformationIcon = false, JumpDisplayOptions = JumpDisplayOptions.Never } }; pager.Pages = pagedEvents.Select(eg => string.Join("\r\n", eg.Select(e => $"`{e.Id}` **{e.Title}** {(e.Active ? "✅" : "❌")}\r\n" + $"Opened at {e.Opened.ToShortDateString()} {e.Opened.ToShortTimeString()}" )) ); await PagedReplyAsync(pager); } [Priority(2)] [Command("participant add")] [Name("Add participant")] [Summary("Add user to event role. Acts like join command.")] public async Task EventParticipantAdd( [Summary("User ID or discord mention.")] IUser user, [Summary("Role emote or role ID to join.")] string emoteOrId, [Summary("Extra information that might be needed by organizers.")] string extraInformation = null, [Summary("Optional event ID for joining event that is not the most recent one.")] Event @event = null) { EventRole er; if (!(user is SocketGuildUser guildUser)) throw new Exception("This command must be executed inside a discord server."); if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null & !(int.TryParse(emoteOrId, out int roleId))) throw new Exception("Unable to locate any events for this discord server."); else if (@event == null || roleId != 0) er = _database.EventRoles.FirstOrDefault(r => r.Id == roleId); else er = @event.Roles.FirstOrDefault(r => r.Emote == emoteOrId); if (er == null) throw new ArgumentException("Invalid emote or event ID specified"); if (!er.Event.Active) throw new Exception("This event is finalized. Please make a new event."); await _events.TryJoinEvent(guildUser, er, extraInformation, false); await Context.Message.DeleteAsync(); // Protect somewhat sensitive data. } [Priority(2)] [Command("participant remove")] [Alias("participant delete")] [Name("Remove participant")] [Summary("Remove participant from event role.")] public async Task EventParticipantRemove( [Summary("User that is participating's ID or discord mention.")] IUser user, [Summary("Event to remove the participant from.")] Event @event = null) { if (@event == null) @event = _events.FindEventBy(Context.Guild); if (@event == null) throw new Exception("No events were found for this discord server."); if (!@event.Active) throw new Exception("This event is finalized. Please make a new event."); if (!(user is IGuildUser guildUser) || !(Context.User is IGuildUser initiator)) throw new Exception("This command must be executed inside a discord server."); await _events.RemoveParticipant(guildUser, @event, initiator); await _database.SaveChangesAsync(); await ReplyAsync($"{guildUser} has been removed from event."); await _events.UpdateEventMessage(@event); } } } }