From 81a82cbb5b1dc4296f0346985465e4ea7bf38d43 Mon Sep 17 00:00:00 2001 From: Karolis2011 Date: Tue, 18 Jun 2019 19:17:06 +0300 Subject: [PATCH] Inital bot commit --- .gitignore | 37 ++ EventBot.sln | 25 ++ EventBot/Attributes/NoHelpAttribute.cs | 11 + EventBot/Entities/Event.cs | 44 ++ EventBot/Entities/EventParticipant.cs | 24 + EventBot/Entities/EventRole.cs | 34 ++ EventBot/Entities/GuildConfig.cs | 18 + EventBot/EventBot.csproj | 44 ++ EventBot/Misc/EventRoleTypeReader.cs | 31 ++ EventBot/Misc/EventTypeReader.cs | 30 ++ EventBot/Modules/BasicModule.cs | 84 ++++ EventBot/Modules/EventModule.cs | 413 ++++++++++++++++++ EventBot/Modules/TestingModule.cs | 18 + EventBot/Program.cs | 72 +++ .../FolderProfile-linux.pubxml | 16 + .../FolderProfile-win-x86.pubxml | 16 + .../PublishProfiles/FolderProfile-win.pubxml | 16 + EventBot/Properties/launchSettings.json | 10 + EventBot/Services/CommandHandlingService.cs | 94 ++++ EventBot/Services/DatabaseService.cs | 74 ++++ EventBot/Services/EmoteService.cs | 46 ++ EventBot/Services/EventManagementService.cs | 152 +++++++ EventBot/Services/MySqlDatabaseService.cs | 18 + EventBot/Services/SqliteDatabaseService.cs | 19 + EventBot/data.db | Bin 0 -> 49152 bytes 25 files changed, 1346 insertions(+) create mode 100644 .gitignore create mode 100644 EventBot.sln create mode 100644 EventBot/Attributes/NoHelpAttribute.cs create mode 100644 EventBot/Entities/Event.cs create mode 100644 EventBot/Entities/EventParticipant.cs create mode 100644 EventBot/Entities/EventRole.cs create mode 100644 EventBot/Entities/GuildConfig.cs create mode 100644 EventBot/EventBot.csproj create mode 100644 EventBot/Misc/EventRoleTypeReader.cs create mode 100644 EventBot/Misc/EventTypeReader.cs create mode 100644 EventBot/Modules/BasicModule.cs create mode 100644 EventBot/Modules/EventModule.cs create mode 100644 EventBot/Modules/TestingModule.cs create mode 100644 EventBot/Program.cs create mode 100644 EventBot/Properties/PublishProfiles/FolderProfile-linux.pubxml create mode 100644 EventBot/Properties/PublishProfiles/FolderProfile-win-x86.pubxml create mode 100644 EventBot/Properties/PublishProfiles/FolderProfile-win.pubxml create mode 100644 EventBot/Properties/launchSettings.json create mode 100644 EventBot/Services/CommandHandlingService.cs create mode 100644 EventBot/Services/DatabaseService.cs create mode 100644 EventBot/Services/EmoteService.cs create mode 100644 EventBot/Services/EventManagementService.cs create mode 100644 EventBot/Services/MySqlDatabaseService.cs create mode 100644 EventBot/Services/SqliteDatabaseService.cs create mode 100644 EventBot/data.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0626272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ \ No newline at end of file diff --git a/EventBot.sln b/EventBot.sln new file mode 100644 index 0000000..3efe70b --- /dev/null +++ b/EventBot.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.452 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventBot", "EventBot\EventBot.csproj", "{CADB4675-F1B7-4269-A225-C0ADCCE88C4F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CADB4675-F1B7-4269-A225-C0ADCCE88C4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CADB4675-F1B7-4269-A225-C0ADCCE88C4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CADB4675-F1B7-4269-A225-C0ADCCE88C4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CADB4675-F1B7-4269-A225-C0ADCCE88C4F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A81E493A-0CEA-4E01-8609-79D2E9D49D09} + EndGlobalSection +EndGlobal diff --git a/EventBot/Attributes/NoHelpAttribute.cs b/EventBot/Attributes/NoHelpAttribute.cs new file mode 100644 index 0000000..4e5b861 --- /dev/null +++ b/EventBot/Attributes/NoHelpAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EventBot.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + class NoHelpAttribute : Attribute + { + } +} diff --git a/EventBot/Entities/Event.cs b/EventBot/Entities/Event.cs new file mode 100644 index 0000000..63eb6d2 --- /dev/null +++ b/EventBot/Entities/Event.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; +using System.Linq; + +namespace EventBot.Entities +{ + public class Event + { + public Event() + { + Active = true; + Opened = DateTime.Now; + } + + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public bool Active { get; set; } + public ulong MessageId { get; set; } + public ulong MessageChannelId { get; set; } + public virtual ICollection Roles { get; set; } + public virtual ICollection Participants { get; set; } + public int ParticipantCount => Participants == null ? 0 : Participants.Count; + public DateTime Opened { get; set; } + public virtual EventParticipactionType Type { get; set; } + public ulong GuildId { get; set; } + [ForeignKey("GuildId")] + public virtual GuildConfig Guild { get; set; } + public int RemainingOpenings => Roles.Sum(r => r.ReamainingOpenings); + + public enum EventParticipactionType + { + Unspecified = -1, + Quick, + Detailed + } + + } +} diff --git a/EventBot/Entities/EventParticipant.cs b/EventBot/Entities/EventParticipant.cs new file mode 100644 index 0000000..28fa4f4 --- /dev/null +++ b/EventBot/Entities/EventParticipant.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace EventBot.Entities +{ + public class EventParticipant + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public int EventRoleId { get; set; } + [ForeignKey("EventRoleId")] + public virtual EventRole Role { get; set; } + public int EventId { get; set; } + [ForeignKey("EventId")] + public virtual Event Event { get; set; } + public ulong UserId { get; set; } + public string UserData { get; set; } + } +} diff --git a/EventBot/Entities/EventRole.cs b/EventBot/Entities/EventRole.cs new file mode 100644 index 0000000..ce690a2 --- /dev/null +++ b/EventBot/Entities/EventRole.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace EventBot.Entities +{ + public class EventRole + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string Emote { get; set; } + public int MaxParticipants { get; set; } + public int EventId { get; set; } + [ForeignKey("EventId")] + public virtual Event Event { get; set; } + public virtual ICollection Participants { get; set; } + public int ParticipantCount => Participants == null ? 0 : Participants.Count; + public int ReamainingOpenings => MaxParticipants < 0 ? 1 : MaxParticipants - ParticipantCount; + + public int SortNumber + { + get + { + if (MaxParticipants < 0) return int.MaxValue; + return MaxParticipants; + } + } + } +} diff --git a/EventBot/Entities/GuildConfig.cs b/EventBot/Entities/GuildConfig.cs new file mode 100644 index 0000000..77f0413 --- /dev/null +++ b/EventBot/Entities/GuildConfig.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace EventBot.Entities +{ + public class GuildConfig + { + [Key] + public ulong GuildId { get; set; } + public string Prefix { get; set; } + public ulong EventRoleConfirmationChannelId { get; set; } + public ulong ParticipantRoleId { get; set; } + public virtual ICollection Events { get; set; } + + } +} diff --git a/EventBot/EventBot.csproj b/EventBot/EventBot.csproj new file mode 100644 index 0000000..b29189f --- /dev/null +++ b/EventBot/EventBot.csproj @@ -0,0 +1,44 @@ + + + + Exe + netcoreapp2.2 + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + PreserveNewest + + + + + + + + diff --git a/EventBot/Misc/EventRoleTypeReader.cs b/EventBot/Misc/EventRoleTypeReader.cs new file mode 100644 index 0000000..f478f37 --- /dev/null +++ b/EventBot/Misc/EventRoleTypeReader.cs @@ -0,0 +1,31 @@ +using Discord.Commands; +using EventBot.Entities; +using EventBot.Services; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace EventBot.Misc +{ + class EventRoleTypeReader : TypeReader + { + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + var database = services.GetRequiredService(); + if (context.Guild == null) + return Task.FromResult(TypeReaderResult.FromError(CommandError.UnmetPrecondition, "Event roles are avaivable only inside guild context.")); + EventRole er = null; + if (int.TryParse(input, out int id)) + er = database.EventRoles.FirstOrDefault(r => r.Id == id); + else + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Event role id is not a valid number.")); + if(er == null) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Specified event role was not found.")); + if(er.Event?.GuildId != context.Guild.Id) + return Task.FromResult(TypeReaderResult.FromError(CommandError.Exception, "Cross guild event role access is denied.")); + return Task.FromResult(TypeReaderResult.FromSuccess(er)); + } + } +} diff --git a/EventBot/Misc/EventTypeReader.cs b/EventBot/Misc/EventTypeReader.cs new file mode 100644 index 0000000..9e15100 --- /dev/null +++ b/EventBot/Misc/EventTypeReader.cs @@ -0,0 +1,30 @@ +using Discord.Commands; +using EventBot.Entities; +using EventBot.Services; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace EventBot.Misc +{ + class EventTypeReader : TypeReader + { + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + var events = services.GetRequiredService(); + if (context.Guild == null) + return Task.FromResult(TypeReaderResult.FromError(CommandError.UnmetPrecondition, "Events are avaivable only inside guild context.")); + Event ev; + if (input == null) + ev = events.FindEventBy(context.Guild); + else if (int.TryParse(input, out int id)) + ev = events.FindEventBy(context.Guild, id); + else + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Event id is not a number.")); + + return Task.FromResult(TypeReaderResult.FromSuccess(ev)); + } + } +} diff --git a/EventBot/Modules/BasicModule.cs b/EventBot/Modules/BasicModule.cs new file mode 100644 index 0000000..a20baca --- /dev/null +++ b/EventBot/Modules/BasicModule.cs @@ -0,0 +1,84 @@ +using Discord; +using Discord.Commands; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Linq; +using EventBot.Attributes; +using EventBot.Services; + +namespace EventBot.Modules +{ + public class BasicModule : ModuleBase + { + private readonly DatabaseService _database; + public BasicModule(DatabaseService database) + { + _database = database; + } + + [RequireUserPermission(GuildPermission.Administrator, Group = "Permission")] + [RequireOwner(Group = "Permission")] + [RequireContext(ContextType.Guild)] + [Command("prefix")] + [Summary("Gets prefix.")] + public async Task PrefixCommand() + { + var guildConfig = _database.GuildConfigs.FirstOrDefault(g => g.GuildId == Context.Guild.Id); + if (guildConfig == null) + throw new Exception("No guild config was foumd."); + if(guildConfig.Prefix != null) + await ReplyAsync($"Current prefix is `{guildConfig.Prefix}`"); + else + await ReplyAsync($"There is no prefix set for this guild."); + } + + [RequireUserPermission(GuildPermission.Administrator, Group = "Permission")] + [RequireOwner(Group = "Permission")] + [RequireContext(ContextType.Guild)] + [Command("prefix")] + [Summary("Sets prefix.")] + public async Task PrefixCommand( + [Summary("New prefix to set")] string newPrefix) + { + var guildConfig = _database.GuildConfigs.FirstOrDefault(g => g.GuildId == Context.Guild.Id); + if (guildConfig == null) + throw new Exception("No guild config was foumd."); + guildConfig.Prefix = newPrefix; + await _database.SaveChangesAsync(); + await ReplyAsync($"Prefix has been set to `{guildConfig.Prefix}`"); + } + + [Group("help")] + public class HelpModule : ModuleBase + { + + private readonly CommandService _commands; + public HelpModule(CommandService commands) + { + _commands = commands; + } + + [Command] + [Summary("Lists all commands with there descriptions.")] + public async Task DefaultHelpAsync() + { + var embed = new EmbedBuilder() + .WithTitle("Command list") + .WithColor(Color.DarkBlue) + .WithCurrentTimestamp() + .WithFields(_commands.Commands + .Where(c => c.Attributes.Where(a => a is NoHelpAttribute || (a is RequireContextAttribute requireContext)).Count() == 0) + .Select(c => + new EmbedFieldBuilder() + { + Name = $"`{string.Join(", ", c.Aliases)} {string.Join(" ", c.Parameters.Select(p => p.IsOptional ? $"[{p.Name}]" : $"<{p.Name}>"))}`", + Value = c.Summary + }) + ); + await Context.User.SendMessageAsync(embed: embed.Build()); + } + } + } +} diff --git a/EventBot/Modules/EventModule.cs b/EventBot/Modules/EventModule.cs new file mode 100644 index 0000000..95d38c4 --- /dev/null +++ b/EventBot/Modules/EventModule.cs @@ -0,0 +1,413 @@ +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 NeoSmart.Unicode; + +namespace EventBot.Modules +{ + [RequireContext(ContextType.Guild)] + public class EventModule : ModuleBase + { + private readonly EventManagementService _events; + private readonly DatabaseService _database; + public EventModule(EventManagementService events, DatabaseService database) + { + _events = events; + _database = database; + } + + [Command("join")] + [Summary("Joins latest or specified event with specified event role.")] + public async Task JoinAsync( + [Summary("Role emote or role id to join.")] string emoteOrId, + [Summary("Extra information that migth be needed by organizers.")] string extraInformation = null, + [Summary("Optional event ID for joining event that is not most recent one.")] Event @event = null) + { + EventRole er; + if (!(Context.User is SocketGuildUser guildUser)) + throw new Exception("This command must be executed inside guild."); + 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 guild."); + 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 can't join not opened event."); + + await _events.TryJoinEvent(guildUser, er, extraInformation); + await Context.Message.DeleteAsync(); // Protect somewhat sensitive data. + } + + [RequireUserPermission(GuildPermission.Administrator, Group = "Permission")] + [RequireOwner(Group = "Permission")] + [Group("event")] + public class EventManagementModule : ModuleBase + { + private readonly EventManagementService _events; + private readonly DatabaseService _database; + private readonly EmoteService _emotes; + public EventManagementModule(EventManagementService events, DatabaseService database, EmoteService emotes) + { + _events = events; + _database = database; + _emotes = emotes; + } + + [Command("config logchannel")] + [Summary("Sets logging channel for role changes.")] + public async Task SetRoleChannelAsync( + [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 guild."); + + guild.EventRoleConfirmationChannelId = channel.Id; + var s = _database.SaveChangesAsync(); + await ReplyAsync($"Event role changes now will be logged to `{channel.Name}` channel."); + await s; + } + + [Command("config partrole")] + [Summary("Sets role to assign when they selelct role.")] + public async Task SetParticipationRole( + [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 guild."); + + guild.ParticipantRoleId = role.Id; + var s = _database.SaveChangesAsync(); + await ReplyAsync($"Event participants will be given `{role.Name}` role."); + await s; + } + + [Command("new")] + [Summary("Creates new event.")] + public async Task NewEvent( + [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 guild."); + 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 of `{description}`. It's ID is `{@event.Id}`."); + } + + [Command("update title")] + [Summary("Updates event 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 guild."); + @event.Title = title; + await _database.SaveChangesAsync(); + await ReplyAsync($"Updated event(`{@event.Id}`) title to `{@event.Title}`"); + await _events.UpdateEventMessage(@event); + } + [Command("update description")] + [Summary("Updates event 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 guild."); + @event.Description = description; + await _database.SaveChangesAsync(); + await ReplyAsync($"Updated event(`{@event.Id}`) description to `{@event.Description}`"); + await _events.UpdateEventMessage(@event); + } + + [Command("update type")] + [Summary("Updates 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 guild."); + if (@event.MessageId != 0 && @event.Type != type) + throw new Exception("Can't change event registration type when it's open for registration. Maube 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}`"); + } + + + [Command("role new")] + [Summary("Adds new role to the event.")] + public async Task NewEventRole( + [Summary("Title for the role.")] string title, + [Summary("Description for 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("Event to that role is meant for.")] Event @event = null) + { + if (@event == null) + @event = _events.FindEventBy(Context.Guild); + if (@event == null) + throw new Exception("Unable to locate any events for this guild."); + 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 reigstration."); + if (!_emotes.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 + }; + _database.Add(er); + await _database.SaveChangesAsync(); + await ReplyAsync($"Added event role `{er.Id}` for event `{er.Event.Id}`, title: `{er.Title}`, description: `{er.Description}`, maxPart: `{er.MaxParticipants}`, emote: {er.Emote}"); + } + + [Command("role update title")] + [Summary("Updates role's title")] + public async Task UpdateEventRoleTitle( + [Summary("Role witch to update.")] EventRole eventRole, + [Summary("New title for role.")][Remainder] string title) + { + if(eventRole == null) + throw new Exception("Please provide correct role."); + eventRole.Title = title; + var s = _database.SaveChangesAsync(); + await ReplyAsync($"Updated event role `{eventRole.Id}` title to `{eventRole.Title}`"); + await s; + await _events.UpdateEventMessage(eventRole.Event); + } + + [Command("role update desc")] + [Summary("Updates role's description.")] + public async Task UpdateEventRoleDescription( + [Summary("Role witch to update.")] EventRole eventRole, + [Summary("New description for role.")][Remainder] string description) + { + if (eventRole == null) + throw new Exception("Please provide correct role."); + eventRole.Description = description; + var s = _database.SaveChangesAsync(); + await ReplyAsync($"Updated event role `{eventRole.Id}` description to `{eventRole.Description}`"); + await s; + await _events.UpdateEventMessage(eventRole.Event); + } + + [Command("role update slots")] + [Summary("Updates role's maximum participants count.")] + public async Task UpdateEventRoleMaxParticipants( + [Summary("Role witch to update.")] EventRole eventRole, + [Summary("New maximum participant count for role.")] int maxParticipants) + { + if (eventRole == null) + throw new Exception("Please provide correct role."); + eventRole.MaxParticipants = maxParticipants; + var s = _database.SaveChangesAsync(); + await ReplyAsync($"Updated event role `{eventRole.Id}` maximum participant count to `{eventRole.MaxParticipants}`"); + await s; + await _events.UpdateEventMessage(eventRole.Event); + } + + + [Command("role update emote")] + [Summary("Updates role's emote.")] + public async Task UpdateEventRoleEmote( + [Summary("Role witch to update.")] EventRole eventRole, + [Summary("New emote for the role.")] string emote) + { + if (eventRole == null) + throw new Exception("Please provide correct role."); + if (!_emotes.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()] + [Summary("Get info about event.")] + public async Task EventInfo( + [Summary("Event about witch info is wanted.")] Event @event = null) + { + if (@event == null) + @event = _events.FindEventBy(Context.Guild); + if (@event == null) + throw new Exception("No events were found for this guild."); + 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(1)] + [Command("role")] + [Summary("Gets role info.")] + public async Task EventRoleInfo( + [Summary("Role about witch info is wanted.")] EventRole eventRole) + { + if (eventRole == null) + throw new Exception("Please provide correct role."); + 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()); + } + } + + [Command("open")] + [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 guild."); + + 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 => _emotes.Parse(r.Emote)).ToArray()); + break; + case Event.EventParticipactionType.Detailed: + break; + default: + throw new Exception("Event type in not implemented."); + } + } + + [Command("participant add")] + [Summary("Add user to event role. Acts like join command.")] + public async Task EventParticipantAdd( + [Summary("User id or mention")] IUser user, + [Summary("Role emote or role id to join.")] string emoteOrId, + [Summary("Extra information that migth be needed by organizers.")] string extraInformation = null, + [Summary("Optional event ID for joining event that is not most recent one.")] Event @event = null) + { + EventRole er; + if (!(user is SocketGuildUser guildUser)) + throw new Exception("This command must be executed inside guild."); + 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 guild."); + 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"); + + await _events.TryJoinEvent(guildUser, er, extraInformation, false); + await Context.Message.DeleteAsync(); // Protect somewhat sensitive data. + } + + [Command("participant remove")] + [Summary("Remove participant from event role.")] + public async Task EventParticipantRemove( + [Summary("User that is participanting id or mention")] IUser user, + [Summary("Event to romove 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 guild."); + + if (!(user is IGuildUser guildUser)) + throw new Exception("This command must be executed inside guild."); + + var participant = @event.Participants.FirstOrDefault(p => p.UserId == guildUser.Id); + _database.Remove(participant); + var embed = new EmbedBuilder() + .WithTitle($"{user} been removed from event `{@event.Title}`, by {Context.User}") + .WithDescription($"They were in `{participant.Role.Title}` role") + .WithColor(Color.Red); + if (participant.UserData != null) + embed.AddField("Provided details", $"`{participant.UserData}`"); + + await _database.SaveChangesAsync(); + await _events.UpdateEventMessage(@event); + if (@event.Guild.EventRoleConfirmationChannelId != 0) + await (await ((IGuild)Context.Guild).GetTextChannelAsync(@event.Guild.EventRoleConfirmationChannelId)).SendMessageAsync(embed: embed.Build()); + if (@event.Guild.ParticipantRoleId != 0) + await guildUser.RemoveRoleAsync(Context.Guild.GetRole(@event.Guild.ParticipantRoleId)); + } + } + } +} diff --git a/EventBot/Modules/TestingModule.cs b/EventBot/Modules/TestingModule.cs new file mode 100644 index 0000000..261cf9c --- /dev/null +++ b/EventBot/Modules/TestingModule.cs @@ -0,0 +1,18 @@ +using Discord.Commands; +using EventBot.Attributes; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace EventBot.Modules +{ + public class TestingModule: ModuleBase + { + [Command("ping")] + [Summary("Test if bot is working.")] + [NoHelp] + public Task SayAsync() + => ReplyAsync("Pong!"); + } +} diff --git a/EventBot/Program.cs b/EventBot/Program.cs new file mode 100644 index 0000000..9852bdc --- /dev/null +++ b/EventBot/Program.cs @@ -0,0 +1,72 @@ +using Discord; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading.Tasks; +using EventBot.Services; +using Discord.Commands; +using Discord.Addons.Interactive; +using Microsoft.EntityFrameworkCore; + +namespace EventBot +{ + class Program + { + static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + + public async Task MainAsync() + { + using (var services = ConfigureServices()) + { + var client = services.GetRequiredService(); + client.Log += LogAsync; + services.GetRequiredService().Log += LogAsync; + services.GetRequiredService().Log += LogAsync; + + services.GetRequiredService(); + // Tokens should be considered secret data and never hard-coded. + // We can read from the environment variable to avoid hardcoding. + + await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await client.StartAsync(); + + // Here we initialize the logic required to register our commands. + await services.GetRequiredService().InitializeAsync(); + + await Task.Delay(-1); + } + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + + return Task.CompletedTask; + } + + private ServiceProvider ConfigureServices() + { + return new ServiceCollection() + .AddSingleton(s => new DiscordSocketClient(new DiscordSocketConfig() { + LogLevel = LogSeverity.Debug, + MessageCacheSize = 1500 + })) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(sp => + { + if (Environment.GetEnvironmentVariable("dbconnection") != null) + return new MySqlDatabaseService(sp); + return new SqliteDatabaseService(sp); + }) + .AddSingleton() + .AddSingleton() + //.AddSingleton() + //.AddSingleton() + .BuildServiceProvider(); + } + } +} diff --git a/EventBot/Properties/PublishProfiles/FolderProfile-linux.pubxml b/EventBot/Properties/PublishProfiles/FolderProfile-linux.pubxml new file mode 100644 index 0000000..ff3ceaf --- /dev/null +++ b/EventBot/Properties/PublishProfiles/FolderProfile-linux.pubxml @@ -0,0 +1,16 @@ + + + + + FileSystem + Release + Any CPU + netcoreapp2.2 + bin\Release\netcoreapp2.2\publish\linux + linux-x64 + true + <_IsPortable>false + + \ No newline at end of file diff --git a/EventBot/Properties/PublishProfiles/FolderProfile-win-x86.pubxml b/EventBot/Properties/PublishProfiles/FolderProfile-win-x86.pubxml new file mode 100644 index 0000000..41800c5 --- /dev/null +++ b/EventBot/Properties/PublishProfiles/FolderProfile-win-x86.pubxml @@ -0,0 +1,16 @@ + + + + + FileSystem + Release + Any CPU + netcoreapp2.2 + bin\Release\netcoreapp2.2\publish\win-x86 + win-x86 + true + <_IsPortable>false + + \ No newline at end of file diff --git a/EventBot/Properties/PublishProfiles/FolderProfile-win.pubxml b/EventBot/Properties/PublishProfiles/FolderProfile-win.pubxml new file mode 100644 index 0000000..b22b8b7 --- /dev/null +++ b/EventBot/Properties/PublishProfiles/FolderProfile-win.pubxml @@ -0,0 +1,16 @@ + + + + + FileSystem + Release + Any CPU + netcoreapp2.2 + bin\Release\netcoreapp2.2\publish\win + win-x64 + true + <_IsPortable>false + + \ No newline at end of file diff --git a/EventBot/Properties/launchSettings.json b/EventBot/Properties/launchSettings.json new file mode 100644 index 0000000..9c63605 --- /dev/null +++ b/EventBot/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "EventBot": { + "commandName": "Project", + "environmentVariables": { + "token": "" + } + } + } +} \ No newline at end of file diff --git a/EventBot/Services/CommandHandlingService.cs b/EventBot/Services/CommandHandlingService.cs new file mode 100644 index 0000000..7bee32e --- /dev/null +++ b/EventBot/Services/CommandHandlingService.cs @@ -0,0 +1,94 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using EventBot.Entities; +using EventBot.Misc; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Linq; +using System.Threading.Tasks; + +namespace EventBot.Services +{ + public class CommandHandlingService + { + private readonly CommandService _commands; + private readonly DiscordSocketClient _discord; + private readonly DatabaseService _database; + private readonly IServiceProvider _services; + + public event Func Log; + + public CommandHandlingService(IServiceProvider services) + { + _commands = services.GetRequiredService(); + _discord = services.GetRequiredService(); + _database = services.GetRequiredService(); + _services = services; + + // Hook CommandExecuted to handle post-command-execution logic. + _commands.CommandExecuted += CommandExecutedAsync; + // Hook MessageReceived so we can process each message to see + // if it qualifies as a command. + _discord.MessageReceived += MessageReceivedAsync; + } + + public async Task InitializeAsync() + { + _commands.AddTypeReader(new EventTypeReader()); + _commands.AddTypeReader(new EventRoleTypeReader()); + // Register modules that are public and inherit ModuleBase. + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + } + + public async Task MessageReceivedAsync(SocketMessage rawMessage) + { + // Ignore system messages, or messages from other bots + if (!(rawMessage is SocketUserMessage message)) return; + if (message.Source != MessageSource.User) return; + + // This value holds the offset where the prefix ends + var argPos = 0; + // Perform prefix check. You may want to replace this with + // (!message.HasCharPrefix('!', ref argPos)) + // for a more traditional command format like !help. + var context = new SocketCommandContext(_discord, message); + GuildConfig guildConfig = context.Guild != null ? _database.GuildConfigs.FirstOrDefault(g => g.GuildId == context.Guild.Id) : null; + if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) + if (guildConfig != null) + { + if (!message.HasStringPrefix(guildConfig.Prefix, ref argPos)) + return; + } else + { + return; + } + + await Log?.Invoke(new LogMessage(LogSeverity.Debug, "CommandHandlingService", $"Got potential command: {message.Content}")); + // Perform the execution of the command. In this method, + // the command service will perform precondition and parsing check + // then execute the command if one is matched. + + await _commands.ExecuteAsync(context, argPos, _services); + // Note that normally a result will be returned by this format, but here + // we will handle the result in CommandExecutedAsync, + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + // command is unspecified when there was a search failure (command not found); we don't care about these errors + if (!command.IsSpecified) + return; + + // the command was successful, we don't care about this result, unless we want to log that a command succeeded. + if (result.IsSuccess) + return; + + // the command failed, let's notify the user that something happened. + await context.Channel.SendMessageAsync($"error: {result}"); + } + } +} diff --git a/EventBot/Services/DatabaseService.cs b/EventBot/Services/DatabaseService.cs new file mode 100644 index 0000000..233ae7d --- /dev/null +++ b/EventBot/Services/DatabaseService.cs @@ -0,0 +1,74 @@ +using Discord.WebSocket; +using EventBot.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Linq; + +namespace EventBot.Services +{ + public abstract class DatabaseService: DbContext + { + private readonly IServiceProvider _services; + private readonly DiscordSocketClient _discord; + + + public DbSet GuildConfigs { get; set; } + public DbSet Events { get; set; } + public DbSet EventRoles { get; set; } + public DbSet EventParticipants { get; set; } + + public DatabaseService(IServiceProvider services, DbContextOptions options) : base(options) + { + _services = services; + + _discord = services.GetRequiredService(); + _discord.GuildAvailable += OnGuildAvaivable; + } + public DatabaseService(IServiceProvider services) : base() + { + _services = services; + + _discord = services.GetRequiredService(); + _discord.GuildAvailable += OnGuildAvaivable; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseLazyLoadingProxies(); +#if DEBUG + optionsBuilder.UseSqlite("Data Source=blogging.db"); +#else + optionsBuilder.UseMySql(Environment.GetEnvironmentVariable("dbconnection")); +#endif + } + + + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(e => e.Type) + .HasConversion(new EnumToNumberConverter()); + } + + protected async Task OnGuildAvaivable(SocketGuild guild) + { + GuildConfig config = default; + if(await GuildConfigs.CountAsync() != 0) + config = await GuildConfigs.FirstAsync(g => g.GuildId == guild.Id); + if(config == null) + { + config = new GuildConfig() + { + GuildId = guild.Id + }; + Add(config); + await SaveChangesAsync(); + } + } + } +} diff --git a/EventBot/Services/EmoteService.cs b/EventBot/Services/EmoteService.cs new file mode 100644 index 0000000..fe39c07 --- /dev/null +++ b/EventBot/Services/EmoteService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NeoSmart.Unicode; +using System.Linq; +using Discord; +using DEmoji = Discord.Emoji; +using UEmoji = NeoSmart.Unicode.Emoji; + +namespace EventBot.Services +{ + + public class EmoteService + { + private IEnumerable emoji; + + public EmoteService() + { + emoji = UEmoji.All.Select(e => e.Sequence.AsString); + } + + public bool TryParse(string input, out IEmote emote) + { + if(Emote.TryParse(input, out Emote parsedEmote)) + { + emote = parsedEmote; + return true; + } + if(emoji.Contains(input)) + { + emote = new DEmoji(input); + return true; + } + emote = null; + return false; + } + + public IEmote Parse(string input) + { + if (!TryParse(input, out IEmote parsed)) + throw new ArgumentException("Failed to parse emote."); + return parsed; + } + + } +} \ No newline at end of file diff --git a/EventBot/Services/EventManagementService.cs b/EventBot/Services/EventManagementService.cs new file mode 100644 index 0000000..811f611 --- /dev/null +++ b/EventBot/Services/EventManagementService.cs @@ -0,0 +1,152 @@ +using Discord; +using Discord.WebSocket; +using EventBot.Entities; +using EventBot.Misc; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Linq; + +namespace EventBot.Services +{ + public class EventManagementService + { + private readonly DiscordSocketClient _discord; + private readonly DatabaseService _database; + private readonly EmoteService _emotes; + private readonly IServiceProvider _services; + + public EventManagementService(IServiceProvider services) + { + _discord = services.GetRequiredService(); + _database = services.GetRequiredService(); + _emotes = services.GetRequiredService(); + _services = services; + + _discord.ReactionAdded += ReactionAddedAsync; + _discord.MessageDeleted += MessageDeletedAsync; + } + + public async Task TryJoinEvent(IGuildUser user, EventRole er, string extra, bool extraChecks = true) + { + if (er.Event.GuildId != user.GuildId) + throw new Exception("Cross guild events are fobidden."); + if (extraChecks && er.ReamainingOpenings <= 0) + throw new Exception("No openings left."); + if(er.Event.Participants.Where(p => p.UserId == user.Id).Count() > 0) + throw new Exception("You are already participating."); + if(extraChecks && !er.Event.Active) + throw new Exception("Event is closed."); + + if (er.Event.Guild.ParticipantRoleId != 0) + await user.AddRoleAsync(user.Guild.GetRole(er.Event.Guild.ParticipantRoleId)); + + var ep = new EventParticipant() + { + UserId = user.Id, + Event = er.Event, + Role = er + }; + var embed = new EmbedBuilder() + .WithTitle($"{user} has joined event `{er.Event.Title}`") + .WithDescription($"They have chosen `{er.Title}` role.") + .WithColor(Color.Green); + if (extra != null && extra != string.Empty) + { + embed.AddField("Provided details", $"`{extra}`"); + ep.UserData = extra; + } + _database.Add(ep); + await _database.SaveChangesAsync(); + await UpdateEventMessage(er.Event); + if (er.Event.Guild.EventRoleConfirmationChannelId != 0) + await (await user.Guild.GetTextChannelAsync(er.Event.Guild.EventRoleConfirmationChannelId)).SendMessageAsync(embed: embed.Build()); + } + + public Event FindEventBy(IGuild guild, bool bypassActive = false) + { + return _database.Events.OrderByDescending(e => e.Opened).FirstOrDefault(e => e.GuildId == guild.Id && (e.Active || bypassActive)); + } + + public Event FindEventBy(IGuild guild, int? eventId, bool bypassActive = false) + { + if (eventId == null) + return FindEventBy(guild, bypassActive); + return _database.Events.OrderByDescending(e => e.Opened).FirstOrDefault(e => e.GuildId == guild.Id && e.Id == eventId && (e.Active || bypassActive)); + } + + public async Task UpdateEventMessage(Event ev) + { + if (ev.MessageChannelId == 0 || ev.MessageId == 0) + return; + var channel = (ITextChannel) _discord.GetChannel(ev.MessageChannelId); + var message = (IUserMessage) await channel.GetMessageAsync(ev.MessageId); + await message.ModifyAsync(m => m.Embed = GenerateEventEmbed(ev).Build()); + } + + public EmbedBuilder GenerateEventEmbed(Event @event) + { + var embed = new EmbedBuilder() + .WithTitle($"{@event.Title}") + .WithDescription(@event.Description) + .WithFooter($"EventId: {@event.Id}") + .WithColor(Color.Purple) + ; + if (@event.Type == Event.EventParticipactionType.Quick) + embed.Description += "\r\nTo participate in this event react with following emotes:"; + if (@event.Type == Event.EventParticipactionType.Detailed) + embed.Description += "\r\nTo participate in this event use command `join ` as following emotes are awaivable:"; + + embed.WithFields(@event.Roles + .OrderBy(e => e.SortNumber) + .Select(e => new EmbedFieldBuilder() + .WithName($"{e.Emote} `{e.Id}`: *{e.Title}*`{ (e.MaxParticipants > 0 ? $" - {e.ReamainingOpenings} ramaining" : "")} - {e.ParticipantCount} participating.`") + .WithValue($"{e.Description}") + )); + return embed; + } + + public async Task MessageDeletedAsync(Cacheable message, ISocketMessageChannel socketMessage) + { + var @event = _database.Events.FirstOrDefault(e => e.MessageId == message.Id); + if(@event != null) + { + @event.MessageId = 0; + @event.MessageChannelId = 0; + await _database.SaveChangesAsync(); + } + + } + + + public async Task ReactionAddedAsync(Cacheable message, ISocketMessageChannel socketMessage, SocketReaction reaction) + { + if (!reaction.User.IsSpecified || reaction.User.Value.IsBot) + return; + var @event = _database.Events.FirstOrDefault(e => e.MessageId == message.Id); + if (@event != null) + { + var role = @event.Roles.FirstOrDefault(r => reaction.Emote.Equals(_emotes.Parse(r.Emote))); + if(role != null) + { + var userMessage = await message.GetOrDownloadAsync(); + if (reaction.User.IsSpecified) + await userMessage.RemoveReactionAsync(reaction.Emote, reaction.User.Value); + try + { + if (!(reaction.User.GetValueOrDefault() is IGuildUser guildUser)) + throw new Exception("Reaction must be made inside guild"); + await TryJoinEvent(guildUser, role, null); + } + catch (Exception ex) + { + if (reaction.User.IsSpecified) + await reaction.User.Value.SendMessageAsync($"Error ocured while processing your reaction: \r\n{ex.GetType()}: {ex.Message}"); + } + } + } + } + } +} diff --git a/EventBot/Services/MySqlDatabaseService.cs b/EventBot/Services/MySqlDatabaseService.cs new file mode 100644 index 0000000..d0a31e9 --- /dev/null +++ b/EventBot/Services/MySqlDatabaseService.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EventBot.Services +{ + public class MySqlDatabaseService : DatabaseService + { + public MySqlDatabaseService(IServiceProvider services, DbContextOptions options) : base(services, options) { } + public MySqlDatabaseService(IServiceProvider services) : base(services) { } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseMySql(Environment.GetEnvironmentVariable("dbconnection")); + } + } +} diff --git a/EventBot/Services/SqliteDatabaseService.cs b/EventBot/Services/SqliteDatabaseService.cs new file mode 100644 index 0000000..021fbd4 --- /dev/null +++ b/EventBot/Services/SqliteDatabaseService.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EventBot.Services +{ + public class SqliteDatabaseService : DatabaseService + { + public SqliteDatabaseService(IServiceProvider services, DbContextOptions options) : base(services, options) { } + public SqliteDatabaseService(IServiceProvider services) : base(services) {} + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=data.db"); + } + } +} diff --git a/EventBot/data.db b/EventBot/data.db new file mode 100644 index 0000000000000000000000000000000000000000..457e04a7043c3be3f864aff8396a446445c58611 GIT binary patch literal 49152 zcmeI)%TD7~7{GCxOUz|bx~SwLwUV(k(nP~>>l8G*roqr?$fY4vpivchz))AgVPb=r zMHkGB+AjJIeS)t00Nr-gN9a>@(M69PJFydUnM$*m|<$3uB+c~l>i~9}Z!g4&fv3+hg zkBUc~SN94vBVRYPVtLcp*Ytr_x>hM`dZ(?fJdagO6s2dCNRW@<-TB~Wfoq@s; zf<3e8qa<3#Mhf=(qXpv@kM!KtN7IU0T9&V#B!;V+ZO=Zm&rE+_L1VvbICA{Wne+hS zP2&AyvBaBnL+Xqo!kU^=R6~T7yCtlYkf!e%l9+!l38~@uLT?@`in_ck|EzlE^QIMG z#2XLnmJJ6mQ2U>owUMZHvh$W+3Ukr~MOW5E5iAHG=L=E;_t9{%1o2F;#THV<1 z1Rr*dPhl%%3Y|%H%URd9DmA0HT@GdxD^9IuY#BA9TrhUyOU2KMpSVq9#}F&5kl!uj zH;uO%JGMZ=l1qyrs>Z=Mg8qKhrpJ<<$-Vw0qo@lD@{i-avI%;1ljABWg%Tb3Z7U?~ zcWY_XZLet!W=!9-+{3ng=GiSL22r}1l(e}2W6_E}?Bf6$t7ie16!^>!)|AUQQ-TqVLTIj(KV=ik`VwACD{Q z{rmE3H#lZIocQyJ#L+rY_M#3~eiZwFUH=6le&jn;T(xZ-+m}%~#RVryW%s-kgyP?L zigUpo>LYdn@KAU2~w=ij82Z?bgxxq4$l|c4KG9a3P4P6~{8_w;Qq)d{PN(!#wv|!Rtoq zIE~fR-Q(h;=9K?&Gbw)FUs_)+_X_-{_p?AAN{i-fB*srAb? z|3AdZWoif@fB*srAb