diff --git a/src/CB.Accessors/Contracts/IChannelAccessor.cs b/src/CB.Accessors/Contracts/IChannelAccessor.cs new file mode 100644 index 0000000..44a65ae --- /dev/null +++ b/src/CB.Accessors/Contracts/IChannelAccessor.cs @@ -0,0 +1,18 @@ +using CB.Data.Entities; +using CB.Shared.Dtos; + +namespace CB.Accessors.Contracts; + +public interface IChannelAccessor +{ + Task> GetAllAsync(); + + Task GetByIdAsync(string id); + + Task CreateAsync(Channel entity); + + Task UpdateAsync(string id, + Channel entity); + + Task DeleteAsync(string id); +} \ No newline at end of file diff --git a/src/CB.Accessors/Contracts/IChannelConfigurationAccessor.cs b/src/CB.Accessors/Contracts/IChannelConfigurationAccessor.cs new file mode 100644 index 0000000..9534bc2 --- /dev/null +++ b/src/CB.Accessors/Contracts/IChannelConfigurationAccessor.cs @@ -0,0 +1,18 @@ +using CB.Data.Entities; +using CB.Shared.Dtos; + +namespace CB.Accessors.Contracts; + +public interface IChannelConfigurationAccessor +{ + Task> GetAllAsync(); + + Task GetByIdAsync(string id); + + Task CreateAsync(ChannelConfiguration entity); + + Task UpdateAsync(string id, + ChannelConfigurationDto dto); + + Task DeleteAsync(string id); +} \ No newline at end of file diff --git a/src/CB.Accessors/Implementations/ChannelAccessor.cs b/src/CB.Accessors/Implementations/ChannelAccessor.cs new file mode 100644 index 0000000..40ffbbf --- /dev/null +++ b/src/CB.Accessors/Implementations/ChannelAccessor.cs @@ -0,0 +1,82 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using CB.Accessors.Contracts; +using CB.Data; +using CB.Data.Entities; +using CB.Shared.Dtos; +using Microsoft.EntityFrameworkCore; + +namespace CB.Accessors.Implementations; + +public class ChannelAccessor(CbContext context, + IMapper mapper) + : IChannelAccessor +{ + public Task> GetAllAsync() => context + .Channels + .AsNoTracking() + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(); + + public Task GetByIdAsync(string id) => context.Channels + .AsNoTracking() + .Where(g => g.Id == id) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + + public async Task CreateAsync(Channel entity) + { + entity.CreatedDate = DateTime.UtcNow; + entity.ModifiedDate = DateTime.UtcNow; + + context.Channels.Add(entity); + await context + .SaveChangesAsync() + .ConfigureAwait(false); + + return mapper.Map(entity); + } + + public async Task UpdateAsync(string id, + Channel updated) + { + var channel = await context + .Channels + .FindAsync(id) + .ConfigureAwait(false); + + if (channel == null) + { + return null; + } + + channel.DisplayName = updated.DisplayName; + channel.ModifiedDate = DateTime.UtcNow; + + await context + .SaveChangesAsync() + .ConfigureAwait(false); + + return mapper.Map(channel); + } + + public async Task DeleteAsync(string id) + { + var channel = await context + .Channels + .FindAsync(id) + .ConfigureAwait(false); + + if (channel == null) + { + return false; + } + + context.Channels.Remove(channel); + await context + .SaveChangesAsync() + .ConfigureAwait(false); + + return true; + } +} \ No newline at end of file diff --git a/src/CB.Accessors/Implementations/ChannelConfigurationAccessor.cs b/src/CB.Accessors/Implementations/ChannelConfigurationAccessor.cs new file mode 100644 index 0000000..7cefbc5 --- /dev/null +++ b/src/CB.Accessors/Implementations/ChannelConfigurationAccessor.cs @@ -0,0 +1,81 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using CB.Accessors.Contracts; +using CB.Data; +using CB.Data.Entities; +using CB.Shared.Dtos; +using Microsoft.EntityFrameworkCore; + +namespace CB.Accessors.Implementations; + +public class ChannelConfigurationAccessor(CbContext context, + IMapper mapper) + : IChannelConfigurationAccessor +{ + public Task> GetAllAsync() => context + .ChannelConfigurations + .AsNoTracking() + .ProjectTo(mapper.ConfigurationProvider) + .ToListAsync(); + + public Task GetByIdAsync(string id) => context.ChannelConfigurations + .AsNoTracking() + .Where(g => g.GuildId == id) + .ProjectTo(mapper.ConfigurationProvider) + .FirstOrDefaultAsync(); + + public async Task CreateAsync(ChannelConfiguration entity) + { + context.ChannelConfigurations.Add(entity); + await context + .SaveChangesAsync() + .ConfigureAwait(false); + + return mapper.Map(entity); + } + + public async Task UpdateAsync(string id, + ChannelConfigurationDto dto) + { + var channelConfiguration = await context + .ChannelConfigurations + .FindAsync(id) + .ConfigureAwait(false); + + if (channelConfiguration == null) + { + return null; + } + + channelConfiguration.GreetingChannelId = dto.GreetingChannelId; + channelConfiguration.GoodbyeChannelId = dto.GoodbyeChannelId; + channelConfiguration.LiveChannelId = dto.LiveChannelId; + channelConfiguration.DiscordLiveChannelId = dto.DiscordLiveChannelId; + + await context + .SaveChangesAsync() + .ConfigureAwait(false); + + return mapper.Map(channelConfiguration); + } + + public async Task DeleteAsync(string id) + { + var channelConfiguration = await context + .ChannelConfigurations + .FindAsync(id) + .ConfigureAwait(false); + + if (channelConfiguration == null) + { + return false; + } + + context.ChannelConfigurations.Remove(channelConfiguration); + await context + .SaveChangesAsync() + .ConfigureAwait(false); + + return true; + } +} \ No newline at end of file diff --git a/src/CB.Bot/Commands/Application/BaseSlashCommands.cs b/src/CB.Bot/Commands/Application/BaseSlashCommands.cs new file mode 100644 index 0000000..d2421f9 --- /dev/null +++ b/src/CB.Bot/Commands/Application/BaseSlashCommands.cs @@ -0,0 +1,36 @@ +using Discord; +using Discord.Interactions; +using Discord.WebSocket; + +namespace CB.Bot.Commands.Application; + +/// +/// Base class to provide easier access to helper functions for our slash command implementations. +/// +public class BaseSlashCommands() : InteractionModuleBase +{ + public IGuildUser GuildUser => (IGuildUser)Context.User; + public IGuildChannel GuildChannel => (IGuildChannel)Context.Channel; + public SocketInteraction SocketInteraction => (SocketInteraction)Context.Interaction; + public SocketSlashCommand SocketSlashCommand => (SocketSlashCommand)SocketInteraction; + + /// + /// Validate if the user executing the slash command is an approved admin, or not. + /// + /// Is user admin? true or false + public async Task IsUserAdmin(bool sendResponse = true) + { + // TODO MS - We need to add ApprovedAdmin code. + // This'll do for now. + var isAdmin = GuildUser.GuildPermissions.ManageGuild; + + if (!isAdmin && sendResponse) + { + await SocketInteraction.FollowupAsync("Sorry, you have to be an admin to use that command.", + ephemeral: true) + .ConfigureAwait(false); + } + + return isAdmin; + } +} \ No newline at end of file diff --git a/src/CB.Bot/Commands/Application/ChannelSlashCommands.cs b/src/CB.Bot/Commands/Application/ChannelSlashCommands.cs new file mode 100644 index 0000000..16da01a --- /dev/null +++ b/src/CB.Bot/Commands/Application/ChannelSlashCommands.cs @@ -0,0 +1,116 @@ +using CB.Accessors.Contracts; +using CB.Shared.Enums; +using Discord; +using Discord.Interactions; +// ReSharper disable UnusedMember.Local +// ReSharper disable UnusedMember.Global + +namespace CB.Bot.Commands.Application; + +[Group("channel", "Channel configuration")] +public class ChannelSlashCommands(IGuildAccessor guildAccessor, + IChannelAccessor channelAccessor, + IChannelConfigurationAccessor channelConfigurationAccessor) : BaseSlashCommands +{ + [SlashCommand( + "live", + "Configure server 'Channel' settings", + false, + RunMode.Async)] + private async Task LiveChannelConfigurationAsync(IGuildChannel channel) + { + await ConfigureChannelAsync(ConfiguredChannelType.Live, + channel); + } + + [SlashCommand( + "greetings", + "Configure server 'Channel' settings", + false, + RunMode.Async)] + private async Task GreetingChannelConfigurationAsync(IGuildChannel channel) + { + await ConfigureChannelAsync(ConfiguredChannelType.Greetings, + channel); + } + + [SlashCommand( + "goodbyes", + "Configure server 'Channel' settings", + false, + RunMode.Async)] + private async Task GoodbyeChannelConfigurationAsync(IGuildChannel channel) + { + await ConfigureChannelAsync(ConfiguredChannelType.Goodbyes, + channel); + } + + [SlashCommand( + "discordlive", + "Configure server 'Discord Live Channel' setting", + false, + RunMode.Async)] + private async Task DiscordLiveChannelConfigurationAsync(IGuildChannel channel) + { + await ConfigureChannelAsync(ConfiguredChannelType.DiscordLive, + channel); + } + + private async Task ConfigureChannelAsync(ConfiguredChannelType configuredChannelType, + IGuildChannel discordChannel) + { + await SocketInteraction + .DeferAsync(true) + .ConfigureAwait(false); + + if (!await IsUserAdmin()) + { + return; + } + + var guild = await guildAccessor + .GetByIdAsync(Context.Guild.Id.ToString()) + .ConfigureAwait(false); + + if (guild == null) + { + await FollowupAsync($"There was an issue setting your '{configuredChannelType}' channel. Contact support.", ephemeral: true) + .ConfigureAwait(false); + return; + } + + var existingChannel = await channelAccessor.GetByIdAsync(discordChannel.Id.ToString()).ConfigureAwait(false) + ?? await channelAccessor.CreateAsync(new() + { + CreatedDate = DateTime.UtcNow, + ModifiedDate = DateTime.UtcNow, + DisplayName = discordChannel.Name, + GuildId = guild.Id, + Id = discordChannel.Id.ToString() + }).ConfigureAwait(false); + + switch (configuredChannelType) + { + case ConfiguredChannelType.Greetings: + guild.ChannelConfiguration.GreetingChannelId = existingChannel.Id; + break; + case ConfiguredChannelType.Goodbyes: + guild.ChannelConfiguration.GoodbyeChannelId = existingChannel.Id; + break; + case ConfiguredChannelType.Live: + guild.ChannelConfiguration.LiveChannelId = existingChannel.Id; + break; + case ConfiguredChannelType.DiscordLive: + guild.ChannelConfiguration.DiscordLiveChannelId = existingChannel.Id; + break; + } + + await channelConfigurationAccessor.UpdateAsync(guild.ChannelConfiguration.GuildId, + guild.ChannelConfiguration) + .ConfigureAwait(false); + + await FollowupAsync($"Your '{configuredChannelType}' channel is now #{existingChannel.DisplayName}", + ephemeral: true) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/CB.Bot/Program.cs b/src/CB.Bot/Program.cs index 2153635..a5b16fc 100644 --- a/src/CB.Bot/Program.cs +++ b/src/CB.Bot/Program.cs @@ -33,6 +33,9 @@ builder.Services.AddSingleton(new InteractionService(client)); builder.Services.AddSingleton(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddDbContext(options => diff --git a/src/CB.Data/CbContext.cs b/src/CB.Data/CbContext.cs index 71c55ca..e317b9a 100644 --- a/src/CB.Data/CbContext.cs +++ b/src/CB.Data/CbContext.cs @@ -6,6 +6,7 @@ namespace CB.Data; public class CbContext(DbContextOptions options) : DbContext(options) { public DbSet Channels => Set(); + public DbSet ChannelConfigurations => Set(); public DbSet Creators => Set(); public DbSet Guilds => Set(); public DbSet Users => Set(); diff --git a/src/CB.Data/Migrations/20250716021538_7152025-906p.Designer.cs b/src/CB.Data/Migrations/20250716021538_7152025-906p.Designer.cs new file mode 100644 index 0000000..a679f64 --- /dev/null +++ b/src/CB.Data/Migrations/20250716021538_7152025-906p.Designer.cs @@ -0,0 +1,398 @@ +// +using System; +using CB.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CB.Data.Migrations +{ + [DbContext(typeof(CbContext))] + [Migration("20250716021538_7152025-906p")] + partial class _7152025906p + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CB.Data.Entities.AllowConfiguration", b => + { + b.Property("GuildId") + .HasColumnType("text"); + + b.Property("AllowCrosspost") + .HasColumnType("boolean"); + + b.Property("AllowDiscordLive") + .HasColumnType("boolean"); + + b.Property("AllowFfa") + .HasColumnType("boolean"); + + b.Property("AllowGoodbyes") + .HasColumnType("boolean"); + + b.Property("AllowGreetings") + .HasColumnType("boolean"); + + b.Property("AllowLive") + .HasColumnType("boolean"); + + b.Property("AllowLiveDiscovery") + .HasColumnType("boolean"); + + b.Property("AllowPublished") + .HasColumnType("boolean"); + + b.Property("AllowStreamVod") + .HasColumnType("boolean"); + + b.Property("AllowThumbnails") + .HasColumnType("boolean"); + + b.HasKey("GuildId"); + + b.ToTable("AllowConfiguration"); + }); + + modelBuilder.Entity("CB.Data.Entities.Channel", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("GuildId") + .HasColumnType("text"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("Channels", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.ChannelConfiguration", b => + { + b.Property("GuildId") + .HasColumnType("text"); + + b.Property("DiscordLiveChannelId") + .HasColumnType("text"); + + b.Property("GoodbyeChannelId") + .HasColumnType("text"); + + b.Property("GreetingChannelId") + .HasColumnType("text"); + + b.Property("LiveChannelId") + .HasColumnType("text"); + + b.HasKey("GuildId"); + + b.HasIndex("DiscordLiveChannelId"); + + b.HasIndex("GoodbyeChannelId"); + + b.HasIndex("GreetingChannelId"); + + b.HasIndex("LiveChannelId"); + + b.ToTable("ChannelConfigurations", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.Creator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("text"); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("IsLive") + .HasColumnType("boolean"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("PlatformId") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Creators", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.Guild", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Guilds", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.GuildConfiguration", b => + { + b.Property("GuildId") + .HasColumnType("text"); + + b.Property("DeleteOffline") + .HasColumnType("boolean"); + + b.Property("TextAnnouncements") + .HasColumnType("boolean"); + + b.HasKey("GuildId"); + + b.ToTable("GuildConfigurations", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.MessageConfiguration", b => + { + b.Property("GuildId") + .HasColumnType("text"); + + b.Property("GoodbyeMessage") + .HasColumnType("text"); + + b.Property("GreetingMessage") + .HasColumnType("text"); + + b.Property("LiveMessage") + .HasColumnType("text"); + + b.Property("PublishedMessage") + .HasColumnType("text"); + + b.Property("StreamOfflineMessage") + .HasColumnType("text"); + + b.HasKey("GuildId"); + + b.ToTable("MessageConfigurations", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.RoleConfiguration", b => + { + b.Property("GuildId") + .HasColumnType("text"); + + b.Property("DiscoveryRoleId") + .HasColumnType("text"); + + b.Property("JoinRoleId") + .HasColumnType("text"); + + b.Property("LiveDiscoveryRoleId") + .HasColumnType("text"); + + b.HasKey("GuildId"); + + b.ToTable("RoleConfigurations", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("ModifiedDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("CB.Data.Entities.AllowConfiguration", b => + { + b.HasOne("CB.Data.Entities.Guild", "Guild") + .WithOne("AllowConfiguration") + .HasForeignKey("CB.Data.Entities.AllowConfiguration", "GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("CB.Data.Entities.Channel", b => + { + b.HasOne("CB.Data.Entities.Guild", "Guild") + .WithMany("Channels") + .HasForeignKey("GuildId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("CB.Data.Entities.ChannelConfiguration", b => + { + b.HasOne("CB.Data.Entities.Channel", "DiscordLiveChannel") + .WithMany() + .HasForeignKey("DiscordLiveChannelId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CB.Data.Entities.Channel", "GoodbyeChannel") + .WithMany() + .HasForeignKey("GoodbyeChannelId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CB.Data.Entities.Channel", "GreetingChannel") + .WithMany() + .HasForeignKey("GreetingChannelId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CB.Data.Entities.Guild", "Guild") + .WithOne("ChannelConfiguration") + .HasForeignKey("CB.Data.Entities.ChannelConfiguration", "GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CB.Data.Entities.Channel", "LiveChannel") + .WithMany() + .HasForeignKey("LiveChannelId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DiscordLiveChannel"); + + b.Navigation("GoodbyeChannel"); + + b.Navigation("GreetingChannel"); + + b.Navigation("Guild"); + + b.Navigation("LiveChannel"); + }); + + modelBuilder.Entity("CB.Data.Entities.Creator", b => + { + b.HasOne("CB.Data.Entities.User", "User") + .WithMany("Creators") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CB.Data.Entities.Guild", b => + { + b.HasOne("CB.Data.Entities.User", "Owner") + .WithMany("Guilds") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("CB.Data.Entities.GuildConfiguration", b => + { + b.HasOne("CB.Data.Entities.Guild", "Guild") + .WithOne("GuildConfiguration") + .HasForeignKey("CB.Data.Entities.GuildConfiguration", "GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("CB.Data.Entities.MessageConfiguration", b => + { + b.HasOne("CB.Data.Entities.Guild", "Guild") + .WithOne("MessageConfiguration") + .HasForeignKey("CB.Data.Entities.MessageConfiguration", "GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("CB.Data.Entities.RoleConfiguration", b => + { + b.HasOne("CB.Data.Entities.Guild", "Guild") + .WithOne("RoleConfiguration") + .HasForeignKey("CB.Data.Entities.RoleConfiguration", "GuildId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Guild"); + }); + + modelBuilder.Entity("CB.Data.Entities.Guild", b => + { + b.Navigation("AllowConfiguration"); + + b.Navigation("ChannelConfiguration"); + + b.Navigation("Channels"); + + b.Navigation("GuildConfiguration"); + + b.Navigation("MessageConfiguration"); + + b.Navigation("RoleConfiguration"); + }); + + modelBuilder.Entity("CB.Data.Entities.User", b => + { + b.Navigation("Creators"); + + b.Navigation("Guilds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/CB.Data/Migrations/20250716021538_7152025-906p.cs b/src/CB.Data/Migrations/20250716021538_7152025-906p.cs new file mode 100644 index 0000000..f078211 --- /dev/null +++ b/src/CB.Data/Migrations/20250716021538_7152025-906p.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CB.Data.Migrations +{ + /// + public partial class _7152025906p : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/CB.Shared/CB.Shared.csproj b/src/CB.Shared/CB.Shared.csproj index b39432d..fc32966 100644 --- a/src/CB.Shared/CB.Shared.csproj +++ b/src/CB.Shared/CB.Shared.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/src/CB.Shared/Enums/ConfiguredChannelType.cs b/src/CB.Shared/Enums/ConfiguredChannelType.cs new file mode 100644 index 0000000..2b25df1 --- /dev/null +++ b/src/CB.Shared/Enums/ConfiguredChannelType.cs @@ -0,0 +1,9 @@ +namespace CB.Shared.Enums; + +public enum ConfiguredChannelType +{ + Greetings, + Goodbyes, + Live, + DiscordLive, +} \ No newline at end of file