diff --git a/Directory.Build.targets b/Directory.Build.targets index e4ba440e3..199826c6c 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -23,8 +23,7 @@ - - + diff --git a/Modix.sln b/Modix.sln index e9b8da100..615ec3d06 100644 --- a/Modix.sln +++ b/Modix.sln @@ -64,6 +64,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modix.Web", "src\Modix.Web\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E74ADFBD-55F3-4E28-885B-2270670294EF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modix.Web.Wasm", "src\Modix.Web.Wasm\Modix.Web.Wasm.csproj", "{F0DFE404-B6A9-495C-A41C-92FD25138619}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modix.Web.Shared", "src\Modix.Web.Shared\Modix.Web.Shared.csproj", "{A603AE22-F588-466E-A7B1-298989E76890}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modix.Models", "src\Modix.Models\Modix.Models.csproj", "{A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -218,17 +224,53 @@ Global {2280A9D0-358E-4668-8855-6832725C740A}.Release|x64.Build.0 = Release|Any CPU {2280A9D0-358E-4668-8855-6832725C740A}.Release|x86.ActiveCfg = Release|Any CPU {2280A9D0-358E-4668-8855-6832725C740A}.Release|x86.Build.0 = Release|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Debug|x64.Build.0 = Debug|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Debug|x86.Build.0 = Debug|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Release|Any CPU.Build.0 = Release|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Release|x64.ActiveCfg = Release|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Release|x64.Build.0 = Release|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Release|x86.ActiveCfg = Release|Any CPU + {F0DFE404-B6A9-495C-A41C-92FD25138619}.Release|x86.Build.0 = Release|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Debug|x64.Build.0 = Debug|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Debug|x86.Build.0 = Debug|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Release|Any CPU.Build.0 = Release|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Release|x64.ActiveCfg = Release|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Release|x64.Build.0 = Release|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Release|x86.ActiveCfg = Release|Any CPU + {A603AE22-F588-466E-A7B1-298989E76890}.Release|x86.Build.0 = Release|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Debug|x64.Build.0 = Debug|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Debug|x86.Build.0 = Debug|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Release|Any CPU.Build.0 = Release|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Release|x64.ActiveCfg = Release|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Release|x64.Build.0 = Release|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Release|x86.ActiveCfg = Release|Any CPU + {A7FCE49E-5579-4E37-81E0-6AADEA0FBEA1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {092916B2-32C6-4ED9-A4EA-DF31D2F91A8F} = {23DA774D-7AE9-48C1-A261-F27D15A07858} - {9A28A475-067B-4CBD-94BC-CA31C0D1555A} = {E74ADFBD-55F3-4E28-885B-2270670294EF} - {4F9BDC85-B8B2-4AC5-99BC-1F2F0CF80016} = {E74ADFBD-55F3-4E28-885B-2270670294EF} + {E8C7267F-5BC3-4A13-B5EF-60DC1B0E919F} = {E74ADFBD-55F3-4E28-885B-2270670294EF} {D9BFC7C7-10B2-4D61-99EA-79805074CCD7} = {E74ADFBD-55F3-4E28-885B-2270670294EF} + {4F9BDC85-B8B2-4AC5-99BC-1F2F0CF80016} = {E74ADFBD-55F3-4E28-885B-2270670294EF} {E9D29AA9-1B7D-4A90-839A-2E8665A8714F} = {E74ADFBD-55F3-4E28-885B-2270670294EF} - {E8C7267F-5BC3-4A13-B5EF-60DC1B0E919F} = {E74ADFBD-55F3-4E28-885B-2270670294EF} + {092916B2-32C6-4ED9-A4EA-DF31D2F91A8F} = {23DA774D-7AE9-48C1-A261-F27D15A07858} + {9A28A475-067B-4CBD-94BC-CA31C0D1555A} = {E74ADFBD-55F3-4E28-885B-2270670294EF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36BDDB86-FBAB-45BF-AA22-DD4509504772} diff --git a/src/Modix.Bot/Behaviors/ModerationLoggingBehavior.cs b/src/Modix.Bot/Behaviors/ModerationLoggingBehavior.cs index bd4d866dd..4d881ad8b 100644 --- a/src/Modix.Bot/Behaviors/ModerationLoggingBehavior.cs +++ b/src/Modix.Bot/Behaviors/ModerationLoggingBehavior.cs @@ -9,8 +9,9 @@ using Modix.Data.Models.Core; using Modix.Data.Models.Moderation; using Modix.Data.Repositories; +using Modix.Models.Core; +using Modix.Models.Moderation; using Modix.Services; -using Modix.Services.Core; using Modix.Services.Moderation; using Modix.Services.Utilities; diff --git a/src/Modix.Bot/Behaviors/PromotionLoggingHandler.cs b/src/Modix.Bot/Behaviors/PromotionLoggingHandler.cs index 205f47c31..71ddadeb4 100644 --- a/src/Modix.Bot/Behaviors/PromotionLoggingHandler.cs +++ b/src/Modix.Bot/Behaviors/PromotionLoggingHandler.cs @@ -12,6 +12,8 @@ using Modix.Common.Messaging; using Modix.Data.Models.Core; using Modix.Data.Models.Promotions; +using Modix.Models.Core; +using Modix.Models.Promotions; using Modix.Services; using Modix.Services.Core; using Modix.Services.Promotions; diff --git a/src/Modix.Bot/DiscordBotSession.cs b/src/Modix.Bot/DiscordBotSession.cs index 13bc302b9..ceca86e2c 100644 --- a/src/Modix.Bot/DiscordBotSession.cs +++ b/src/Modix.Bot/DiscordBotSession.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Discord.Commands; using Discord.WebSocket; -using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services; namespace Modix.Bot; diff --git a/src/Modix.Bot/Modules/AuthorizationModule.cs b/src/Modix.Bot/Modules/AuthorizationModule.cs index 20113ba1b..4485900af 100644 --- a/src/Modix.Bot/Modules/AuthorizationModule.cs +++ b/src/Modix.Bot/Modules/AuthorizationModule.cs @@ -10,6 +10,7 @@ using Modix.Bot.Extensions; using Modix.Bot.Preconditions; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.CommandHelp; using Modix.Services.Core; diff --git a/src/Modix.Bot/Modules/DesignatedChannelsModule.cs b/src/Modix.Bot/Modules/DesignatedChannelsModule.cs index 2b49c92b2..1cbe5836d 100644 --- a/src/Modix.Bot/Modules/DesignatedChannelsModule.cs +++ b/src/Modix.Bot/Modules/DesignatedChannelsModule.cs @@ -9,6 +9,7 @@ using Modix.Bot.Preconditions; using Modix.Common.Extensions; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services; using Modix.Services.CommandHelp; diff --git a/src/Modix.Bot/Modules/DesignatedRoleModule.cs b/src/Modix.Bot/Modules/DesignatedRoleModule.cs index 9e0e5fcd3..058be5b5c 100644 --- a/src/Modix.Bot/Modules/DesignatedRoleModule.cs +++ b/src/Modix.Bot/Modules/DesignatedRoleModule.cs @@ -9,6 +9,7 @@ using Modix.Bot.Preconditions; using Modix.Common.Extensions; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.CommandHelp; using Modix.Services.Core; diff --git a/src/Modix.Bot/Modules/InfractionModule.cs b/src/Modix.Bot/Modules/InfractionModule.cs index 9a4671e9a..2a1f832de 100644 --- a/src/Modix.Bot/Modules/InfractionModule.cs +++ b/src/Modix.Bot/Modules/InfractionModule.cs @@ -15,6 +15,8 @@ using Modix.Data.Models; using Modix.Data.Models.Core; using Modix.Data.Models.Moderation; +using Modix.Models.Core; +using Modix.Models.Moderation; using Modix.Services.CommandHelp; using Modix.Services.Moderation; using Modix.Services.Utilities; diff --git a/src/Modix.Bot/Modules/ModerationModule.cs b/src/Modix.Bot/Modules/ModerationModule.cs index 93bab6272..22a26bf38 100644 --- a/src/Modix.Bot/Modules/ModerationModule.cs +++ b/src/Modix.Bot/Modules/ModerationModule.cs @@ -12,7 +12,7 @@ using Modix.Bot.Extensions; using Modix.Common.Extensions; using Modix.Data.Models.Core; -using Modix.Data.Models.Moderation; +using Modix.Models.Moderation; using Modix.Services.CommandHelp; using Modix.Services.Core; using Modix.Services.Moderation; diff --git a/src/Modix.Bot/Modules/PromotionsModule.cs b/src/Modix.Bot/Modules/PromotionsModule.cs index 5b6c8b384..4daa95dd7 100644 --- a/src/Modix.Bot/Modules/PromotionsModule.cs +++ b/src/Modix.Bot/Modules/PromotionsModule.cs @@ -20,6 +20,9 @@ using Modix.Data.Models.Core; using Modix.Data.Models.Promotions; using Modix.Data.Utilities; +using Modix.Models.Core; +using Modix.Models.Promotions; +using Modix.Models.Utilities; using Modix.Services.CommandHelp; using Modix.Services.Promotions; using Modix.Services.Utilities; @@ -334,7 +337,7 @@ public async Task AcceptAsync( return; } - var timeRemaining = campaign.GetTimeUntilCampaignCanBeClosed(); + var timeRemaining = campaign.CreateAction.Created.GetTimeUntilCampaignCanBeClosed(); if (timeRemaining.TotalMinutes > 1) { @@ -441,7 +444,7 @@ await message.ModifyAsync(x => _ => Color.Red, }; - var expectedCloseTime = campaign.GetExpectedCampaignCloseTimeStamp(); + var expectedCloseTime = campaign.CreateAction.Created.GetExpectedCampaignCloseTimeStamp(); var timeRemainingLabel = campaign.Outcome switch { diff --git a/src/Modix.Bot/Modules/TagModule.cs b/src/Modix.Bot/Modules/TagModule.cs index d701d83bd..d017fe6bf 100644 --- a/src/Modix.Bot/Modules/TagModule.cs +++ b/src/Modix.Bot/Modules/TagModule.cs @@ -14,6 +14,7 @@ using Modix.Common.Extensions; using Modix.Data.Models.Core; using Modix.Data.Models.Tags; +using Modix.Models.Core; using Modix.Services.CommandHelp; using Modix.Services.Core; using Modix.Services.Tags; diff --git a/src/Modix.Bot/Modules/UserInfoModule.cs b/src/Modix.Bot/Modules/UserInfoModule.cs index 7078b5e5f..ada571796 100644 --- a/src/Modix.Bot/Modules/UserInfoModule.cs +++ b/src/Modix.Bot/Modules/UserInfoModule.cs @@ -21,6 +21,7 @@ using Modix.Data.Models.Emoji; using Modix.Data.Models.Promotions; using Modix.Data.Repositories; +using Modix.Models.Core; using Modix.Services.AutoRemoveMessage; using Modix.Services.CommandHelp; using Modix.Services.Core; diff --git a/src/Modix.Bot/Preconditions/RequireAnyClaimAttribute.cs b/src/Modix.Bot/Preconditions/RequireAnyClaimAttribute.cs index 7037b2c55..c8e3394d6 100644 --- a/src/Modix.Bot/Preconditions/RequireAnyClaimAttribute.cs +++ b/src/Modix.Bot/Preconditions/RequireAnyClaimAttribute.cs @@ -6,8 +6,7 @@ using Discord.Interactions; using Microsoft.Extensions.DependencyInjection; - -using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; namespace Modix.Bot.Preconditions diff --git a/src/Modix.Bot/Preconditions/RequireClaimsAttribute.cs b/src/Modix.Bot/Preconditions/RequireClaimsAttribute.cs index 4fc8c1dd3..b2c8962bb 100644 --- a/src/Modix.Bot/Preconditions/RequireClaimsAttribute.cs +++ b/src/Modix.Bot/Preconditions/RequireClaimsAttribute.cs @@ -6,8 +6,7 @@ using Discord.Interactions; using Microsoft.Extensions.DependencyInjection; - -using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; namespace Modix.Bot.Preconditions diff --git a/src/Modix.Bot/Responders/AuditLogCreatedResponder.cs b/src/Modix.Bot/Responders/AuditLogCreatedResponder.cs index 7db9beb52..a33d95b60 100644 --- a/src/Modix.Bot/Responders/AuditLogCreatedResponder.cs +++ b/src/Modix.Bot/Responders/AuditLogCreatedResponder.cs @@ -5,7 +5,7 @@ using Discord.WebSocket; using MediatR; using Modix.Bot.Notifications; -using Modix.Data.Models.Moderation; +using Modix.Models.Moderation; using Modix.Services.Core; using Modix.Services.Moderation; using Modix.Services.Utilities; diff --git a/src/Modix.Bot/Responders/MutedUserJoinedResponder.cs b/src/Modix.Bot/Responders/MutedUserJoinedResponder.cs index da2c08926..5feed44d4 100644 --- a/src/Modix.Bot/Responders/MutedUserJoinedResponder.cs +++ b/src/Modix.Bot/Responders/MutedUserJoinedResponder.cs @@ -4,7 +4,7 @@ using Discord.WebSocket; using MediatR; using Modix.Bot.Notifications; -using Modix.Data.Models.Moderation; +using Modix.Models.Moderation; using Modix.Services.Moderation; using Serilog; diff --git a/src/Modix.Bot/Responders/StarboardReactionResponder.cs b/src/Modix.Bot/Responders/StarboardReactionResponder.cs index 79501474c..7184670ed 100644 --- a/src/Modix.Bot/Responders/StarboardReactionResponder.cs +++ b/src/Modix.Bot/Responders/StarboardReactionResponder.cs @@ -4,7 +4,7 @@ using MediatR; using Modix.Bot.Notifications; using Modix.Bot.Responders.MessageQuotes; -using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services; using Modix.Services.Starboard; using Modix.Services.Utilities; diff --git a/src/Modix.Data/Extensions/MessageQueryExtensions.cs b/src/Modix.Data/Extensions/MessageQueryExtensions.cs index b38ada2c2..d8a1de814 100644 --- a/src/Modix.Data/Extensions/MessageQueryExtensions.cs +++ b/src/Modix.Data/Extensions/MessageQueryExtensions.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Linq.Expressions; using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Data.Extensions { diff --git a/src/Modix.Data/Extensions/PromotionCampaignQueryExtensions.cs b/src/Modix.Data/Extensions/PromotionCampaignQueryExtensions.cs index 38195d002..0d08feda4 100644 --- a/src/Modix.Data/Extensions/PromotionCampaignQueryExtensions.cs +++ b/src/Modix.Data/Extensions/PromotionCampaignQueryExtensions.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Linq.Expressions; using Modix.Data.Models.Promotions; +using Modix.Models.Promotions; namespace Modix.Data.Extensions { diff --git a/src/Modix.Data/Models/Core/AuthorizationClaim.cs b/src/Modix.Data/Models/Core/AuthorizationClaim.cs deleted file mode 100644 index 0bdf7cb4d..000000000 --- a/src/Modix.Data/Models/Core/AuthorizationClaim.cs +++ /dev/null @@ -1,167 +0,0 @@ -using Modix.Data.Utilities; -using static Modix.Data.Models.Core.AuthorizationClaimCategory; - -namespace Modix.Data.Models.Core -{ - /// - /// Defines the types of claims that can be used to authorize a request. - /// - public enum AuthorizationClaim - { - /// - /// Authorizes a request to configure the authorization feature. - /// - [ClaimInfo(Configuration, "Authorizes a request to configure the authorization feature.")] - AuthorizationConfigure, - /// - /// Authorizes a request to read infraction/moderation data. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to read infraction/moderation data.")] - ModerationRead, - /// - /// Authorizes a request to attach a note upon a user. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to attach a note upon a user.")] - ModerationNote, - /// - /// Authorizes a request to issue a warning to a user. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to issue a warning to a user.")] - ModerationWarn, - /// - /// Authorizes a request to mute a user. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to mute a user.")] - ModerationMute, - /// - /// Authorizes a request to ban a user. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to ban a user.")] - ModerationBan, - /// - /// Authorizes a request to configure the moderation feature. - /// - [ClaimInfo(Configuration, "Authorizes a request to configure the moderation feature.")] - ModerationConfigure, - /// - /// Authorizes a request to rescind an infraction upon a user. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to rescind an infraction upon a user.")] - ModerationRescind, - /// - /// Authorizes a request to delete an infraction upon a user. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to delete an infraction upon a user.")] - ModerationDeleteInfraction, - /// - /// Authorizes a request to update an infraction. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to update an infraction.")] - ModerationUpdateInfraction, - /// - /// Authorizes a request to delete a message from a guild. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to delete a message from a guild.")] - ModerationDeleteMessage, - /// - /// Authorizes a request to mass-delete messages from a guild. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to mass-delete messages from a guild.")] - ModerationMassDeleteMessages, - /// - /// Authorizes a request to view deleted message logs. - /// - [ClaimInfo(Log, "Authorizes a request to view deleted message logs.")] - LogViewDeletedMessages, - /// - /// Authorizes a request to post a message to a guild, containing content that is in a pattern check. - /// - [ClaimInfo(Misc, "Authorizes a request to post a message to a guild, containing blocked content.")] - BypassMessageContentPatternCheck, - /// - /// Authorizes a request to create a designated channel mapping. - /// - [ClaimInfo(DesignatedChannels, "Authorizes a request to create a designated channel mapping.")] - DesignatedChannelMappingCreate, - /// - /// Authorizes a request to read designated channel mappings. - /// - [ClaimInfo(DesignatedChannels, "Authorizes a request to read designated channel mappings.")] - DesignatedChannelMappingRead, - /// - /// Authorizes a request to delete a designated channel mapping. - /// - [ClaimInfo(DesignatedChannels, "Authorizes a request to delete a designated channel mapping.")] - DesignatedChannelMappingDelete, - /// - /// Authorizes a request to create a designated role mapping. - /// - [ClaimInfo(DesignatedRoles, "Authorizes a request to create a designated role mapping.")] - DesignatedRoleMappingCreate, - /// - /// Authorizes a request to read designated role mappings. - /// - [ClaimInfo(DesignatedRoles, "Authorizes a request to read designated role mappings.")] - DesignatedRoleMappingRead, - /// - /// Authorizes a request to delete a designated role mapping. - /// - [ClaimInfo(DesignatedRoles, "Authorizes a request to delete a designated role mapping.")] - DesignatedRoleMappingDelete, - /// - /// Authorizes a request to create a promotion campaign for a user. - /// - [ClaimInfo(PromotionActions, "Authorizes a request to create a promotion campaign for a user.")] - PromotionsCreateCampaign, - /// - /// Authorizes a request to close a promotion campaign for a user. - /// - [ClaimInfo(PromotionActions, "Authorizes a request to close a promotion campaign for a user.")] - PromotionsCloseCampaign, - /// - /// Authorizes a request to comment on a promotion campaign for a user. - /// - [ClaimInfo(PromotionActions, "Authorizes a request to comment on a promotion campaign for a user.")] - PromotionsComment, - /// - /// Authorizes a request to read promotion campaign data. - /// - [ClaimInfo(PromotionActions, "Authorizes a request to read promotion campaign data.")] - PromotionsRead, - /// - /// Authorizes a request to perform a count for a popularity contest - /// - [ClaimInfo(Misc, "Authorizes a request to perform a count for a popularity contest")] - PopularityContestCount, - /// - /// Authorizes a request to mention a role that has restricted mentionability. - /// - [ClaimInfo(Misc, "Authorizes a request to mention a role that has restricted mentionability.")] - MentionRestrictedRole, - /// - /// Authorizes a request to create a tag. - /// - [ClaimInfo(TagActions, "Authorizes a request to create a tag.")] - CreateTag, - /// - /// Authorizes a request to invoke a tag. - /// - [ClaimInfo(TagActions, "Authorizes a request to invoke a tag.")] - UseTag, - /// - /// Authorizes a request to maintain a tag that was not created by the requesting user. - /// - [ClaimInfo(TagActions, "Authorizes a request to maintain a tag that was not created by the requesting user.")] - MaintainOtherUserTag, - /// - /// Authorizes a request to create a giveaway and determine its winners. - /// - [ClaimInfo(Misc, "Authorizes a request to create a giveaway and determine its winners.")] - ExecuteGiveaway, - /// - /// Authorizes a request to manage message patterns. - /// - [ClaimInfo(ModerationActions, "Authorizes a request to manage message patterns.")] - ManageMessageContentPatterns, - } -} diff --git a/src/Modix.Data/Models/Core/AuthorizationClaimCategory.cs b/src/Modix.Data/Models/Core/AuthorizationClaimCategory.cs deleted file mode 100644 index 2c55ea9c5..000000000 --- a/src/Modix.Data/Models/Core/AuthorizationClaimCategory.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Modix.Data.Models.Core -{ - public enum AuthorizationClaimCategory - { - Configuration, - ModerationActions, - DesignatedChannels, - DesignatedRoles, - PromotionActions, - TagActions, - Misc, - Log, - } -} diff --git a/src/Modix.Data/Models/Core/ClaimInfoData.cs b/src/Modix.Data/Models/Core/ClaimInfoData.cs deleted file mode 100644 index 0611663f8..000000000 --- a/src/Modix.Data/Models/Core/ClaimInfoData.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Modix.Data.Models.Core; -using Modix.Data.Utilities; - -namespace Modix.Models.Core -{ - public class ClaimInfoData - { - public string Name { get; set; } = null!; - public string Description { get; set; } = null!; - public AuthorizationClaimCategory Category { get; set; } - - private static Dictionary? _cachedClaimData; - - public static Dictionary GetClaims() - { - _cachedClaimData ??= typeof(AuthorizationClaim).GetFields(BindingFlags.Public | BindingFlags.Static).ToDictionary - ( - d => (AuthorizationClaim)d.GetValue(null)!, - d => - { - var claimInfo = (ClaimInfoAttribute)d.GetCustomAttributes(typeof(ClaimInfoAttribute), true).First()!; - - return new ClaimInfoData - { - Name = d.Name, - Description = claimInfo.Description, - Category = claimInfo.Category - }; - } - ); - - return _cachedClaimData; - } - } -} diff --git a/src/Modix.Data/Models/Core/ClaimMappingBrief.cs b/src/Modix.Data/Models/Core/ClaimMappingBrief.cs index 6432dcedb..bdc87d9dc 100644 --- a/src/Modix.Data/Models/Core/ClaimMappingBrief.cs +++ b/src/Modix.Data/Models/Core/ClaimMappingBrief.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using Modix.Data.ExpandableQueries; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/ClaimMappingCreationData.cs b/src/Modix.Data/Models/Core/ClaimMappingCreationData.cs index 8d039ef51..ef545a768 100644 --- a/src/Modix.Data/Models/Core/ClaimMappingCreationData.cs +++ b/src/Modix.Data/Models/Core/ClaimMappingCreationData.cs @@ -1,4 +1,5 @@ using System; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/ClaimMappingEntity.cs b/src/Modix.Data/Models/Core/ClaimMappingEntity.cs index 6f808434a..a468513d5 100644 --- a/src/Modix.Data/Models/Core/ClaimMappingEntity.cs +++ b/src/Modix.Data/Models/Core/ClaimMappingEntity.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/ClaimMappingSearchCriteria.cs b/src/Modix.Data/Models/Core/ClaimMappingSearchCriteria.cs index 1079bfe4e..8de04be18 100644 --- a/src/Modix.Data/Models/Core/ClaimMappingSearchCriteria.cs +++ b/src/Modix.Data/Models/Core/ClaimMappingSearchCriteria.cs @@ -3,6 +3,7 @@ using Modix.Data.Repositories; using Modix.Data.Utilities; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/ClaimMappingSummary.cs b/src/Modix.Data/Models/Core/ClaimMappingSummary.cs index feb5aef57..c8e62671f 100644 --- a/src/Modix.Data/Models/Core/ClaimMappingSummary.cs +++ b/src/Modix.Data/Models/Core/ClaimMappingSummary.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using Modix.Data.ExpandableQueries; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/ClaimMappingType.cs b/src/Modix.Data/Models/Core/ClaimMappingType.cs deleted file mode 100644 index 4c34f635e..000000000 --- a/src/Modix.Data/Models/Core/ClaimMappingType.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Modix.Data.Models.Core -{ - /// - /// Defines the possible types of claim mappings. - /// - public enum ClaimMappingType - { - /// - /// Describes a claim mapping where a claim is granted to an entity. - /// - Granted, - /// - /// Describes a claim mapping where a claim is denied to an entity. - /// - Denied, - } -} diff --git a/src/Modix.Data/Models/Core/DesignatedChannelMappingBrief.cs b/src/Modix.Data/Models/Core/DesignatedChannelMappingBrief.cs index 19ca3b76e..10b0ab727 100644 --- a/src/Modix.Data/Models/Core/DesignatedChannelMappingBrief.cs +++ b/src/Modix.Data/Models/Core/DesignatedChannelMappingBrief.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using Modix.Data.ExpandableQueries; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/DesignatedChannelMappingCreationData.cs b/src/Modix.Data/Models/Core/DesignatedChannelMappingCreationData.cs index 4a48f3749..ed712da48 100644 --- a/src/Modix.Data/Models/Core/DesignatedChannelMappingCreationData.cs +++ b/src/Modix.Data/Models/Core/DesignatedChannelMappingCreationData.cs @@ -1,4 +1,5 @@ using System; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/DesignatedChannelMappingEntity.cs b/src/Modix.Data/Models/Core/DesignatedChannelMappingEntity.cs index 7c6bc6957..8525fef95 100644 --- a/src/Modix.Data/Models/Core/DesignatedChannelMappingEntity.cs +++ b/src/Modix.Data/Models/Core/DesignatedChannelMappingEntity.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modix.Models.Core; namespace Modix.Data.Models.Core; diff --git a/src/Modix.Data/Models/Core/DesignatedChannelMappingSearchCriteria.cs b/src/Modix.Data/Models/Core/DesignatedChannelMappingSearchCriteria.cs index 63cd6c1a6..88ca7d244 100644 --- a/src/Modix.Data/Models/Core/DesignatedChannelMappingSearchCriteria.cs +++ b/src/Modix.Data/Models/Core/DesignatedChannelMappingSearchCriteria.cs @@ -2,6 +2,7 @@ using System.Linq; using Modix.Data.Utilities; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/DesignatedChannelType.cs b/src/Modix.Data/Models/Core/DesignatedChannelType.cs deleted file mode 100644 index 81594568d..000000000 --- a/src/Modix.Data/Models/Core/DesignatedChannelType.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Modix.Data.Models.Core -{ - /// - /// Defines the possible types designations that may be assigned to channels. - /// - public enum DesignatedChannelType - { - /// - /// Defines a channel that logs actions performed by the moderation feature. - /// - ModerationLog, - /// - /// Defines a channel that logs modified and deleted messages. - /// - MessageLog, - /// - /// Defines a channel that logs actions performed by the promotions feature. - /// - PromotionLog, - /// - /// Defines a channel to send promotion campaign creation/closing notifications. - /// - PromotionNotifications, - /// - /// Defines a channel that is not subject to auto-moderation behaviors of the moderation feature. - /// - Unmoderated, - /// - /// Defines a channel to which starred messages are sent. - /// - Starboard, - /// - /// Defines a channel that should be included when calculating user participation. - /// - CountsTowardsParticipation, - /// - /// Defines a channel where messages, if starred, are not sent to the starboard - /// - IgnoredFromStarboard, - } -} diff --git a/src/Modix.Data/Models/Core/DesignatedRoleMappingBrief.cs b/src/Modix.Data/Models/Core/DesignatedRoleMappingBrief.cs index d072aaa7d..9b1070dd6 100644 --- a/src/Modix.Data/Models/Core/DesignatedRoleMappingBrief.cs +++ b/src/Modix.Data/Models/Core/DesignatedRoleMappingBrief.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using Modix.Data.ExpandableQueries; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/DesignatedRoleMappingCreationData.cs b/src/Modix.Data/Models/Core/DesignatedRoleMappingCreationData.cs index ddf4ece1e..f99e10037 100644 --- a/src/Modix.Data/Models/Core/DesignatedRoleMappingCreationData.cs +++ b/src/Modix.Data/Models/Core/DesignatedRoleMappingCreationData.cs @@ -1,4 +1,5 @@ using System; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/DesignatedRoleMappingEntity.cs b/src/Modix.Data/Models/Core/DesignatedRoleMappingEntity.cs index 1fb15adbb..a4006c6be 100644 --- a/src/Modix.Data/Models/Core/DesignatedRoleMappingEntity.cs +++ b/src/Modix.Data/Models/Core/DesignatedRoleMappingEntity.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/DesignatedRoleMappingSearchCriteria.cs b/src/Modix.Data/Models/Core/DesignatedRoleMappingSearchCriteria.cs index 5c034480e..92c1cc414 100644 --- a/src/Modix.Data/Models/Core/DesignatedRoleMappingSearchCriteria.cs +++ b/src/Modix.Data/Models/Core/DesignatedRoleMappingSearchCriteria.cs @@ -3,6 +3,7 @@ using System.Linq; using Modix.Data.Utilities; +using Modix.Models.Core; namespace Modix.Data.Models.Core { diff --git a/src/Modix.Data/Models/Core/DesignatedRoleType.cs b/src/Modix.Data/Models/Core/DesignatedRoleType.cs deleted file mode 100644 index 416922084..000000000 --- a/src/Modix.Data/Models/Core/DesignatedRoleType.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Modix.Data.Models.Core -{ - /// - /// Defines the possible types designations that may be assigned to roles. - /// - public enum DesignatedRoleType - { - /// - /// Defines a role that serves as a member of the rank hierarchy. - /// - Rank, - /// - /// Defines a role that is used by the moderation feature to mute users. - /// - ModerationMute, - /// - /// Defines a role whose mentionability is allowed throughout the guild. - /// - Pingable, - } -} diff --git a/src/Modix.Data/Models/Moderation/InfractionBrief.cs b/src/Modix.Data/Models/Moderation/InfractionBrief.cs index d90c9e002..8f223abe0 100644 --- a/src/Modix.Data/Models/Moderation/InfractionBrief.cs +++ b/src/Modix.Data/Models/Moderation/InfractionBrief.cs @@ -3,6 +3,7 @@ using Modix.Data.ExpandableQueries; using Modix.Data.Models.Core; +using Modix.Models.Moderation; namespace Modix.Data.Models.Moderation { diff --git a/src/Modix.Data/Models/Moderation/InfractionCreationData.cs b/src/Modix.Data/Models/Moderation/InfractionCreationData.cs index f9491cef8..5580cad2d 100644 --- a/src/Modix.Data/Models/Moderation/InfractionCreationData.cs +++ b/src/Modix.Data/Models/Moderation/InfractionCreationData.cs @@ -1,4 +1,5 @@ using System; +using Modix.Models.Moderation; namespace Modix.Data.Models.Moderation { diff --git a/src/Modix.Data/Models/Moderation/InfractionEntity.cs b/src/Modix.Data/Models/Moderation/InfractionEntity.cs index b0bb8c49f..c1681fc4a 100644 --- a/src/Modix.Data/Models/Moderation/InfractionEntity.cs +++ b/src/Modix.Data/Models/Moderation/InfractionEntity.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Modix.Data.Models.Core; +using Modix.Models.Moderation; namespace Modix.Data.Models.Moderation { diff --git a/src/Modix.Data/Models/Moderation/InfractionSearchCriteria.cs b/src/Modix.Data/Models/Moderation/InfractionSearchCriteria.cs index f4b4177cb..437b1871f 100644 --- a/src/Modix.Data/Models/Moderation/InfractionSearchCriteria.cs +++ b/src/Modix.Data/Models/Moderation/InfractionSearchCriteria.cs @@ -5,6 +5,7 @@ using Modix.Data.Models.Core; using Modix.Data.Repositories; using Modix.Data.Utilities; +using Modix.Models.Moderation; namespace Modix.Data.Models.Moderation { diff --git a/src/Modix.Data/Models/Moderation/InfractionSummary.cs b/src/Modix.Data/Models/Moderation/InfractionSummary.cs index 60ca0a5f5..ac7b7fc92 100644 --- a/src/Modix.Data/Models/Moderation/InfractionSummary.cs +++ b/src/Modix.Data/Models/Moderation/InfractionSummary.cs @@ -4,6 +4,7 @@ using Modix.Data.ExpandableQueries; using Modix.Data.Models.Core; +using Modix.Models.Moderation; namespace Modix.Data.Models.Moderation { diff --git a/src/Modix.Data/Models/Moderation/InfractionType.cs b/src/Modix.Data/Models/Moderation/InfractionType.cs deleted file mode 100644 index f751801d9..000000000 --- a/src/Modix.Data/Models/Moderation/InfractionType.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Modix.Data.Models.Moderation -{ - /// - /// Defines the possible types of infractions that can be recorded for a user. - /// - public enum InfractionType - { - /// - /// Describes an "infraction" that is really just a note for communication between moderators. - /// - Notice, - /// - /// Describes an infraction where the user was issued a warning. - /// - Warning, - /// - /// Describes an infraction where the user was muted, preventing them from sending messages in text channels or speaking in voice channels. - /// - Mute, - /// - /// Describes an infraction where the user was banned from a guild. - /// - Ban, - } -} diff --git a/src/Modix.Data/Models/Promotions/PromotionCampaignBrief.cs b/src/Modix.Data/Models/Promotions/PromotionCampaignBrief.cs index be6bfcbf2..8a793378c 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCampaignBrief.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCampaignBrief.cs @@ -3,6 +3,7 @@ using Modix.Data.ExpandableQueries; using Modix.Data.Models.Core; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCampaignDetails.cs b/src/Modix.Data/Models/Promotions/PromotionCampaignDetails.cs index d1f9a32ea..b05113cb5 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCampaignDetails.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCampaignDetails.cs @@ -5,6 +5,7 @@ using Modix.Data.ExpandableQueries; using Modix.Data.Models.Core; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCampaignEntity.cs b/src/Modix.Data/Models/Promotions/PromotionCampaignEntity.cs index f0e875c2b..86bf04511 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCampaignEntity.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCampaignEntity.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using Modix.Data.Models.Core; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCampaignOutcome.cs b/src/Modix.Data/Models/Promotions/PromotionCampaignOutcome.cs deleted file mode 100644 index 1db2866f3..000000000 --- a/src/Modix.Data/Models/Promotions/PromotionCampaignOutcome.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Modix.Data.Models.Promotions -{ - /// - /// Defines the possible types of outcomes of a promotion campaign. - /// - public enum PromotionCampaignOutcome - { - /// - /// Describes a campaign that was accepted, for which the subject was promoted to the proposed rank. - /// - Accepted, - /// - /// Describes a campaign that was rejected, for which the subject was not promoted to the proposed rank. - /// - Rejected, - /// - /// Describes a campaign for which an error occurred during processing, that prevented the proposed promotion from being fully applied. - /// - Failed - } -} diff --git a/src/Modix.Data/Models/Promotions/PromotionCampaignSearchCriteria.cs b/src/Modix.Data/Models/Promotions/PromotionCampaignSearchCriteria.cs index e14131e95..6578ede7d 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCampaignSearchCriteria.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCampaignSearchCriteria.cs @@ -3,6 +3,7 @@ using Modix.Data.Repositories; using Modix.Data.Utilities; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCampaignSummary.cs b/src/Modix.Data/Models/Promotions/PromotionCampaignSummary.cs index 601995072..cbc97bf99 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCampaignSummary.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCampaignSummary.cs @@ -4,6 +4,7 @@ using Modix.Data.ExpandableQueries; using Modix.Data.Models.Core; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCommentActionBrief.cs b/src/Modix.Data/Models/Promotions/PromotionCommentActionBrief.cs index 3b4531025..3a9511aef 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCommentActionBrief.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCommentActionBrief.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using Modix.Data.ExpandableQueries; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCommentCampaignBrief.cs b/src/Modix.Data/Models/Promotions/PromotionCommentCampaignBrief.cs index 5a9323e75..c4789255b 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCommentCampaignBrief.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCommentCampaignBrief.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using Modix.Data.ExpandableQueries; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCommentCreationData.cs b/src/Modix.Data/Models/Promotions/PromotionCommentCreationData.cs index f2ca6713c..363761fbd 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCommentCreationData.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCommentCreationData.cs @@ -1,4 +1,5 @@ using System; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCommentEntity.cs b/src/Modix.Data/Models/Promotions/PromotionCommentEntity.cs index b8e3b1963..fec63eb1b 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCommentEntity.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCommentEntity.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCommentMutationData.cs b/src/Modix.Data/Models/Promotions/PromotionCommentMutationData.cs index 5e326a5cf..61600ec85 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCommentMutationData.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCommentMutationData.cs @@ -1,4 +1,6 @@ -namespace Modix.Data.Models.Promotions +using Modix.Models.Promotions; + +namespace Modix.Data.Models.Promotions { /// /// Describes an operation to modify a object. diff --git a/src/Modix.Data/Models/Promotions/PromotionCommentSearchCriteria.cs b/src/Modix.Data/Models/Promotions/PromotionCommentSearchCriteria.cs index b6da35ba8..1cb8d8d29 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCommentSearchCriteria.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCommentSearchCriteria.cs @@ -3,6 +3,7 @@ using Modix.Data.Repositories; using Modix.Data.Utilities; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionCommentSummary.cs b/src/Modix.Data/Models/Promotions/PromotionCommentSummary.cs index ecc050dcf..0123c83fd 100644 --- a/src/Modix.Data/Models/Promotions/PromotionCommentSummary.cs +++ b/src/Modix.Data/Models/Promotions/PromotionCommentSummary.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using Modix.Data.ExpandableQueries; +using Modix.Models.Promotions; namespace Modix.Data.Models.Promotions { diff --git a/src/Modix.Data/Models/Promotions/PromotionSentiment.cs b/src/Modix.Data/Models/Promotions/PromotionSentiment.cs deleted file mode 100644 index 0127de699..000000000 --- a/src/Modix.Data/Models/Promotions/PromotionSentiment.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Modix.Data.Models.Promotions -{ - /// - /// Defines the possible opinions that a may express about a . - /// - public enum PromotionSentiment - { - /// - /// Describes a comment that does not express a specific opinion about a promotion campaign. - /// - Abstain, - /// - /// Describes a comment that approves of a promotion campaign. - /// - Approve, - /// - /// Describes a comment that opposes a promotion campaign. - /// - Oppose - } -} diff --git a/src/Modix.Data/Models/RecordsPage.cs b/src/Modix.Data/Models/RecordsPage.cs deleted file mode 100644 index b07a626b5..000000000 --- a/src/Modix.Data/Models/RecordsPage.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; - -namespace Modix.Data.Models -{ - /// - /// Describes paged subset of records, from a larger recordset. - /// - /// The type of record contained in the page. - public class RecordsPage - { - /// - /// The total number of records in the recordset. - /// - public long TotalRecordCount { get; set; } - - /// - /// The number of records in the recordset, after applying an optional set of filtering criteria. - /// - public long FilteredRecordCount { get; set; } - - /// - /// The current page of records, selected from the larger recordset. - /// - public IReadOnlyCollection Records { get; set; } = null!; - } -} diff --git a/src/Modix.Data/Modix.Data.csproj b/src/Modix.Data/Modix.Data.csproj index 17148c129..1bb9bd3d5 100644 --- a/src/Modix.Data/Modix.Data.csproj +++ b/src/Modix.Data/Modix.Data.csproj @@ -18,5 +18,6 @@ + \ No newline at end of file diff --git a/src/Modix.Data/Repositories/DeletedMessageRepository.cs b/src/Modix.Data/Repositories/DeletedMessageRepository.cs index 770de2bc0..8d52a75ae 100644 --- a/src/Modix.Data/Repositories/DeletedMessageRepository.cs +++ b/src/Modix.Data/Repositories/DeletedMessageRepository.cs @@ -10,6 +10,7 @@ using Modix.Data.Models; using Modix.Data.Models.Moderation; using Modix.Data.Utilities; +using Modix.Models; namespace Modix.Data.Repositories { diff --git a/src/Modix.Data/Repositories/InfractionRepository.cs b/src/Modix.Data/Repositories/InfractionRepository.cs index 8e00b4b20..c1c3d8a8e 100644 --- a/src/Modix.Data/Repositories/InfractionRepository.cs +++ b/src/Modix.Data/Repositories/InfractionRepository.cs @@ -9,6 +9,8 @@ using Modix.Data.Models; using Modix.Data.Models.Moderation; using Modix.Data.Utilities; +using Modix.Models; +using Modix.Models.Moderation; namespace Modix.Data.Repositories { diff --git a/src/Modix.Data/Repositories/PromotionCampaignRepository.cs b/src/Modix.Data/Repositories/PromotionCampaignRepository.cs index d964d5739..7d7a8cd77 100644 --- a/src/Modix.Data/Repositories/PromotionCampaignRepository.cs +++ b/src/Modix.Data/Repositories/PromotionCampaignRepository.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Modix.Data.ExpandableQueries; using Modix.Data.Models.Promotions; +using Modix.Models.Promotions; namespace Modix.Data.Repositories { diff --git a/src/Modix.Data/Utilities/ClaimInfoAttribute.cs b/src/Modix.Data/Utilities/ClaimInfoAttribute.cs deleted file mode 100644 index cb00ffd66..000000000 --- a/src/Modix.Data/Utilities/ClaimInfoAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Modix.Data.Models.Core; - -namespace Modix.Data.Utilities -{ - [AttributeUsage(AttributeTargets.Field)] - public class ClaimInfoAttribute : Attribute - { - public AuthorizationClaimCategory Category { get; set; } - public string Description { get; set; } - - public ClaimInfoAttribute(AuthorizationClaimCategory category, string description) - { - Category = category; - Description = description; - } - } -} diff --git a/src/Modix.Data/Utilities/PromotionCampaignEntityExtensions.cs b/src/Modix.Data/Utilities/PromotionCampaignEntityExtensions.cs deleted file mode 100644 index d9848127c..000000000 --- a/src/Modix.Data/Utilities/PromotionCampaignEntityExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Modix.Data.Models.Promotions; - -namespace Modix.Data.Utilities -{ - public static class PromotionCampaignEntityExtensions - { - public static readonly TimeSpan CampaignAcceptCooldown = TimeSpan.FromHours(48); - - public static TimeSpan GetTimeUntilCampaignCanBeClosed(this PromotionCampaignSummary campaign) - => campaign.CreateAction.Created.Add(CampaignAcceptCooldown) - DateTimeOffset.UtcNow; - - public static DateTimeOffset GetExpectedCampaignCloseTimeStamp(this PromotionCampaignSummary campaign) - => campaign.CreateAction.Created.Add(CampaignAcceptCooldown); - } -} diff --git a/src/Modix.Models/Core/AuthorizationClaim.cs b/src/Modix.Models/Core/AuthorizationClaim.cs new file mode 100644 index 000000000..ea11e2265 --- /dev/null +++ b/src/Modix.Models/Core/AuthorizationClaim.cs @@ -0,0 +1,166 @@ +using Modix.Models.Utilities; +using static Modix.Models.Core.AuthorizationClaimCategory; + +namespace Modix.Models.Core; + +/// +/// Defines the types of claims that can be used to authorize a request. +/// +public enum AuthorizationClaim +{ + /// + /// Authorizes a request to configure the authorization feature. + /// + [ClaimInfo(Configuration, "Authorizes a request to configure the authorization feature.")] + AuthorizationConfigure, + /// + /// Authorizes a request to read infraction/moderation data. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to read infraction/moderation data.")] + ModerationRead, + /// + /// Authorizes a request to attach a note upon a user. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to attach a note upon a user.")] + ModerationNote, + /// + /// Authorizes a request to issue a warning to a user. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to issue a warning to a user.")] + ModerationWarn, + /// + /// Authorizes a request to mute a user. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to mute a user.")] + ModerationMute, + /// + /// Authorizes a request to ban a user. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to ban a user.")] + ModerationBan, + /// + /// Authorizes a request to configure the moderation feature. + /// + [ClaimInfo(Configuration, "Authorizes a request to configure the moderation feature.")] + ModerationConfigure, + /// + /// Authorizes a request to rescind an infraction upon a user. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to rescind an infraction upon a user.")] + ModerationRescind, + /// + /// Authorizes a request to delete an infraction upon a user. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to delete an infraction upon a user.")] + ModerationDeleteInfraction, + /// + /// Authorizes a request to update an infraction. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to update an infraction.")] + ModerationUpdateInfraction, + /// + /// Authorizes a request to delete a message from a guild. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to delete a message from a guild.")] + ModerationDeleteMessage, + /// + /// Authorizes a request to mass-delete messages from a guild. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to mass-delete messages from a guild.")] + ModerationMassDeleteMessages, + /// + /// Authorizes a request to view deleted message logs. + /// + [ClaimInfo(Log, "Authorizes a request to view deleted message logs.")] + LogViewDeletedMessages, + /// + /// Authorizes a request to post a message to a guild, containing content that is in a pattern check. + /// + [ClaimInfo(Misc, "Authorizes a request to post a message to a guild, containing blocked content.")] + BypassMessageContentPatternCheck, + /// + /// Authorizes a request to create a designated channel mapping. + /// + [ClaimInfo(DesignatedChannels, "Authorizes a request to create a designated channel mapping.")] + DesignatedChannelMappingCreate, + /// + /// Authorizes a request to read designated channel mappings. + /// + [ClaimInfo(DesignatedChannels, "Authorizes a request to read designated channel mappings.")] + DesignatedChannelMappingRead, + /// + /// Authorizes a request to delete a designated channel mapping. + /// + [ClaimInfo(DesignatedChannels, "Authorizes a request to delete a designated channel mapping.")] + DesignatedChannelMappingDelete, + /// + /// Authorizes a request to create a designated role mapping. + /// + [ClaimInfo(DesignatedRoles, "Authorizes a request to create a designated role mapping.")] + DesignatedRoleMappingCreate, + /// + /// Authorizes a request to read designated role mappings. + /// + [ClaimInfo(DesignatedRoles, "Authorizes a request to read designated role mappings.")] + DesignatedRoleMappingRead, + /// + /// Authorizes a request to delete a designated role mapping. + /// + [ClaimInfo(DesignatedRoles, "Authorizes a request to delete a designated role mapping.")] + DesignatedRoleMappingDelete, + /// + /// Authorizes a request to create a promotion campaign for a user. + /// + [ClaimInfo(PromotionActions, "Authorizes a request to create a promotion campaign for a user.")] + PromotionsCreateCampaign, + /// + /// Authorizes a request to close a promotion campaign for a user. + /// + [ClaimInfo(PromotionActions, "Authorizes a request to close a promotion campaign for a user.")] + PromotionsCloseCampaign, + /// + /// Authorizes a request to comment on a promotion campaign for a user. + /// + [ClaimInfo(PromotionActions, "Authorizes a request to comment on a promotion campaign for a user.")] + PromotionsComment, + /// + /// Authorizes a request to read promotion campaign data. + /// + [ClaimInfo(PromotionActions, "Authorizes a request to read promotion campaign data.")] + PromotionsRead, + /// + /// Authorizes a request to perform a count for a popularity contest + /// + [ClaimInfo(Misc, "Authorizes a request to perform a count for a popularity contest")] + PopularityContestCount, + /// + /// Authorizes a request to mention a role that has restricted mentionability. + /// + [ClaimInfo(Misc, "Authorizes a request to mention a role that has restricted mentionability.")] + MentionRestrictedRole, + /// + /// Authorizes a request to create a tag. + /// + [ClaimInfo(TagActions, "Authorizes a request to create a tag.")] + CreateTag, + /// + /// Authorizes a request to invoke a tag. + /// + [ClaimInfo(TagActions, "Authorizes a request to invoke a tag.")] + UseTag, + /// + /// Authorizes a request to maintain a tag that was not created by the requesting user. + /// + [ClaimInfo(TagActions, "Authorizes a request to maintain a tag that was not created by the requesting user.")] + MaintainOtherUserTag, + /// + /// Authorizes a request to create a giveaway and determine its winners. + /// + [ClaimInfo(Misc, "Authorizes a request to create a giveaway and determine its winners.")] + ExecuteGiveaway, + /// + /// Authorizes a request to manage message patterns. + /// + [ClaimInfo(ModerationActions, "Authorizes a request to manage message patterns.")] + ManageMessageContentPatterns, +} diff --git a/src/Modix.Models/Core/AuthorizationClaimCategory.cs b/src/Modix.Models/Core/AuthorizationClaimCategory.cs new file mode 100644 index 000000000..7277416a5 --- /dev/null +++ b/src/Modix.Models/Core/AuthorizationClaimCategory.cs @@ -0,0 +1,13 @@ +namespace Modix.Models.Core; + +public enum AuthorizationClaimCategory +{ + Configuration, + ModerationActions, + DesignatedChannels, + DesignatedRoles, + PromotionActions, + TagActions, + Misc, + Log, +} diff --git a/src/Modix.Models/Core/ClaimInfoData.cs b/src/Modix.Models/Core/ClaimInfoData.cs new file mode 100644 index 000000000..21ab5d976 --- /dev/null +++ b/src/Modix.Models/Core/ClaimInfoData.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using Modix.Models.Utilities; + +namespace Modix.Models.Core; + +public class ClaimInfoData +{ + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public AuthorizationClaimCategory Category { get; set; } + + private static Dictionary? _cachedClaimData; + + public static Dictionary GetClaims() + { + _cachedClaimData ??= typeof(AuthorizationClaim).GetFields(BindingFlags.Public | BindingFlags.Static).ToDictionary + ( + d => (AuthorizationClaim)d.GetValue(null)!, + d => + { + var claimInfo = (ClaimInfoAttribute)d.GetCustomAttributes(typeof(ClaimInfoAttribute), true).First()!; + + return new ClaimInfoData + { + Name = d.Name, + Description = claimInfo.Description, + Category = claimInfo.Category + }; + } + ); + + return _cachedClaimData; + } +} diff --git a/src/Modix.Models/Core/ClaimMappingType.cs b/src/Modix.Models/Core/ClaimMappingType.cs new file mode 100644 index 000000000..bcd0628e8 --- /dev/null +++ b/src/Modix.Models/Core/ClaimMappingType.cs @@ -0,0 +1,16 @@ +namespace Modix.Models.Core; + +/// +/// Defines the possible types of claim mappings. +/// +public enum ClaimMappingType +{ + /// + /// Describes a claim mapping where a claim is granted to an entity. + /// + Granted, + /// + /// Describes a claim mapping where a claim is denied to an entity. + /// + Denied, +} diff --git a/src/Modix.Models/Core/DesignatedChannelType.cs b/src/Modix.Models/Core/DesignatedChannelType.cs new file mode 100644 index 000000000..0acde2a89 --- /dev/null +++ b/src/Modix.Models/Core/DesignatedChannelType.cs @@ -0,0 +1,40 @@ +namespace Modix.Models.Core; + +/// +/// Defines the possible types designations that may be assigned to channels. +/// +public enum DesignatedChannelType +{ + /// + /// Defines a channel that logs actions performed by the moderation feature. + /// + ModerationLog, + /// + /// Defines a channel that logs modified and deleted messages. + /// + MessageLog, + /// + /// Defines a channel that logs actions performed by the promotions feature. + /// + PromotionLog, + /// + /// Defines a channel to send promotion campaign creation/closing notifications. + /// + PromotionNotifications, + /// + /// Defines a channel that is not subject to auto-moderation behaviors of the moderation feature. + /// + Unmoderated, + /// + /// Defines a channel to which starred messages are sent. + /// + Starboard, + /// + /// Defines a channel that should be included when calculating user participation. + /// + CountsTowardsParticipation, + /// + /// Defines a channel where messages, if starred, are not sent to the starboard + /// + IgnoredFromStarboard, +} diff --git a/src/Modix.Models/Core/DesignatedRoleType.cs b/src/Modix.Models/Core/DesignatedRoleType.cs new file mode 100644 index 000000000..d49bc2979 --- /dev/null +++ b/src/Modix.Models/Core/DesignatedRoleType.cs @@ -0,0 +1,20 @@ +namespace Modix.Models.Core; + +/// +/// Defines the possible types designations that may be assigned to roles. +/// +public enum DesignatedRoleType +{ + /// + /// Defines a role that serves as a member of the rank hierarchy. + /// + Rank, + /// + /// Defines a role that is used by the moderation feature to mute users. + /// + ModerationMute, + /// + /// Defines a role whose mentionability is allowed throughout the guild. + /// + Pingable, +} diff --git a/src/Modix.Models/Moderation/InfractionType.cs b/src/Modix.Models/Moderation/InfractionType.cs new file mode 100644 index 000000000..57759420e --- /dev/null +++ b/src/Modix.Models/Moderation/InfractionType.cs @@ -0,0 +1,24 @@ +namespace Modix.Models.Moderation; + +/// +/// Defines the possible types of infractions that can be recorded for a user. +/// +public enum InfractionType +{ + /// + /// Describes an "infraction" that is really just a note for communication between moderators. + /// + Notice, + /// + /// Describes an infraction where the user was issued a warning. + /// + Warning, + /// + /// Describes an infraction where the user was muted, preventing them from sending messages in text channels or speaking in voice channels. + /// + Mute, + /// + /// Describes an infraction where the user was banned from a guild. + /// + Ban, +} diff --git a/src/Modix.Models/Modix.Models.csproj b/src/Modix.Models/Modix.Models.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/src/Modix.Models/Modix.Models.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Modix.Models/Promotions/PromotionCampaignOutcome.cs b/src/Modix.Models/Promotions/PromotionCampaignOutcome.cs new file mode 100644 index 000000000..a37e93ba4 --- /dev/null +++ b/src/Modix.Models/Promotions/PromotionCampaignOutcome.cs @@ -0,0 +1,20 @@ +namespace Modix.Models.Promotions; + +/// +/// Defines the possible types of outcomes of a promotion campaign. +/// +public enum PromotionCampaignOutcome +{ + /// + /// Describes a campaign that was accepted, for which the subject was promoted to the proposed rank. + /// + Accepted, + /// + /// Describes a campaign that was rejected, for which the subject was not promoted to the proposed rank. + /// + Rejected, + /// + /// Describes a campaign for which an error occurred during processing, that prevented the proposed promotion from being fully applied. + /// + Failed +} diff --git a/src/Modix.Models/Promotions/PromotionSentiment.cs b/src/Modix.Models/Promotions/PromotionSentiment.cs new file mode 100644 index 000000000..f0c27edad --- /dev/null +++ b/src/Modix.Models/Promotions/PromotionSentiment.cs @@ -0,0 +1,20 @@ +namespace Modix.Models.Promotions; + +/// +/// Defines the possible opinions that a may express about a . +/// +public enum PromotionSentiment +{ + /// + /// Describes a comment that does not express a specific opinion about a promotion campaign. + /// + Abstain, + /// + /// Describes a comment that approves of a promotion campaign. + /// + Approve, + /// + /// Describes a comment that opposes a promotion campaign. + /// + Oppose +} diff --git a/src/Modix.Models/RecordsPage.cs b/src/Modix.Models/RecordsPage.cs new file mode 100644 index 000000000..0c514532f --- /dev/null +++ b/src/Modix.Models/RecordsPage.cs @@ -0,0 +1,23 @@ +namespace Modix.Models; + +/// +/// Describes paged subset of records, from a larger recordset. +/// +/// The type of record contained in the page. +public class RecordsPage +{ + /// + /// The total number of records in the recordset. + /// + public long TotalRecordCount { get; set; } + + /// + /// The number of records in the recordset, after applying an optional set of filtering criteria. + /// + public long FilteredRecordCount { get; set; } + + /// + /// The current page of records, selected from the larger recordset. + /// + public IReadOnlyCollection Records { get; set; } = null!; +} diff --git a/src/Modix.Models/Utilities/ClaimInfoAttribute.cs b/src/Modix.Models/Utilities/ClaimInfoAttribute.cs new file mode 100644 index 000000000..8b486a264 --- /dev/null +++ b/src/Modix.Models/Utilities/ClaimInfoAttribute.cs @@ -0,0 +1,16 @@ +using Modix.Models.Core; + +namespace Modix.Models.Utilities; + +[AttributeUsage(AttributeTargets.Field)] +public class ClaimInfoAttribute : Attribute +{ + public AuthorizationClaimCategory Category { get; set; } + public string Description { get; set; } + + public ClaimInfoAttribute(AuthorizationClaimCategory category, string description) + { + Category = category; + Description = description; + } +} diff --git a/src/Modix.Models/Utilities/PromotionCampaignEntityExtensions.cs b/src/Modix.Models/Utilities/PromotionCampaignEntityExtensions.cs new file mode 100644 index 000000000..93291f62b --- /dev/null +++ b/src/Modix.Models/Utilities/PromotionCampaignEntityExtensions.cs @@ -0,0 +1,12 @@ +namespace Modix.Models.Utilities; + +public static class PromotionCampaignEntityExtensions +{ + public static readonly TimeSpan CampaignAcceptCooldown = TimeSpan.FromHours(48); + + public static TimeSpan GetTimeUntilCampaignCanBeClosed(this DateTimeOffset campaignCreationDate) + => campaignCreationDate.Add(CampaignAcceptCooldown) - DateTimeOffset.UtcNow; + + public static DateTimeOffset GetExpectedCampaignCloseTimeStamp(this DateTimeOffset campaignCreationDate) + => campaignCreationDate.Add(CampaignAcceptCooldown); +} diff --git a/src/Modix.Services/AuthorizationClaimMappingService.cs b/src/Modix.Services/AuthorizationClaimMappingService.cs index f48169bd8..dffac3f43 100644 --- a/src/Modix.Services/AuthorizationClaimMappingService.cs +++ b/src/Modix.Services/AuthorizationClaimMappingService.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Modix.Data; using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Services; diff --git a/src/Modix.Services/AuthorizationClaimService.cs b/src/Modix.Services/AuthorizationClaimService.cs index 2ca4e6248..134c32fc1 100644 --- a/src/Modix.Services/AuthorizationClaimService.cs +++ b/src/Modix.Services/AuthorizationClaimService.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Modix.Data; using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Services; diff --git a/src/Modix.Services/Core/AuthorizationService.cs b/src/Modix.Services/Core/AuthorizationService.cs index 90c4f9946..6460802c5 100644 --- a/src/Modix.Services/Core/AuthorizationService.cs +++ b/src/Modix.Services/Core/AuthorizationService.cs @@ -15,6 +15,7 @@ using Modix.Data.Models.Core; using Modix.Data.Repositories; using Modix.Services.Utilities; +using Modix.Models.Core; namespace Modix.Services.Core { diff --git a/src/Modix.Services/Core/DesignatedRoleService.cs b/src/Modix.Services/Core/DesignatedRoleService.cs index 0df61133e..85327a9b1 100644 --- a/src/Modix.Services/Core/DesignatedRoleService.cs +++ b/src/Modix.Services/Core/DesignatedRoleService.cs @@ -6,6 +6,7 @@ using Modix.Data.Models.Core; using Modix.Data.Repositories; +using Modix.Models.Core; namespace Modix.Services.Core { diff --git a/src/Modix.Services/DesignatedChannelService.cs b/src/Modix.Services/DesignatedChannelService.cs index 0343d33d8..f706f320c 100644 --- a/src/Modix.Services/DesignatedChannelService.cs +++ b/src/Modix.Services/DesignatedChannelService.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Modix.Data; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; namespace Modix.Services; diff --git a/src/Modix.Services/IScopedSession.cs b/src/Modix.Services/IScopedSession.cs index b79b02bc5..bf16c5eab 100644 --- a/src/Modix.Services/IScopedSession.cs +++ b/src/Modix.Services/IScopedSession.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Services; diff --git a/src/Modix.Services/MessageContentPatterns/MessageContentPatternService.cs b/src/Modix.Services/MessageContentPatterns/MessageContentPatternService.cs index 1f7c026ce..a9e61b0c4 100644 --- a/src/Modix.Services/MessageContentPatterns/MessageContentPatternService.cs +++ b/src/Modix.Services/MessageContentPatterns/MessageContentPatternService.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Modix.Data; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; namespace Modix.Services.MessageContentPatterns diff --git a/src/Modix.Services/MessageLogging/MessageLoggingBehavior.cs b/src/Modix.Services/MessageLogging/MessageLoggingBehavior.cs index 6ad69105a..681e1192a 100644 --- a/src/Modix.Services/MessageLogging/MessageLoggingBehavior.cs +++ b/src/Modix.Services/MessageLogging/MessageLoggingBehavior.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; using Modix.Common.Messaging; -using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; using Modix.Services.Utilities; diff --git a/src/Modix.Services/Moderation/AttachmentBlacklistBehavior.cs b/src/Modix.Services/Moderation/AttachmentBlacklistBehavior.cs index 527781e46..d2404841b 100644 --- a/src/Modix.Services/Moderation/AttachmentBlacklistBehavior.cs +++ b/src/Modix.Services/Moderation/AttachmentBlacklistBehavior.cs @@ -13,7 +13,7 @@ using Modix.Common.Messaging; using Modix.Services.Core; -using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Services.Moderation { diff --git a/src/Modix.Services/Moderation/GuildOnboardingService.cs b/src/Modix.Services/Moderation/GuildOnboardingService.cs index a64adec39..56fb57ac8 100644 --- a/src/Modix.Services/Moderation/GuildOnboardingService.cs +++ b/src/Modix.Services/Moderation/GuildOnboardingService.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Modix.Data; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; using Serilog; diff --git a/src/Modix.Services/Moderation/MessageContentCheckBehaviour.cs b/src/Modix.Services/Moderation/MessageContentCheckBehaviour.cs index 42c3bdb17..77d17f3c3 100644 --- a/src/Modix.Services/Moderation/MessageContentCheckBehaviour.cs +++ b/src/Modix.Services/Moderation/MessageContentCheckBehaviour.cs @@ -7,6 +7,7 @@ using Discord.WebSocket; using Modix.Common.Messaging; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; using Modix.Services.MessageContentPatterns; using Serilog; diff --git a/src/Modix.Services/Moderation/ModerationService.cs b/src/Modix.Services/Moderation/ModerationService.cs index 7dea9e2f3..0de4ae29a 100644 --- a/src/Modix.Services/Moderation/ModerationService.cs +++ b/src/Modix.Services/Moderation/ModerationService.cs @@ -1,19 +1,22 @@ #nullable enable using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Discord; +using Microsoft.EntityFrameworkCore; +using Modix.Data; using Modix.Data.Models; using Modix.Data.Models.Core; using Modix.Data.Models.Moderation; using Modix.Data.Repositories; +using Modix.Models; +using Modix.Models.Core; +using Modix.Models.Moderation; using Modix.Services.Core; using Modix.Services.Utilities; using Serilog; -using System.Threading; -using Microsoft.EntityFrameworkCore; -using Modix.Data; namespace Modix.Services.Moderation; diff --git a/src/Modix.Services/Promotions/PromotionsService.cs b/src/Modix.Services/Promotions/PromotionsService.cs index b0f45dff9..a895eeb13 100644 --- a/src/Modix.Services/Promotions/PromotionsService.cs +++ b/src/Modix.Services/Promotions/PromotionsService.cs @@ -15,6 +15,9 @@ using Modix.Data.Models.Promotions; using Modix.Data.Repositories; using Modix.Data.Utilities; +using Modix.Models.Core; +using Modix.Models.Promotions; +using Modix.Models.Utilities; using Modix.Services.Core; using Modix.Services.Utilities; diff --git a/src/Modix.Services/Starboard/StarboardService.cs b/src/Modix.Services/Starboard/StarboardService.cs index 1bb97abf0..e118afa9d 100644 --- a/src/Modix.Services/Starboard/StarboardService.cs +++ b/src/Modix.Services/Starboard/StarboardService.cs @@ -2,9 +2,9 @@ using System.Threading.Tasks; using Discord; using Modix.Services.Core; -using Modix.Data.Models.Core; using System.Collections.Generic; using Modix.Data.Repositories; +using Modix.Models.Core; namespace Modix.Services.Starboard { diff --git a/src/Modix.Services/Tags/TagInlineParsingHandler.cs b/src/Modix.Services/Tags/TagInlineParsingHandler.cs index 15bb94557..3f6e8f0b3 100644 --- a/src/Modix.Services/Tags/TagInlineParsingHandler.cs +++ b/src/Modix.Services/Tags/TagInlineParsingHandler.cs @@ -9,6 +9,7 @@ using Modix.Common.Messaging; using Modix.Data.Models.Core; +using Modix.Models.Core; using Modix.Services.Core; namespace Modix.Services.Tags diff --git a/src/Modix.Services/Tags/TagService.cs b/src/Modix.Services/Tags/TagService.cs index 331786c40..13e097fd7 100644 --- a/src/Modix.Services/Tags/TagService.cs +++ b/src/Modix.Services/Tags/TagService.cs @@ -13,6 +13,7 @@ using Modix.Data.Models.Core; using Modix.Data.Models.Tags; using Modix.Data.Repositories; +using Modix.Models.Core; using Modix.Services.Core; namespace Modix.Services.Tags diff --git a/src/Modix.Services/Utilities/FormatUtilities.cs b/src/Modix.Services/Utilities/FormatUtilities.cs index a2fbe2783..bee73be46 100644 --- a/src/Modix.Services/Utilities/FormatUtilities.cs +++ b/src/Modix.Services/Utilities/FormatUtilities.cs @@ -3,7 +3,6 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; @@ -11,7 +10,7 @@ using Discord; using Humanizer; using Humanizer.Localisation; -using Modix.Data.Models.Moderation; +using Modix.Models.Moderation; namespace Modix.Services.Utilities { diff --git a/src/Modix.Web.Shared/Models/Commands/Command.cs b/src/Modix.Web.Shared/Models/Commands/Command.cs new file mode 100644 index 000000000..187d4d019 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Commands/Command.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.Commands; + +public record Command(string Name, string Summary, IReadOnlyCollection Aliases, IReadOnlyCollection Parameters, bool IsSlashCommand); diff --git a/src/Modix.Web/Models/Commands/Module.cs b/src/Modix.Web.Shared/Models/Commands/Module.cs similarity index 63% rename from src/Modix.Web/Models/Commands/Module.cs rename to src/Modix.Web.Shared/Models/Commands/Module.cs index e8f111a56..6e22098cc 100644 --- a/src/Modix.Web/Models/Commands/Module.cs +++ b/src/Modix.Web.Shared/Models/Commands/Module.cs @@ -1,3 +1,3 @@ -namespace Modix.Web.Models.Commands; +namespace Modix.Web.Shared.Models.Commands; public record Module(string Name, string Summary, IEnumerable Commands); diff --git a/src/Modix.Web.Shared/Models/Commands/Parameter.cs b/src/Modix.Web.Shared/Models/Commands/Parameter.cs new file mode 100644 index 000000000..650a40760 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Commands/Parameter.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.Commands; + +public record Parameter(string Name, string Summary, IReadOnlyCollection Options, string Type, bool IsOptional); diff --git a/src/Modix.Web/Models/Common/ChannelInformation.cs b/src/Modix.Web.Shared/Models/Common/ChannelInformation.cs similarity index 63% rename from src/Modix.Web/Models/Common/ChannelInformation.cs rename to src/Modix.Web.Shared/Models/Common/ChannelInformation.cs index 871dd49cf..7acfddb22 100644 --- a/src/Modix.Web/Models/Common/ChannelInformation.cs +++ b/src/Modix.Web.Shared/Models/Common/ChannelInformation.cs @@ -1,3 +1,3 @@ -namespace Modix.Web.Models.Common; +namespace Modix.Web.Shared.Models.Common; public record ChannelInformation(ulong Id, string Name) : IAutoCompleteItem; diff --git a/src/Modix.Web/Models/Common/IAutoCompleteItem.cs b/src/Modix.Web.Shared/Models/Common/IAutoCompleteItem.cs similarity index 61% rename from src/Modix.Web/Models/Common/IAutoCompleteItem.cs rename to src/Modix.Web.Shared/Models/Common/IAutoCompleteItem.cs index 6e28089cc..c5c6fe79f 100644 --- a/src/Modix.Web/Models/Common/IAutoCompleteItem.cs +++ b/src/Modix.Web.Shared/Models/Common/IAutoCompleteItem.cs @@ -1,4 +1,4 @@ -namespace Modix.Web.Models.Common; +namespace Modix.Web.Shared.Models.Common; public interface IAutoCompleteItem { diff --git a/src/Modix.Web.Shared/Models/Common/ModixUser.cs b/src/Modix.Web.Shared/Models/Common/ModixUser.cs new file mode 100644 index 000000000..21687a6fe --- /dev/null +++ b/src/Modix.Web.Shared/Models/Common/ModixUser.cs @@ -0,0 +1,8 @@ +namespace Modix.Web.Shared.Models.Common; + +public sealed class ModixUser : IAutoCompleteItem +{ + public string? Name { get; init; } + public ulong UserId { get; init; } + public string? AvatarUrl { get; init; } +} diff --git a/src/Modix.Web/Models/Common/RoleInformation.cs b/src/Modix.Web.Shared/Models/Common/RoleInformation.cs similarity index 66% rename from src/Modix.Web/Models/Common/RoleInformation.cs rename to src/Modix.Web.Shared/Models/Common/RoleInformation.cs index d5a36484c..d0e184335 100644 --- a/src/Modix.Web/Models/Common/RoleInformation.cs +++ b/src/Modix.Web.Shared/Models/Common/RoleInformation.cs @@ -1,3 +1,3 @@ -namespace Modix.Web.Models.Common; +namespace Modix.Web.Shared.Models.Common; public record RoleInformation(ulong Id, string Name, string Color) : IAutoCompleteItem; diff --git a/src/Modix.Web.Shared/Models/Configuration/ClaimMappingData.cs b/src/Modix.Web.Shared/Models/Configuration/ClaimMappingData.cs new file mode 100644 index 000000000..6405ce1d2 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Configuration/ClaimMappingData.cs @@ -0,0 +1,5 @@ +using Modix.Models.Core; + +namespace Modix.Web.Shared.Models.Configuration; + +public record class ClaimMappingData(ulong? RoleId, AuthorizationClaim Claim, ClaimMappingType Type); diff --git a/src/Modix.Web/Models/Configuration/DesignatedChannelData.cs b/src/Modix.Web.Shared/Models/Configuration/DesignatedChannelData.cs similarity index 60% rename from src/Modix.Web/Models/Configuration/DesignatedChannelData.cs rename to src/Modix.Web.Shared/Models/Configuration/DesignatedChannelData.cs index d2e8cbdbd..d244d73d0 100644 --- a/src/Modix.Web/Models/Configuration/DesignatedChannelData.cs +++ b/src/Modix.Web.Shared/Models/Configuration/DesignatedChannelData.cs @@ -1,5 +1,5 @@ -using Modix.Data.Models.Core; +using Modix.Models.Core; -namespace Modix.Web.Models.Configuration; +namespace Modix.Web.Shared.Models.Configuration; public record DesignatedChannelData(long Id, ulong RoleId, DesignatedChannelType ChannelDesignation, string Name); diff --git a/src/Modix.Web/Models/Configuration/DesignatedRoleData.cs b/src/Modix.Web.Shared/Models/Configuration/DesignatedRoleData.cs similarity index 58% rename from src/Modix.Web/Models/Configuration/DesignatedRoleData.cs rename to src/Modix.Web.Shared/Models/Configuration/DesignatedRoleData.cs index 169c92c19..ae1246b5a 100644 --- a/src/Modix.Web/Models/Configuration/DesignatedRoleData.cs +++ b/src/Modix.Web.Shared/Models/Configuration/DesignatedRoleData.cs @@ -1,5 +1,5 @@ -using Modix.Data.Models.Core; +using Modix.Models.Core; -namespace Modix.Web.Models.Configuration; +namespace Modix.Web.Shared.Models.Configuration; public record DesignatedRoleData(long Id, ulong RoleId, DesignatedRoleType RoleDesignation, string Name); diff --git a/src/Modix.Web/Models/CookieConstants.cs b/src/Modix.Web.Shared/Models/CookieConstants.cs similarity index 100% rename from src/Modix.Web/Models/CookieConstants.cs rename to src/Modix.Web.Shared/Models/CookieConstants.cs diff --git a/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessageBatchInformation.cs b/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessageBatchInformation.cs new file mode 100644 index 000000000..eeaf38f42 --- /dev/null +++ b/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessageBatchInformation.cs @@ -0,0 +1,12 @@ +namespace Modix.Web.Shared.Models.DeletedMessages; + +public record DeletedMessageBatchInformation +( + string ChannelName, + string AuthorUsername, + DateTimeOffset Created, + string CreatedByUsername, + string Content, + string Reason, + long? BatchId +); diff --git a/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessageInformation.cs b/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessageInformation.cs new file mode 100644 index 000000000..e194d77e6 --- /dev/null +++ b/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessageInformation.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.DeletedMessages; + +public record DeletedMessageInformation(ulong MessageId, DateTimeOffset? SentTime, string? Url, string Username, string Content); diff --git a/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessagesQuery.cs b/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessagesQuery.cs new file mode 100644 index 000000000..077a182fd --- /dev/null +++ b/src/Modix.Web.Shared/Models/DeletedMessages/DeletedMessagesQuery.cs @@ -0,0 +1,5 @@ +using MudBlazor; + +namespace Modix.Web.Shared.Models.DeletedMessages; + +public record DeletedMessagesQuery(TableFilter Filter, TableState TableState); diff --git a/src/Modix.Web/Models/DeletedMessages/TableFilter.cs b/src/Modix.Web.Shared/Models/DeletedMessages/TableFilter.cs similarity index 97% rename from src/Modix.Web/Models/DeletedMessages/TableFilter.cs rename to src/Modix.Web.Shared/Models/DeletedMessages/TableFilter.cs index 48157fbfe..f08f0417e 100644 --- a/src/Modix.Web/Models/DeletedMessages/TableFilter.cs +++ b/src/Modix.Web.Shared/Models/DeletedMessages/TableFilter.cs @@ -1,4 +1,4 @@ -namespace Modix.Web.Models.DeletedMessages; +namespace Modix.Web.Shared.Models.DeletedMessages; public class TableFilter { diff --git a/src/Modix.Web.Shared/Models/DiscordUser.cs b/src/Modix.Web.Shared/Models/DiscordUser.cs new file mode 100644 index 000000000..e1635bc6b --- /dev/null +++ b/src/Modix.Web.Shared/Models/DiscordUser.cs @@ -0,0 +1,10 @@ +namespace Modix.Web.Shared.Models; + +public class DiscordUser +{ + public required ulong UserId { get; init; } + public required string Name { get; init; } + public required string AvatarHash { get; init; } + public required ulong CurrentGuild { get; init; } + public required IEnumerable Claims { get; set; } +} diff --git a/src/Modix.Web/Models/GuildOption.cs b/src/Modix.Web.Shared/Models/GuildOption.cs similarity index 63% rename from src/Modix.Web/Models/GuildOption.cs rename to src/Modix.Web.Shared/Models/GuildOption.cs index 7307007f5..fe039b07c 100644 --- a/src/Modix.Web/Models/GuildOption.cs +++ b/src/Modix.Web.Shared/Models/GuildOption.cs @@ -1,3 +1,3 @@ -namespace Modix.Web.Models; +namespace Modix.Web.Shared.Models; public record GuildOption(ulong Id, string Name, string IconUrl); diff --git a/src/Modix.Web.Shared/Models/Infractions/InfractionCreationData.cs b/src/Modix.Web.Shared/Models/Infractions/InfractionCreationData.cs new file mode 100644 index 000000000..1f812b64d --- /dev/null +++ b/src/Modix.Web.Shared/Models/Infractions/InfractionCreationData.cs @@ -0,0 +1,5 @@ +using Modix.Models.Moderation; + +namespace Modix.Web.Shared.Models.Infractions; + +public record InfractionCreationData(InfractionType Type, ulong SubjectId, string Reason, TimeSpan? Duration); diff --git a/src/Modix.Web.Shared/Models/Infractions/InfractionData.cs b/src/Modix.Web.Shared/Models/Infractions/InfractionData.cs new file mode 100644 index 000000000..b0045f2f5 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Infractions/InfractionData.cs @@ -0,0 +1,18 @@ +using Modix.Models.Moderation; + +namespace Modix.Web.Shared.Models.Infractions; + +public record InfractionData( + long Id, + ulong GuildId, + InfractionType Type, + string Reason, + TimeSpan? Duration, + string SubjectName, + string CreatedBy, + DateTimeOffset Created, + bool IsRescinded, + bool IsDeleted, + bool CanBeRescinded, + bool CanBeDeleted +); diff --git a/src/Modix.Web.Shared/Models/Infractions/InfractionsQuery.cs b/src/Modix.Web.Shared/Models/Infractions/InfractionsQuery.cs new file mode 100644 index 000000000..96ec600b3 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Infractions/InfractionsQuery.cs @@ -0,0 +1,4 @@ +using MudBlazor; + +namespace Modix.Web.Shared.Models.Infractions; +public record InfractionsQuery(TableFilter Filter, TableState TableState); diff --git a/src/Modix.Web/Models/Infractions/TableFilter.cs b/src/Modix.Web.Shared/Models/Infractions/TableFilter.cs similarity index 87% rename from src/Modix.Web/Models/Infractions/TableFilter.cs rename to src/Modix.Web.Shared/Models/Infractions/TableFilter.cs index fc7e112ab..27015331c 100644 --- a/src/Modix.Web/Models/Infractions/TableFilter.cs +++ b/src/Modix.Web.Shared/Models/Infractions/TableFilter.cs @@ -1,6 +1,6 @@ -using Modix.Data.Models.Moderation; +using Modix.Models.Moderation; -namespace Modix.Web.Models.Infractions; +namespace Modix.Web.Shared.Models.Infractions; public class TableFilter { @@ -22,13 +22,9 @@ public string? Subject SubjectId = null; } else if (ulong.TryParse(value, out var subjectId)) - { SubjectId = subjectId; - } else - { _subject = value; - } } } @@ -46,13 +42,9 @@ public string? Creator CreatedById = null; } else if (ulong.TryParse(value, out var createdById)) - { CreatedById = createdById; - } else - { _creator = value; - } } } diff --git a/src/Modix.Web/Models/Promotions/CampaignCommentData.cs b/src/Modix.Web.Shared/Models/Promotions/CampaignCommentData.cs similarity index 65% rename from src/Modix.Web/Models/Promotions/CampaignCommentData.cs rename to src/Modix.Web.Shared/Models/Promotions/CampaignCommentData.cs index d245e89a6..27d1fbbe9 100644 --- a/src/Modix.Web/Models/Promotions/CampaignCommentData.cs +++ b/src/Modix.Web.Shared/Models/Promotions/CampaignCommentData.cs @@ -1,5 +1,5 @@ -using Modix.Data.Models.Promotions; +using Modix.Models.Promotions; -namespace Modix.Web.Models.Promotions; +namespace Modix.Web.Shared.Models.Promotions; public record CampaignCommentData(long Id, PromotionSentiment PromotionSentiment, string Content, DateTimeOffset CreatedAt, bool IsFromCurrentUser); diff --git a/src/Modix.Web/Models/Promotions/NextRank.cs b/src/Modix.Web.Shared/Models/Promotions/NextRank.cs similarity index 51% rename from src/Modix.Web/Models/Promotions/NextRank.cs rename to src/Modix.Web.Shared/Models/Promotions/NextRank.cs index 5c58b804a..d60b8210d 100644 --- a/src/Modix.Web/Models/Promotions/NextRank.cs +++ b/src/Modix.Web.Shared/Models/Promotions/NextRank.cs @@ -1,3 +1,3 @@ -namespace Modix.Web.Models.Promotions; +namespace Modix.Web.Shared.Models.Promotions; public record NextRank(string? Name, string Color); diff --git a/src/Modix.Web.Shared/Models/Promotions/PromotionCampaignData.cs b/src/Modix.Web.Shared/Models/Promotions/PromotionCampaignData.cs new file mode 100644 index 000000000..5b98207de --- /dev/null +++ b/src/Modix.Web.Shared/Models/Promotions/PromotionCampaignData.cs @@ -0,0 +1,18 @@ +using Modix.Models.Promotions; + +namespace Modix.Web.Shared.Models.Promotions; + +public record PromotionCampaignData +{ + public required long Id { get; init; } + public required ulong SubjectId { get; init; } + public required string SubjectName { get; init; } + public required ulong TargetRoleId { get; init; } + public required string TargetRoleName { get; init; } + public required PromotionCampaignOutcome? Outcome { get; set; } + public required DateTimeOffset Created { get; init; } + public required bool IsCurrentUserCampaign { get; init; } + public required int ApproveCount { get; init; } + public required int OpposeCount { get; init; } + public required bool IsClosed { get; init; } +} diff --git a/src/Modix.Web.Shared/Models/Promotions/PromotionCreationData.cs b/src/Modix.Web.Shared/Models/Promotions/PromotionCreationData.cs new file mode 100644 index 000000000..43cf035be --- /dev/null +++ b/src/Modix.Web.Shared/Models/Promotions/PromotionCreationData.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.Promotions; + +public record PromotionCreationData(ulong UserId, string? Comment); diff --git a/src/Modix.Web/Models/SessionState.cs b/src/Modix.Web.Shared/Models/SessionState.cs similarity index 100% rename from src/Modix.Web/Models/SessionState.cs rename to src/Modix.Web.Shared/Models/SessionState.cs diff --git a/src/Modix.Web.Shared/Models/Stats/GuildRoleMemberCount.cs b/src/Modix.Web.Shared/Models/Stats/GuildRoleMemberCount.cs new file mode 100644 index 000000000..9b2830d66 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Stats/GuildRoleMemberCount.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.Stats; + +public record GuildRoleMemberCount(string Name, int Count, string Color); diff --git a/src/Modix.Web.Shared/Models/Stats/GuildStatData.cs b/src/Modix.Web.Shared/Models/Stats/GuildStatData.cs new file mode 100644 index 000000000..78ed0f649 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Stats/GuildStatData.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.Stats; + +public record GuildStatData(string GuildName, IReadOnlyCollection GuildRoleCounts, IReadOnlyCollection TopUserMessageCounts); diff --git a/src/Modix.Web.Shared/Models/Stats/PerUserMessageCount.cs b/src/Modix.Web.Shared/Models/Stats/PerUserMessageCount.cs new file mode 100644 index 000000000..dd553d2e9 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Stats/PerUserMessageCount.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.Stats; + +public record PerUserMessageCount(string Username, string Discriminator, int Rank, int MessageCount, bool IsCurrentUser); diff --git a/src/Modix.Web.Shared/Models/Tags/TagCreationData.cs b/src/Modix.Web.Shared/Models/Tags/TagCreationData.cs new file mode 100644 index 000000000..adccc14f8 --- /dev/null +++ b/src/Modix.Web.Shared/Models/Tags/TagCreationData.cs @@ -0,0 +1,3 @@ +namespace Modix.Web.Shared.Models.Tags; + +public record TagCreationData(string? Name, string? Content); diff --git a/src/Modix.Web.Shared/Models/Tags/TagData.cs b/src/Modix.Web.Shared/Models/Tags/TagData.cs new file mode 100644 index 000000000..25924a8ce --- /dev/null +++ b/src/Modix.Web.Shared/Models/Tags/TagData.cs @@ -0,0 +1,11 @@ +namespace Modix.Web.Shared.Models.Tags; + +public record TagData( + string Name, + DateTimeOffset Created, + bool IsOwnedByRole, + ulong OwnerId, + string OwnerName, + string Content, + uint Uses, + bool CanMaintain); diff --git a/src/Modix.Web/Models/UserLookup/MessageCountPerChannelInformation.cs b/src/Modix.Web.Shared/Models/UserLookup/MessageCountPerChannelInformation.cs similarity index 66% rename from src/Modix.Web/Models/UserLookup/MessageCountPerChannelInformation.cs rename to src/Modix.Web.Shared/Models/UserLookup/MessageCountPerChannelInformation.cs index af3d869cc..4775edca6 100644 --- a/src/Modix.Web/Models/UserLookup/MessageCountPerChannelInformation.cs +++ b/src/Modix.Web.Shared/Models/UserLookup/MessageCountPerChannelInformation.cs @@ -1,3 +1,3 @@ -namespace Modix.Web.Models.UserLookup; +namespace Modix.Web.Shared.Models.UserLookup; public record MessageCountPerChannelInformation(string ChannelName, double Count, string Color); diff --git a/src/Modix.Web.Shared/Models/UserLookup/UserInformation.cs b/src/Modix.Web.Shared/Models/UserLookup/UserInformation.cs new file mode 100644 index 000000000..67bc08498 --- /dev/null +++ b/src/Modix.Web.Shared/Models/UserLookup/UserInformation.cs @@ -0,0 +1,24 @@ +using Modix.Web.Shared.Models.Common; + +namespace Modix.Web.Shared.Models.UserLookup; + +public record UserInformation( + string Id, + string? Username, + string? Nickname, + string? Discriminator, + string? AvatarUrl, + DateTimeOffset CreatedAt, + DateTimeOffset? JoinedAt, + DateTimeOffset? FirstSeen, + DateTimeOffset? LastSeen, + int Rank, + int Last7DaysMessages, + int Last30DaysMessages, + decimal AverageMessagesPerDay, + int Percentile, + IEnumerable Roles, + bool IsBanned, + string? BanReason, + bool IsGuildMember +); diff --git a/src/Modix.Web.Shared/Modix.Web.Shared.csproj b/src/Modix.Web.Shared/Modix.Web.Shared.csproj new file mode 100644 index 000000000..680ff4000 --- /dev/null +++ b/src/Modix.Web.Shared/Modix.Web.Shared.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/Modix.Web.Shared/Services/ICookieService.cs b/src/Modix.Web.Shared/Services/ICookieService.cs new file mode 100644 index 000000000..c5452785c --- /dev/null +++ b/src/Modix.Web.Shared/Services/ICookieService.cs @@ -0,0 +1,10 @@ +namespace Modix.Web.Shared.Services; + +public interface ICookieService +{ + Task SetSelectedGuildAsync(ulong guildId); + Task SetShowDeletedInfractionsAsync(bool showDeleted); + Task SetShowInfractionStateAsync(bool showInfractionState); + Task SetShowInactivePromotionsAsync(bool showInactivePromotions); + Task SetUseDarkModeAsync(bool useDarkMode); +} diff --git a/src/Modix.Web/Components/AnchorNavigation.razor b/src/Modix.Web.Wasm/Components/AnchorNavigation.razor similarity index 100% rename from src/Modix.Web/Components/AnchorNavigation.razor rename to src/Modix.Web.Wasm/Components/AnchorNavigation.razor diff --git a/src/Modix.Web/Components/AutoComplete.razor b/src/Modix.Web.Wasm/Components/AutoComplete.razor similarity index 96% rename from src/Modix.Web/Components/AutoComplete.razor rename to src/Modix.Web.Wasm/Components/AutoComplete.razor index ae430f952..0391a4587 100644 --- a/src/Modix.Web/Components/AutoComplete.razor +++ b/src/Modix.Web.Wasm/Components/AutoComplete.razor @@ -1,5 +1,4 @@ @using Modix.Web.Models; -@using Modix.Web.Services; @using MudBlazor @typeparam T diff --git a/src/Modix.Web/Components/AutoComplete.razor.cs b/src/Modix.Web.Wasm/Components/AutoComplete.razor.cs similarity index 91% rename from src/Modix.Web/Components/AutoComplete.razor.cs rename to src/Modix.Web.Wasm/Components/AutoComplete.razor.cs index e9d40f651..85c7015db 100644 --- a/src/Modix.Web/Components/AutoComplete.razor.cs +++ b/src/Modix.Web.Wasm/Components/AutoComplete.razor.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Components; -using Modix.Web.Models.Common; +using Modix.Web.Shared.Models.Common; -namespace Modix.Web.Components; +namespace Modix.Web.Wasm.Components; public partial class AutoComplete where T : IAutoCompleteItem { diff --git a/src/Modix.Web.Wasm/Components/Configuration/Channels.razor b/src/Modix.Web.Wasm/Components/Configuration/Channels.razor new file mode 100644 index 000000000..7ce209950 --- /dev/null +++ b/src/Modix.Web.Wasm/Components/Configuration/Channels.razor @@ -0,0 +1,179 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models.Core +@using Modix.Web.Shared.Models.Configuration +@using Modix.Web.Wasm.Components +@using MudBlazor +@using Humanizer; +@using Modix.Web.Shared.Models.Common; + +Modix - Channels +Channel Designations + +@if (DesignatedChannelMappings is not null && DesignatedChannelTypes is not null) +{ + + + Assign a Channel + + + + Designation + + @foreach (var designation in DesignatedChannelTypes) + { + + } + + + + + Assign + + + Cancel + + + + + + @foreach (var designatedChannelType in DesignatedChannelTypes.OrderBy(x => x.ToString())) + { + +
+
+ + @designatedChannelType.ToString().Titleize() + + @if (!DesignatedChannelMappings.TryGetValue(designatedChannelType, out var channelDesignations) || !channelDesignations.Any()) + { + + NONE ASSIGNED + + } + else + { + @foreach (var designatedChannelMapping in channelDesignations) + { + + } + } +
+ +
+ + + +
+
+
+ + } +
+
+} + +@code { + [Inject] + public required ISnackbar Snackbar { get; set; } + + [Inject] + public required IHttpClientFactory HttpClientFactory { get; set; } + + private Dictionary>? DesignatedChannelMappings { get; set; } + private DesignatedChannelType[]? DesignatedChannelTypes { get; set; } + + private bool _createDialogVisible; + private DesignatedChannelType? _selectedDesignatedChannelType; + private ChannelInformation? _selectedChannel; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + using var client = HttpClientFactory.CreateClient("api"); + DesignatedChannelMappings = await client.GetFromJsonAsync>>("api/config/channels"); + + DesignatedChannelTypes = Enum.GetValues(); + + StateHasChanged(); + } + + private async Task> AutoCompleteChannels(string query) + { + using var client = HttpClientFactory.CreateClient("api"); + + var escapedQuery = Uri.EscapeDataString(query); + var channels = await client.GetFromJsonAsync($"api/autocomplete/channels/{escapedQuery}"); + + return channels ?? []; + } + + public void ToggleCreateDialog() + { + _createDialogVisible = !_createDialogVisible; + if (_createDialogVisible) + { + _selectedChannel = null; + _selectedDesignatedChannelType = null; + } + } + + private void SelectedChannelChanged(ChannelInformation channel) + { + _selectedChannel = channel; + } + + public async Task SaveDesignation() + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PutAsync($"api/config/channels/{_selectedChannel.Id}/{_selectedDesignatedChannelType.Value}", default); + + _createDialogVisible = false; + + if(!response.IsSuccessStatusCode) + { + var errorMessage = await response.Content.ReadAsStringAsync(); + Snackbar.Add(errorMessage, Severity.Error); + + return; + } + + var id = long.Parse(await response.Content.ReadAsStringAsync()); + + if (!DesignatedChannelMappings!.ContainsKey(_selectedDesignatedChannelType.Value)) + { + DesignatedChannelMappings[_selectedDesignatedChannelType.Value] = new List(); + } + + DesignatedChannelMappings[_selectedDesignatedChannelType.Value].Add(new DesignatedChannelData(id, _selectedChannel.Id, _selectedDesignatedChannelType.Value, _selectedChannel.Name)); + + Snackbar.Add($"Added designation '{_selectedDesignatedChannelType}' to channel '{_selectedChannel.Name}'", Severity.Success); + } + + public async Task RemoveDesignation(long id, DesignatedChannelType designatedChannelType) + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.DeleteAsync($"api/config/channels/{id}"); + + response.EnsureSuccessStatusCode(); + + var channelMappingsWithType = DesignatedChannelMappings![designatedChannelType]; + var removedChannelMapping = channelMappingsWithType.First(x => x.Id == id); + + channelMappingsWithType.Remove(removedChannelMapping); + + Snackbar.Add($"Removed designation '{designatedChannelType}' from channel '{removedChannelMapping.Name}'", Severity.Success); + } +} diff --git a/src/Modix.Web/Components/Configuration/Claims.razor b/src/Modix.Web.Wasm/Components/Configuration/Claims.razor similarity index 70% rename from src/Modix.Web/Components/Configuration/Claims.razor rename to src/Modix.Web.Wasm/Components/Configuration/Claims.razor index c7192a595..46ca4105d 100644 --- a/src/Modix.Web/Components/Configuration/Claims.razor +++ b/src/Modix.Web.Wasm/Components/Configuration/Claims.razor @@ -1,13 +1,10 @@ -@using Modix.Data.Models.Core; -@using Modix.Data.Repositories; -@using Modix.Data.Utilities; -@using Modix.Models.Core; -@using Modix.Web.Models.UserLookup; -@using Modix.Web.Services; +@using Modix.Models.Core; +@using Modix.Models.Utilities +@using Modix.Web.Shared.Models.Common @using MudBlazor @using System.Reflection; @using Humanizer; -@using Modix.Web.Models.Common; +@using Modix.Web.Shared.Models.Configuration; Modix - Claims @@ -76,19 +73,13 @@ @code { [Inject] - public IClaimMappingRepository ClaimMappingRepository { get; set; } = null!; + public required IHttpClientFactory HttpClientFactory { get; set; } [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; - - [Inject] - public Modix.Services.Core.IAuthorizationService AuthorizationService { get; set; } = null!; - - [Inject] - public ISnackbar Snackbar { get; set; } = null!; + public required ISnackbar Snackbar { get; set; } private Dictionary? ClaimData { get; set; } - private Dictionary<(ulong?, AuthorizationClaim), ClaimMappingBrief>? MappedClaims { get; set; } + private Dictionary<(ulong?, AuthorizationClaim), ClaimMappingData>? MappedClaims { get; set; } private Dictionary? Roles { get; set; } private ulong? _selectedRole; @@ -98,37 +89,18 @@ if (!firstRender) return; - ClaimData = typeof(AuthorizationClaim).GetFields(BindingFlags.Public | BindingFlags.Static).ToDictionary - ( - d => (AuthorizationClaim)d.GetValue(null)!, - d => - { - var claimInfo = (ClaimInfoAttribute)d.GetCustomAttributes(typeof(ClaimInfoAttribute), true).First()!; + ClaimData = ClaimInfoData.GetClaims(); - return new ClaimInfoData - { - Name = d.Name, - Description = claimInfo.Description, - Category = claimInfo.Category - }; - } - ); + using var client = HttpClientFactory.CreateClient("api"); - var currentGuild = DiscordHelper.GetUserGuild(); - var mappedClaims = await ClaimMappingRepository.SearchBriefsAsync(new ClaimMappingSearchCriteria - { - IsDeleted = false, - GuildId = currentGuild.Id - }); + var rolesTask = client.GetFromJsonAsync>("api/roles"); - MappedClaims = mappedClaims.ToDictionary(x => (x.RoleId, x.Claim), x => x); + var mappedClaims = await client.GetFromJsonAsync("api/config/claims"); + MappedClaims = mappedClaims?.ToDictionary(x => (x.RoleId, x.Claim), x => x); - Roles = currentGuild.Roles - .Select(d => new RoleInformation(d.Id, d.Name, d.Color.ToString())) - .OrderBy(x => x.Name) - .ToDictionary(x => x.Id, x => x); + Roles = await rolesTask; - _selectedRole = Roles.First().Key; + _selectedRole = Roles?.First().Key; StateHasChanged(); } @@ -139,22 +111,22 @@ if (MappedClaims!.TryGetValue(key, out var claimMapping) && claimMapping.Type == claimMappingType) return; - await AuthorizationService.ModifyClaimMappingAsync(roleId, authorizationClaim, claimMappingType); + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PatchAsync($"api/config/claims/{roleId}/{authorizationClaim}/{claimMappingType}", default); + + response.EnsureSuccessStatusCode(); + if (claimMappingType is ClaimMappingType.Denied or ClaimMappingType.Granted) { Snackbar.Add($"Claim '{authorizationClaim}' for '{Roles![roleId].Name}' was changed to '{claimMappingType}'", Severity.Success); if (claimMapping is null) { - MappedClaims[key] = new ClaimMappingBrief - { - Claim = authorizationClaim, - RoleId = roleId, - Type = claimMappingType.Value - }; + MappedClaims[key] = new ClaimMappingData(roleId, authorizationClaim, claimMappingType.Value); } else { - claimMapping.Type = claimMappingType.Value; + var clone = claimMapping with { Type = claimMappingType.Value }; + MappedClaims[key] = clone; } } else diff --git a/src/Modix.Web.Wasm/Components/Configuration/IndividualDesignation.razor b/src/Modix.Web.Wasm/Components/Configuration/IndividualDesignation.razor new file mode 100644 index 000000000..82ef7baf6 --- /dev/null +++ b/src/Modix.Web.Wasm/Components/Configuration/IndividualDesignation.razor @@ -0,0 +1,63 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using MudBlazor + + + + @NamePrefix@Name + + + @if (!_showConfirm) + { + + X + + } + else + { + Remove Designation? + + + Yes + + + No + + } + + + +@code { + [Parameter, EditorRequired] + public EventCallback RemoveDesignation { get; set; } + + [Parameter, EditorRequired] + public string? AuthorizationRoleForDelete { get; set; } + + [Parameter, EditorRequired] + public string? NamePrefix { get; set; } + + [Parameter, EditorRequired] + public string? Name { get; set; } + + [Parameter, EditorRequired] + public long Id { get; set; } + + private bool _showConfirm; +} diff --git a/src/Modix.Web.Wasm/Components/Configuration/Roles.razor b/src/Modix.Web.Wasm/Components/Configuration/Roles.razor new file mode 100644 index 000000000..9b3433e2d --- /dev/null +++ b/src/Modix.Web.Wasm/Components/Configuration/Roles.razor @@ -0,0 +1,179 @@ +@using Humanizer +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models.Core +@using Modix.Web.Shared.Models.Common +@using Modix.Web.Shared.Models.Configuration +@using MudBlazor + +Modix - Roles +Role Designations + +@if (DesignatedRoleMappings is not null && DesignatedRoleTypes is not null) +{ + + + Assign a Role + + + + + Designation + + @foreach (var designation in DesignatedRoleTypes) + { + + } + + + + + Assign + + + Cancel + + + + + + @foreach (var designatedRoleType in DesignatedRoleTypes.OrderBy(x => x.ToString())) + { + +
+
+ + @designatedRoleType.ToString().Titleize() + + @if (!DesignatedRoleMappings.TryGetValue(designatedRoleType, out var roleDesignations) || !roleDesignations.Any()) + { + + NONE ASSIGNED + + } + else + { + @foreach (var designatedRoleMapping in roleDesignations) + { + + } + } +
+ +
+ + + +
+
+
+ + } +
+
+} + +@code { + [Inject] + public required ISnackbar Snackbar { get; set; } + + [Inject] + public required IHttpClientFactory HttpClientFactory { get; set; } + + private Dictionary>? DesignatedRoleMappings { get; set; } + private DesignatedRoleType[]? DesignatedRoleTypes { get; set; } + + private bool _createDialogVisible; + private DesignatedRoleType? _selectedDesignatedRoleType; + private RoleInformation? _selectedRole; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + + using var client = HttpClientFactory.CreateClient("api"); + DesignatedRoleMappings = await client.GetFromJsonAsync>>("api/config/roles"); + + DesignatedRoleTypes = Enum.GetValues(); + + StateHasChanged(); + } + + public void ToggleCreateDialog() + { + _createDialogVisible = !_createDialogVisible; + if (_createDialogVisible) + { + _selectedRole = null; + _selectedDesignatedRoleType = null; + } + } + + private async Task> AutoCompleteRoles(string query) + { + using var client = HttpClientFactory.CreateClient("api"); + var roles = await client.GetFromJsonAsync($"api/autocomplete/roles/{query}"); + + return roles ?? []; + } + + private void SelectedRoleChanged(RoleInformation role) + { + _selectedRole = role; + } + + public async Task SaveDesignation() + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PutAsync($"api/config/roles/{_selectedRole.Id}/{_selectedDesignatedRoleType.Value}", default); + + _createDialogVisible = false; + + if (!response.IsSuccessStatusCode) + { + var errorMessage = await response.Content.ReadAsStringAsync(); + Snackbar.Add(errorMessage, Severity.Error); + + return; + } + + var id = long.Parse(await response.Content.ReadAsStringAsync()); + + if (!DesignatedRoleMappings!.ContainsKey(_selectedDesignatedRoleType.Value)) + { + DesignatedRoleMappings[_selectedDesignatedRoleType.Value] = new List(); + } + + DesignatedRoleMappings[_selectedDesignatedRoleType.Value].Add(new DesignatedRoleData(id, _selectedRole.Id, _selectedDesignatedRoleType.Value, _selectedRole.Name)); + + Snackbar.Add($"Added designation '{_selectedDesignatedRoleType}' to role '{_selectedRole.Name}'", Severity.Success); + } + + public async Task RemoveDesignation(long id, DesignatedRoleType designatedRoleType) + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.DeleteAsync($"api/config/roles/{id}"); + + response.EnsureSuccessStatusCode(); + + var roleMappingsWithType = DesignatedRoleMappings![designatedRoleType]; + var removedRoleMapping = roleMappingsWithType.First(x => x.Id == id); + + roleMappingsWithType.Remove(removedRoleMapping); + + Snackbar.Add($"Removed designation '{designatedRoleType}' from role '{removedRoleMapping.Name}'", Severity.Success); + } +} diff --git a/src/Modix.Web/Components/ConfirmationDialog.razor b/src/Modix.Web.Wasm/Components/ConfirmationDialog.razor similarity index 92% rename from src/Modix.Web/Components/ConfirmationDialog.razor rename to src/Modix.Web.Wasm/Components/ConfirmationDialog.razor index 4fd7aba01..2c929c5f1 100644 --- a/src/Modix.Web/Components/ConfirmationDialog.razor +++ b/src/Modix.Web.Wasm/Components/ConfirmationDialog.razor @@ -1,5 +1,4 @@ -@using Modix.Data.Models.Promotions; -@using MudBlazor +@using MudBlazor diff --git a/src/Modix.Web/Components/CreateCampaignComment.razor b/src/Modix.Web.Wasm/Components/CreateCampaignComment.razor similarity index 95% rename from src/Modix.Web/Components/CreateCampaignComment.razor rename to src/Modix.Web.Wasm/Components/CreateCampaignComment.razor index 1d520e2cd..9b60c6368 100644 --- a/src/Modix.Web/Components/CreateCampaignComment.razor +++ b/src/Modix.Web.Wasm/Components/CreateCampaignComment.razor @@ -1,4 +1,5 @@ -@using Modix.Data.Models.Promotions; +@using Modix.Models.Promotions +@using Modix.Web.Shared.Models.Promotions @using MudBlazor;
diff --git a/src/Modix.Web/Components/EditPromotionCommentDialog.razor b/src/Modix.Web.Wasm/Components/EditPromotionCommentDialog.razor similarity index 95% rename from src/Modix.Web/Components/EditPromotionCommentDialog.razor rename to src/Modix.Web.Wasm/Components/EditPromotionCommentDialog.razor index 33a7f599f..3f72fe9b9 100644 --- a/src/Modix.Web/Components/EditPromotionCommentDialog.razor +++ b/src/Modix.Web.Wasm/Components/EditPromotionCommentDialog.razor @@ -1,4 +1,5 @@ -@using Modix.Data.Models.Promotions; +@using Modix.Models.Promotions +@using Modix.Web.Shared.Models.Promotions @using MudBlazor diff --git a/src/Modix.Web/Components/DeletedMessages.razor b/src/Modix.Web.Wasm/Components/Infractions/DeletedMessages.razor similarity index 57% rename from src/Modix.Web/Components/DeletedMessages.razor rename to src/Modix.Web.Wasm/Components/Infractions/DeletedMessages.razor index d9055cd97..5fe136d75 100644 --- a/src/Modix.Web/Components/DeletedMessages.razor +++ b/src/Modix.Web.Wasm/Components/Infractions/DeletedMessages.razor @@ -1,12 +1,6 @@ -@using Discord.WebSocket; -@using Discord; -@using Modix.Data.Models.Moderation; -@using Modix.Data.Models; -@using Modix.Services.Moderation; -@using Modix.Web.Models.DeletedMessages; -@using Modix.Web.Services; +@using Modix.Models +@using Modix.Web.Shared.Models.DeletedMessages @using MudBlazor -@using Modix.Services.Utilities Modix - Deletions @@ -73,45 +67,45 @@ - + Refresh - Channel + Channel - Author + Author - Deleted On + Deleted On - Deleted By + Deleted By - Content + Content - Reason + Reason - Batch ID + Batch ID Actions - #@deletedMessage.Channel.Name - @deletedMessage.Author.GetFullUsername() + #@deletedMessage.ChannelName + @deletedMessage.AuthorUsername @deletedMessage.Created - @deletedMessage.CreatedBy.GetFullUsername() + @deletedMessage.CreatedByUsername @@ -137,15 +131,12 @@ @code { [Inject] - public ModerationService ModerationService { get; set; } = null!; + public required IHttpClientFactory HttpClientFactory { get; set; } [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; + public required ISnackbar Snackbar { get; set; } - [Inject] - public ISnackbar Snackbar { get; set; } = null!; - - private MudTable? TableRef; + private MudTable? TableRef; private Dictionary> DeletedMessagesContext { get; } = new Dictionary>(); private bool _deletedMessagesContextDialogVisible; private long _currentContext; @@ -179,45 +170,18 @@ private void CloseDialog() => _deletedMessagesContextDialogVisible = false; - private async Task> LoadDeletedMessages(TableState tableState) + private async Task> LoadDeletedMessages(TableState tableState) { - var currentGuild = DiscordHelper.GetUserGuild(); + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PutAsJsonAsync("api/deletedmessages", new DeletedMessagesQuery(_tableFilter, tableState)); - var searchCriteria = new DeletedMessageSearchCriteria - { - GuildId = currentGuild.Id, - Channel = _tableFilter.Channel, - ChannelId = _tableFilter.ChannelId, - Author = _tableFilter.Author, - AuthorId = _tableFilter.AuthorId, - CreatedBy = _tableFilter.CreatedBy, - CreatedById = _tableFilter.CreatedById, - Content = _tableFilter.Content, - Reason = _tableFilter.Reason, - BatchId = _tableFilter.BatchId - }; + response.EnsureSuccessStatusCode(); - var result = await ModerationService.SearchDeletedMessagesAsync(searchCriteria, - new[] - { - new SortingCriteria - { - PropertyName = tableState.SortLabel ?? nameof(DeletedMessageSummary.Created), - Direction = tableState.SortDirection == MudBlazor.SortDirection.Ascending - ? Data.Models.SortDirection.Ascending - : Data.Models.SortDirection.Descending - } - }, - new PagingCriteria - { - FirstRecordIndex = tableState.Page * tableState.PageSize, - PageSize = tableState.PageSize, - } - ); + var result = await response.Content.ReadFromJsonAsync>(); - return new TableData + return new TableData { - TotalItems = (int)result.FilteredRecordCount, + TotalItems = (int)result.TotalRecordCount, Items = result.Records }; } @@ -229,53 +193,21 @@ if (DeletedMessagesContext.ContainsKey(batchId)) return; - var deletedMessages = await ModerationService.SearchDeletedMessagesAsync( - new DeletedMessageSearchCriteria - { - BatchId = batchId - }, - new SortingCriteria[] - { - //Sort ascending, so the earliest message is first - new SortingCriteria { PropertyName = nameof(DeletedMessageSummary.MessageId), Direction = Data.Models.SortDirection.Ascending } - }, - new PagingCriteria() - ); - - var firstMessage = deletedMessages.Records.FirstOrDefault(); + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.GetAsync($"api/deletedmessages/{batchId}"); - if (firstMessage is null) + if(!response.IsSuccessStatusCode) { CloseDialog(); - Snackbar.Add($"Couldn't find messages for batch id {batchId}", Severity.Error); - return; - } - var currentUser = DiscordHelper.GetCurrentUser(); - var batchChannelId = deletedMessages.Records.First().Channel.Id; - if (currentUser!.Guild.GetChannel(batchChannelId) is not ISocketMessageChannel foundChannel) - { - CloseDialog(); - Snackbar.Add($"Couldn't recreate context - text channel with id {batchChannelId} not found", Severity.Error); - return; - } + var errorMessage = await response.Content.ReadAsStringAsync(); + Snackbar.Add(errorMessage, Severity.Error); - if (currentUser.GetPermissions(foundChannel as IGuildChannel).ReadMessageHistory == false) - { - CloseDialog(); - Snackbar.Add($"You don't have read permissions for the channel this batch was deleted in (#{foundChannel.Name})", Severity.Error); return; } - var beforeMessages = await foundChannel.GetMessagesAsync(firstMessage.MessageId, Discord.Direction.Before, 25).FlattenAsync(); - var afterMessages = await foundChannel.GetMessagesAsync(firstMessage.MessageId, Discord.Direction.After, 25 + (int)deletedMessages.FilteredRecordCount).FlattenAsync(); - - var allMessages = new List(); - allMessages.AddRange(deletedMessages.Records.Select(d => new DeletedMessageInformation(d.MessageId, null, null, d.Author.GetFullUsername(), d.Content))); - allMessages.AddRange(beforeMessages.Select(d => DeletedMessageInformation.FromIMessage(d))); - allMessages.AddRange(afterMessages.Select(d => DeletedMessageInformation.FromIMessage(d))); - - DeletedMessagesContext[batchId] = allMessages.OrderBy(d => d.MessageId).ToList(); + var allMessages = await response.Content.ReadFromJsonAsync(); + DeletedMessagesContext[batchId] = allMessages!.OrderBy(d => d.MessageId).ToList(); } } diff --git a/src/Modix.Web.Wasm/Components/Infractions/Infractions.razor b/src/Modix.Web.Wasm/Components/Infractions/Infractions.razor new file mode 100644 index 000000000..af70a14c7 --- /dev/null +++ b/src/Modix.Web.Wasm/Components/Infractions/Infractions.razor @@ -0,0 +1,437 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models +@using Modix.Models.Core +@using Modix.Web.Models; +@using Modix.Web.Shared.Models.Common +@using Modix.Web.Shared.Models.Infractions +@using Modix.Web.Shared.Services +@using Modix.Web.Wasm.Components +@using MudBlazor; +@using System.Security.Claims; +@using Modix.Models.Moderation; + +Modix - Infractions + + + + Create Infraction + + + + + + @user.Name + + + + Infraction +
+
+ + + + + + + + + + + + + + +
+ +
+ + @if (_infractionType == InfractionType.Mute) + { + Duration +
+ + + + + +
+ } +
+ + + Save + + + Cancel + +
+ + + +
+
+ Create + Refresh +
+ +
+ + +
+
+ + + + + Id + + + + Type + + @foreach (var infractionType in Enum.GetValues()) + { + + } + + + + Created On + + + Subject + + + + Creator + + + Reason + @if (_showState) + { + State + } + @if (_canDeleteInfractions || _canRescind) + { + Actions + } + + + @infraction.Id + @infraction.Type + @infraction.Created.ToString("MM/dd/yy, h:mm:ss tt") + @infraction.SubjectName + @infraction.CreatedBy + @infraction.Reason + @if (_showState) + { + if(!infraction.IsRescinded && !infraction.IsDeleted) + { + Active + } + else if (infraction.IsDeleted) + { + Deleted + } + else + { + Rescinded + } + } + @if (_canDeleteInfractions || _canRescind) + { + +
+ @if (infraction.CanBeDeleted) + { + Delete + } + @if (infraction.CanBeRescinded) + { + Rescind + } +
+
+ } +
+ + + +
+
+ +@code { + + [Parameter] + public string? Subject { get; set; } + + [Parameter] + public string? Id { get; set; } + + [Inject] + public required ISnackbar Snackbar { get; set; } + + [Inject] + public required IDialogService DialogService { get; set; } + + [Inject] + public required IHttpClientFactory HttpClientFactory { get; set; } + + [Inject] + public required ICookieService CookieService { get; set; } + + [CascadingParameter] + public required SessionState SessionState { get; set; } + + [CascadingParameter] + private Task? AuthState { get; set; } + + private MudTable? TableRef; + + private bool _showState; + + private bool _canRescind; + private bool _canDeleteInfractions; + + private ModixUser? _selectedUser; + private InfractionType _infractionType = InfractionType.Notice; + private string? _infractionReason; + private bool _createDialogVisible; + private int? _newInfractionMonths; + private int? _newInfractionDays; + private int? _newInfractionHours; + private int? _newInfractionMinutes; + private int? _newInfractionSeconds; + + private TableFilter _tableFilter = new(); + + protected override void OnInitialized() + { + _tableFilter.ShowDeleted = SessionState.ShowDeletedInfractions; + _showState = SessionState.ShowInfractionState; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + if (AuthState is null) + return; + + var auth = await AuthState; + + _canRescind = auth.User.HasClaim(ClaimTypes.Role, nameof(AuthorizationClaim.ModerationRescind)); + _canDeleteInfractions = auth.User.HasClaim(ClaimTypes.Role, nameof(AuthorizationClaim.ModerationDeleteInfraction)); + + _tableFilter.Subject = Subject; + _tableFilter.IdString = Id; + } + + private async Task ShowStateChanged(bool showState) + { + _showState = showState; + await CookieService.SetShowInfractionStateAsync(showState); + } + + private async Task ShowDeletedChanged(bool showDeleted) + { + await FilterChanged(() => _tableFilter.ShowDeleted = showDeleted); + await CookieService.SetShowDeletedInfractionsAsync(showDeleted); + } + + private void ToggleDialog() + { + _createDialogVisible = !_createDialogVisible; + } + + private void SelectedUserChanged(ModixUser user) + { + _selectedUser = user; + } + + private async Task FilterChanged(Action filterSetter) + { + filterSetter(); + await RefreshTable(); + } + + private async Task RescindInfraction(InfractionData infraction) + { + try + { + var dialogParams = new DialogParameters + { + { x => x.Content, $"Are you sure you want to rescind infraction #{infraction.Id}?"} + }; + + var dialog = DialogService.Show("", dialogParams); + var result = await dialog.Result; + + if (result.Canceled) + { + Snackbar.Add("Action was cancelled", Severity.Info); + return; + } + + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PostAsync($"api/infractions/rescind/{infraction.Id}", default); + + response.EnsureSuccessStatusCode(); + + await RefreshTable(); + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + } + } + + private async Task DeleteInfraction(InfractionData infraction) + { + try + { + var dialogParams = new DialogParameters + { + { x => x.Content, $"Are you sure you want to delete infraction #{infraction.Id}?"} + }; + + var dialog = DialogService.Show("", dialogParams); + var result = await dialog.Result; + + if (result.Canceled) + { + Snackbar.Add("Action was cancelled", Severity.Info); + return; + } + + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.DeleteAsync($"api/infractions/{infraction.Id}"); + + response.EnsureSuccessStatusCode(); + + await RefreshTable(); + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + } + } + + private async Task SaveInfraction() + { + _createDialogVisible = false; + + var duration = GetTimeSpan( + _newInfractionMonths, + _newInfractionDays, + _newInfractionHours, + _newInfractionMinutes, + _newInfractionSeconds); + + try + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PostAsJsonAsync("api/infractions/create", new InfractionCreationData(_infractionType, _selectedUser!.UserId, _infractionReason!, duration)); + + response.EnsureSuccessStatusCode(); + + Snackbar.Add($"Added infraction for user {_selectedUser!.Name}", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + return; + } + + _selectedUser = null; + _newInfractionMonths = null; + _newInfractionDays = null; + _newInfractionHours = null; + _newInfractionMinutes = null; + _newInfractionSeconds = null; + _infractionReason = null; + + await RefreshTable(); + + TimeSpan? GetTimeSpan(int? months, int? days, int? hours, int? minutes, int? seconds) + { + if (months is null + && days is null + && hours is null + && minutes is null + && seconds is null) + return null; + + var now = DateTimeOffset.UtcNow; + var daysInMonth = DateTime.DaysInMonth(now.Year, now.Month); + + var monthSpan = months is null + ? TimeSpan.Zero + : TimeSpan.FromDays(months.Value * daysInMonth); + + var daySpan = days is null + ? TimeSpan.Zero + : TimeSpan.FromDays(days.Value); + + var hourSpan = hours is null + ? TimeSpan.Zero + : TimeSpan.FromHours(hours.Value); + + var minuteSpan = minutes is null + ? TimeSpan.Zero + : TimeSpan.FromMinutes(minutes.Value); + + var secondSpan = seconds is null + ? TimeSpan.Zero + : TimeSpan.FromSeconds(seconds.Value); + + return monthSpan + daySpan + hourSpan + minuteSpan + secondSpan; + } + } + + private async Task RefreshTable() + { + if (TableRef is null) + return; + + await TableRef.ReloadServerData(); + } + + private async Task> LoadInfractions(TableState tableState) + { + + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PutAsJsonAsync("api/infractions", new InfractionsQuery(_tableFilter, tableState)); + response.EnsureSuccessStatusCode(); + + var infractions = await response.Content.ReadFromJsonAsync>(); + + return new TableData + { + Items = infractions.Records, + TotalItems = (int)infractions.TotalRecordCount + }; + } + + private async Task> AutoCompleteAsync(string query) + { + using var client = HttpClientFactory.CreateClient("api"); + + var users = await client.GetFromJsonAsync($"api/autocomplete/users/{query}"); + + return users ?? []; + } +} diff --git a/src/Modix.Web/Shared/MiniUser.razor b/src/Modix.Web.Wasm/Components/MiniUser.razor similarity index 64% rename from src/Modix.Web/Shared/MiniUser.razor rename to src/Modix.Web.Wasm/Components/MiniUser.razor index 73d1bc300..f6734622c 100644 --- a/src/Modix.Web/Shared/MiniUser.razor +++ b/src/Modix.Web.Wasm/Components/MiniUser.razor @@ -1,8 +1,9 @@ -@using AspNet.Security.OAuth.Discord; -@using Discord.WebSocket; -@using Modix.Web.Models; -@using Modix.Web.Services; +@using Modix.Web.Models +@using Modix.Web.Shared.Models +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Web.Shared.Services @using MudBlazor +@using System.Security.Claims
@if (AvatarUrl is not null && Username is not null) @@ -48,23 +49,26 @@ @code { - [CascadingParameter] - public Task? AuthenticationState { get; set; } = null!; - - [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; - private string? AvatarUrl { get; set; } private string? Username { get; set; } private IEnumerable GuildOptions { get; set; } = Array.Empty(); - private SocketGuild? SelectedGuild { get; set; } + private GuildOption? SelectedGuild { get; set; } + + [CascadingParameter] + public required Task AuthenticationState { get; set; } + + [Inject] + public required ICookieService CookieService { get; set; } + + [CascadingParameter] + public required SessionState SessionState { get; set; } [Inject] - public CookieService CookieService { get; set; } = null!; + public required IHttpClientFactory HttpClientFactory { get; set; } [Inject] - public NavigationManager NavigationManager { get; set; } = null!; + public required NavigationManager NavigationManager { get; set; } protected override async Task OnInitializedAsync() { @@ -72,17 +76,25 @@ return; var authState = await AuthenticationState; - if (!authState.User.Identity?.IsAuthenticated ?? false) + if (authState.User.Identity?.IsAuthenticated is not true) return; - var avatarHash = authState.User.FindFirst(x => x.Type == DiscordAuthenticationConstants.Claims.AvatarHash)?.Value; - var user = DiscordHelper.GetCurrentUser(); + var avatarHash = authState.User.FindFirst(x => x.Type == nameof(DiscordUser.AvatarHash))?.Value; + var userId = ulong.Parse(authState.User.FindFirst(d => d.Type == ClaimTypes.NameIdentifier)?.Value); + AvatarUrl = $"https://cdn.discordapp.com/avatars/{userId}/{avatarHash}.png"; + Username = authState.User.Identity.Name; + + // TODO: Figure out why HttpClient does not have a base address in OnInitializedAsync() + + using var client = HttpClientFactory.CreateClient("api"); + + if (client.BaseAddress is null) + return; - AvatarUrl = $"https://cdn.discordapp.com/avatars/{user!.Id}/{avatarHash}.png"; - Username = authState.User.Identity?.Name; + var result = await client.GetFromJsonAsync("api/guild/guildoptions"); - GuildOptions = DiscordHelper.GetGuildOptions(); - SelectedGuild = user.Guild; + GuildOptions = result ?? []; + SelectedGuild = GuildOptions.FirstOrDefault(x => x.Id == SessionState.SelectedGuild); } private async Task SelectGuild(ulong guildId) diff --git a/src/Modix.Web.Wasm/Components/NavMenu.razor b/src/Modix.Web.Wasm/Components/NavMenu.razor new file mode 100644 index 000000000..b5ef812ab --- /dev/null +++ b/src/Modix.Web.Wasm/Components/NavMenu.razor @@ -0,0 +1,59 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Web.Models; +@using Modix.Web.Shared.Services +@using MudBlazor; +@using System.Security.Claims; + + +
+ +
+ +
+ +
+ + + @* The color: inherit here is needed to keep the colors consistent between link icons and light/dark theme icons + I would suggest not thinking about it too much :) *@ + + + + + +
+ + + + + + + +@code { + [Inject] + public required ICookieService CookieService { get; set; } + + [Parameter] + public bool DarkMode { get; set; } + + [Parameter] + public EventCallback DarkModeChanged { get; set; } + + private bool _drawerVisible; + + private void ToggleDrawer() => _drawerVisible = !_drawerVisible; + + private async Task ToggleDarkMode(bool toggled) + { + DarkMode = toggled; + await DarkModeChanged.InvokeAsync(DarkMode); + await CookieService.SetUseDarkModeAsync(DarkMode); + } +} diff --git a/src/Modix.Web.Wasm/Components/NavMenuLinks.razor b/src/Modix.Web.Wasm/Components/NavMenuLinks.razor new file mode 100644 index 000000000..53f467cdf --- /dev/null +++ b/src/Modix.Web.Wasm/Components/NavMenuLinks.razor @@ -0,0 +1,49 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models.Core +@using Modix.Web.Models; +@using MudBlazor + + + + Home + Stats + Commands + User Lookup + Tags + + + Promotions + + + + Logs + + + + Config + + + + +
+ Home + Commands +
+ +
+ Log In +
+
+
+ + \ No newline at end of file diff --git a/src/Modix.Web/Components/UserLookupField.razor b/src/Modix.Web.Wasm/Components/UserLookupField.razor similarity index 100% rename from src/Modix.Web/Components/UserLookupField.razor rename to src/Modix.Web.Wasm/Components/UserLookupField.razor diff --git a/src/Modix.Web.Wasm/Modix.Web.Wasm.csproj b/src/Modix.Web.Wasm/Modix.Web.Wasm.csproj new file mode 100644 index 000000000..8f865ec67 --- /dev/null +++ b/src/Modix.Web.Wasm/Modix.Web.Wasm.csproj @@ -0,0 +1,27 @@ + + + + enable + enable + true + Default + + + + + + + + + + + + + + + + + + + + diff --git a/src/Modix.Web/Pages/Commands.razor b/src/Modix.Web.Wasm/Pages/Commands.razor similarity index 86% rename from src/Modix.Web/Pages/Commands.razor rename to src/Modix.Web.Wasm/Pages/Commands.razor index 15e4a0fcd..898f77180 100644 --- a/src/Modix.Web/Pages/Commands.razor +++ b/src/Modix.Web.Wasm/Pages/Commands.razor @@ -1,17 +1,15 @@ @page "/commands" -@using Modix.Services.CommandHelp; -@using Modix.Services.Utilities; -@using Modix.Web.Components -@using Modix.Web.Models -@using Modix.Web.Models.Commands; -@using MudBlazor; -@using Humanizer; +@using Humanizer +@using Modix.Web.Shared.Models.Commands +@using Modix.Web.Wasm.Components +@using MudBlazor Modix - Commands +@attribute [StreamRendering] -@if (Modules is not null) +@if (Modules.Any()) { @@ -94,6 +92,10 @@ } +else +{ +

Loading...

+} @code { + [Inject] - public ICommandHelpService CommandHelpService { get; set; } = null!; + public required IHttpClientFactory HttpClientFactory { get; set; } - private IReadOnlyCollection? Modules; + private List Modules = []; - protected override void OnAfterRender(bool firstRender) + protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; - var modules = CommandHelpService.GetModuleHelpData(); + using var client = HttpClientFactory.CreateClient("api"); + + var request = new HttpRequestMessage(HttpMethod.Get, "api/commands"); + request.SetBrowserResponseStreamingEnabled(true); + + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + + var modules = response.Content.ReadFromJsonAsAsyncEnumerable(); - Modules = modules.Select(m => + await foreach (var module in modules) { - var commands = m.Commands.Select(c => new Command(c.Name, c.Summary, FormatUtilities.CollapsePlurals(c.Aliases), c.Parameters, c.IsSlashCommand)); - return new Module(m.Name, m.Summary, commands); - }).ToArray(); + if (module is null) + continue; - StateHasChanged(); + Modules.Add(module); + StateHasChanged(); + } } } diff --git a/src/Modix.Web.Wasm/Pages/Configuration.razor b/src/Modix.Web.Wasm/Pages/Configuration.razor new file mode 100644 index 000000000..183c443be --- /dev/null +++ b/src/Modix.Web.Wasm/Pages/Configuration.razor @@ -0,0 +1,58 @@ +@page "/config" +@page "/config/{SubPage}" + +@attribute [Authorize( + Roles = $@" + {nameof(AuthorizationClaim.DesignatedRoleMappingRead)}, + {nameof(AuthorizationClaim.DesignatedChannelMappingRead)}, + {nameof(AuthorizationClaim.AuthorizationConfigure)}")] + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models.Core +@using Modix.Web.Wasm.Components.Configuration +@using MudBlazor + +Modix - Configuration + +
+
+ Configuration + + + + + + + + + +
+
+ @if (SubPage == "roles") + { + + + + } + else if (SubPage == "channels") + { + + + + } + else if (SubPage == "claims") + { + + + + } +
+
+ +@code { + + [Parameter] + public string? SubPage { get; set; } + +} diff --git a/src/Modix.Web/Pages/CreatePromotion.razor b/src/Modix.Web.Wasm/Pages/CreatePromotion.razor similarity index 75% rename from src/Modix.Web/Pages/CreatePromotion.razor rename to src/Modix.Web.Wasm/Pages/CreatePromotion.razor index 8a8f5fefa..c42d995cf 100644 --- a/src/Modix.Web/Pages/CreatePromotion.razor +++ b/src/Modix.Web.Wasm/Pages/CreatePromotion.razor @@ -1,14 +1,14 @@ @page "/promotions/create" -@attribute [Authorize(Roles = nameof(AuthorizationClaim.PromotionsCreateCampaign))] -@using Modix.Data.Models.Core; -@using Modix.Services.Promotions; -@using Modix.Web.Components +@using Microsoft.AspNetCore.Authorization +@using Modix.Models.Core @using Modix.Web.Models; -@using Modix.Web.Models.Common; -@using Modix.Web.Models.Promotions; -@using Modix.Web.Services; +@using Modix.Web.Shared.Models.Common +@using Modix.Web.Shared.Models.Promotions +@using Modix.Web.Wasm.Components @using MudBlazor +@attribute [Authorize(Roles = nameof(AuthorizationClaim.PromotionsCreateCampaign))] + Modix - Start A Campaign @@ -28,7 +28,7 @@ @@ -69,16 +69,13 @@ @code { [Inject] - public IPromotionsService PromotionsService { get; set; } = null!; - - [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; + public required IHttpClientFactory HttpClientFactory { get; set; } [Inject] - public ISnackbar Snackbar { get; set; } = null!; + public required ISnackbar Snackbar { get; set; } [Inject] - public NavigationManager NavigationManager { get; set; } = null!; + public required NavigationManager NavigationManager { get; set; } private ModixUser? _selectedUser; private string? _promotionComment; @@ -97,32 +94,36 @@ if (user is null) return; - var nextRank = await PromotionsService.GetNextRankRoleForUserAsync(user.UserId); - var currentGuild = DiscordHelper.GetUserGuild(); - - if (nextRank is null) - { - _nextRank = new NextRank("None", "#607d8b"); - } - else - { - _nextRank = new NextRank(nextRank.Name, currentGuild.Roles.First(x => x.Id == nextRank.Id).Color.ToString()); - } + using var client = HttpClientFactory.CreateClient("api"); + _nextRank = await client.GetFromJsonAsync($"api/campaigns/{user.UserId}/nextrank"); } private async Task CreateCampaign() { try { - await PromotionsService.CreateCampaignAsync(_selectedUser!.UserId, _promotionComment); + using var client = HttpClientFactory.CreateClient("api"); + + using var response = await client.PutAsJsonAsync("api/campaigns/create", new PromotionCreationData(_selectedUser!.UserId, _promotionComment)); + + response.EnsureSuccessStatusCode(); + + NavigationManager.NavigateTo("/promotions"); } - catch (InvalidOperationException ex) + catch (Exception ex) { Snackbar.Configuration.PositionClass = Defaults.Classes.Position.BottomCenter; Snackbar.Add(ex.Message, Severity.Error); return; } + } + + private async Task> AutoCompleteAsync(string query) + { + using var client = HttpClientFactory.CreateClient("api"); + + var users = await client.GetFromJsonAsync($"api/autocomplete/users/{query}"); - NavigationManager.NavigateTo("/promotions"); + return users ?? []; } } diff --git a/src/Modix.Web/Pages/Index.razor b/src/Modix.Web.Wasm/Pages/Index.razor similarity index 100% rename from src/Modix.Web/Pages/Index.razor rename to src/Modix.Web.Wasm/Pages/Index.razor diff --git a/src/Modix.Web.Wasm/Pages/Logs.razor b/src/Modix.Web.Wasm/Pages/Logs.razor new file mode 100644 index 000000000..d4dbd7551 --- /dev/null +++ b/src/Modix.Web.Wasm/Pages/Logs.razor @@ -0,0 +1,66 @@ +@page "/logs/{SubPage}" +@page "/logs" +@page "/infractions" + +@attribute [Authorize(Roles = nameof(AuthorizationClaim.ModerationRead))] + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models.Core +@using Modix.Web.Wasm.Components.Infractions +@using MudBlazor + +Modix - Logs + +
+
+ Logs + + + + + + + +
+
+ @if (SubPage == "infractions") + { + + + + } + else if (SubPage == "deletedMessages") + { + + + + } +
+
+ +@code { + [Parameter] + public string? SubPage { get; set; } + + [Inject] + public NavigationManager NavigationManager { get; set; } = null!; + + [Parameter] + [SupplyParameterFromQuery] + public string? Subject { get; set; } + + [Parameter] + [SupplyParameterFromQuery] + public string? Id { get; set; } + + protected override void OnAfterRender(bool firstRender) + { + var relativePath = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + + if (relativePath.StartsWith("infractions")) + { + NavigationManager.NavigateTo($"/logs/{relativePath}", new NavigationOptions { ReplaceHistoryEntry = true, ForceLoad = false }); + } + } +} diff --git a/src/Modix.Web.Wasm/Pages/Promotions.razor b/src/Modix.Web.Wasm/Pages/Promotions.razor new file mode 100644 index 000000000..d38b010b5 --- /dev/null +++ b/src/Modix.Web.Wasm/Pages/Promotions.razor @@ -0,0 +1,375 @@ +@page "/promotions" + +@attribute [Authorize(Roles = nameof(AuthorizationClaim.PromotionsRead))] + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models.Core +@using Modix.Models.Promotions +@using Modix.Models.Utilities +@using Modix.Web.Models; +@using Modix.Web.Shared.Models.Common +@using Modix.Web.Shared.Models.Promotions +@using Modix.Web.Shared.Services +@using Modix.Web.Wasm.Components +@using MudBlazor +@using Humanizer; +@using System.Security.Claims + +Modix - Promotions + + + Promotion Campaigns +
+ + Start One +
+ + @foreach (var (roleColor, campaign) in Campaigns + .Where(x => _showInactive ? true : !x.Campaign.IsClosed) + .OrderByDescending(x => !x.Campaign.IsClosed) + .ThenByDescending(x => x.Campaign.Created)) + { + var icon = campaign.Outcome switch + { + PromotionCampaignOutcome.Accepted => Icons.Material.Filled.Check, + PromotionCampaignOutcome.Rejected => Icons.Material.Filled.NotInterested, + PromotionCampaignOutcome.Failed => Icons.Material.Filled.Error, + _ => Icons.Material.Filled.HowToVote + }; + + var sentimentRatio = campaign.IsCurrentUserCampaign ? 0d : (double)campaign.ApproveCount / (campaign.ApproveCount + campaign.OpposeCount); + var sentimentColor = sentimentRatio switch + { + _ when campaign.IsCurrentUserCampaign => Color.Transparent, + > 0.67 => Color.Success, + > 0.33 => Color.Warning, + _ => Color.Error + }; + + + +
+
+ + + + + @campaign.SubjectName + + + @campaign.TargetRoleName +
+ +
+ @if (campaign.Outcome is null) + { + + + + + + + } + +
+ +
+
+
+ + @(campaign.IsCurrentUserCampaign ? "?" : campaign.ApproveCount.ToString()) +
+
+ + @(campaign.IsCurrentUserCampaign ? "?" : campaign.OpposeCount.ToString()) +
+
+ +
+
+
+ + Campaign started @campaign.Created.ToString("MM/dd/yy, h:mm:ss tt") + + @if (campaign.IsCurrentUserCampaign) + { + + Sorry, you aren't allowed to see comments on your own campaign. + + } + else if (!CampaignCommentData.ContainsKey(campaign.Id)) + { + + } + else + { + foreach (var comment in CampaignCommentData[campaign.Id].Values.OrderByDescending(x => x.CreatedAt)) + { + var sentimentIcon = comment.PromotionSentiment == PromotionSentiment.Approve ? Icons.Material.Filled.ThumbUp : Icons.Material.Filled.ThumbDown; +
+ + @comment.Content + + @if (comment.IsFromCurrentUser && !campaign.IsClosed) + { + + Edit + + } + @comment.CreatedAt.ToString("MM/dd/yy, h:mm:ss tt") +
+ + } + + if (!campaign.IsClosed && !CampaignCommentData[campaign.Id].Any(x => x.Value.IsFromCurrentUser)) + { + + } + } +
+
+ } +
+
+ + + +@code { + [Inject] + public required IDialogService DialogService { get; set; } + + [Inject] + public required ISnackbar Snackbar { get; set; } + + [Inject] + public required ICookieService CookieService { get; set; } + + [Inject] + public required IHttpClientFactory HttpClientFactory { get; set; } + + [CascadingParameter] + public required SessionState SessionState { get; set; } + + [CascadingParameter] + public required Task AuthenticationState { get; set; } + + private List<(string RoleColor, PromotionCampaignData Campaign)> Campaigns = []; + private Dictionary> CampaignCommentData = new Dictionary>(); + + private bool _showInactive; + + protected override void OnInitialized() + { + _showInactive = SessionState.ShowInactivePromotions; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + using var client = HttpClientFactory.CreateClient("api"); + + var roles = await client.GetFromJsonAsync>("api/roles"); + var campaigns = client.GetFromJsonAsAsyncEnumerable("api/campaigns"); + + await foreach (var campaign in campaigns) + { + var roleColor = GetRoleColor(roles, campaign.TargetRoleId); + Campaigns.Add((roleColor, campaign)); + StateHasChanged(); + } + } + + private string GetRoleColor(Dictionary roles, ulong roleId) + { + // In case the role has been deleted and we still have a campaign record for that role we serve a grey color. + if (!roles.TryGetValue(roleId, out var roleInformation)) + { + return $"color: grey"; + } + + return $"color: {roleInformation.Color}"; + } + + private async Task ShowInactiveChanged(bool showInactive) + { + _showInactive = showInactive; + await CookieService.SetShowInactivePromotionsAsync(showInactive); + } + + private async Task CampaignExpanded(bool wasExpanded, long campaignId, ulong userId) + { + if (!wasExpanded) + return; + + var authState = await AuthenticationState; + var currentUserId = authState.User.FindFirst(ClaimTypes.NameIdentifier)!.Value; + var userSnowflake = ulong.Parse(currentUserId); + + if (userSnowflake == userId) + return; + + if (CampaignCommentData.ContainsKey(campaignId)) + return; + + using var client = HttpClientFactory.CreateClient("api"); + var campaignComments = await client.GetFromJsonAsync>($"api/campaigns/{campaignId}"); + + if (campaignComments is null) + { + Snackbar.Add($"Unable to load campaign details for campaign id {campaignId}.", Severity.Error); + return; + } + + CampaignCommentData[campaignId] = campaignComments; + + StateHasChanged(); + } + + private async Task OnCampaignCommentCreation(long campaignId, string campaignSubjectName, PromotionSentiment sentiment, string? content) + { + try + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PutAsJsonAsync($"api/campaigns/{campaignId}/createcomment", new CampaignCommentData(default, sentiment, content, default, default)); + + response.EnsureSuccessStatusCode(); + + var newComment = await response.Content.ReadFromJsonAsync(); + + CampaignCommentData[campaignId][newComment!.Id] = newComment; + + Snackbar.Add($"Added comment to campaign for user {campaignSubjectName}.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + return; + } + + } + + private async Task ToggleEditDialog(long campaignId, long commentId, PromotionSentiment oldPromotionSentiment, string oldContent) + { + var dialogParams = new DialogParameters + { + { x => x.PromotionSentiment, oldPromotionSentiment }, + { x => x.Content, oldContent} + }; + + var dialog = DialogService.Show("", dialogParams); + var result = await dialog.Result; + + if (result.Canceled) + return; + + var (newPromotionSentiment, newContent) = ((PromotionSentiment, string))result.Data; + + try + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PatchAsJsonAsync("api/campaigns/updatecomment", new CampaignCommentData(commentId, newPromotionSentiment, newContent, default, default)); + + response.EnsureSuccessStatusCode(); + + var newComment = await response.Content.ReadFromJsonAsync(); + + CampaignCommentData[campaignId].Remove(commentId); + CampaignCommentData[campaignId][newComment!.Id] = newComment; + + Snackbar.Add("Campaign vote was updated.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + return; + } + } + + private async Task AcceptCampaign(PromotionCampaignData campaign) + { + var timeSince = DateTime.UtcNow - campaign.Created; + + var username = campaign.SubjectName; + bool force = false; + + if (timeSince < PromotionCampaignEntityExtensions.CampaignAcceptCooldown) + { + var timeLeftHumanized = campaign.Created.GetTimeUntilCampaignCanBeClosed().Humanize(3); + var dialogParams = new DialogParameters + { + { x => x.Content, $"There is {timeLeftHumanized} left on the campaign. Do you want to force accept the campaign for {username}?" } + }; + + var dialog = DialogService.Show("", dialogParams); + var confirmationResult = await dialog.Result; + + if (confirmationResult.Canceled) + { + Snackbar.Add("Action was cancelled", Severity.Info); + return; + } + + force = true; + } + + try + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PostAsync($"api/campaigns/{campaign.Id}/accept/{force}", default); + + response.EnsureSuccessStatusCode(); + + campaign.Outcome = PromotionCampaignOutcome.Accepted; + Snackbar.Add($"Campaign for '{username}' was accepted.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + return; + } + + } + + private async Task RejectCampaign(PromotionCampaignData campaign) + { + try + { + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PostAsync($"api/campaigns/{campaign.Id}/reject", default); + + response.EnsureSuccessStatusCode(); + + campaign.Outcome = PromotionCampaignOutcome.Rejected; + Snackbar.Add($"Campaign for '{campaign.SubjectName}' was rejected.", Severity.Success); + + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + return; + } + } +} diff --git a/src/Modix.Web/Pages/Stats.razor b/src/Modix.Web.Wasm/Pages/Stats.razor similarity index 84% rename from src/Modix.Web/Pages/Stats.razor rename to src/Modix.Web.Wasm/Pages/Stats.razor index 9220d27d4..841a26365 100644 --- a/src/Modix.Web/Pages/Stats.razor +++ b/src/Modix.Web.Wasm/Pages/Stats.razor @@ -1,11 +1,10 @@ @page "/stats" -@attribute [Authorize] -@using Modix.Data.Models.Core; -@using Modix.Services.GuildStats; -@using Modix.Web.Models.Stats; -@using Modix.Web.Services; +@using Microsoft.AspNetCore.Authorization +@using Modix.Web.Shared.Models.Stats @using MudBlazor +@attribute [Authorize] + Modix - Stats @if (Data is not null) @@ -83,27 +82,22 @@ } @code { - GuildStatData Data { get; set; } = null!; - List GuildRoleCountView { get; set; } = null!; + GuildStatData? Data { get; set; } + IReadOnlyCollection? GuildRoleCountView { get; set; } [Inject] - IGuildStatService GuildStatService { get; set; } = null!; - - [Inject] - DiscordHelper DiscordHelper { get; set; } = null!; + public required IHttpClientFactory HttpClientFactory { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; - var currentUser = DiscordHelper.GetCurrentUser(); - - var roleCounts = await GuildStatService.GetGuildMemberDistributionAsync(currentUser!.Guild); - var messageCounts = await GuildStatService.GetTopMessageCounts(currentUser.Guild, currentUser.Id); + using var client = HttpClientFactory.CreateClient("api"); + var guildStatData = await client.GetFromJsonAsync("api/guildstats"); - Data = new GuildStatData(currentUser.Guild.Name, roleCounts, messageCounts); - GuildRoleCountView = roleCounts; + Data = guildStatData; + GuildRoleCountView = guildStatData?.GuildRoleCounts; StateHasChanged(); } diff --git a/src/Modix.Web.Wasm/Pages/Tags.razor b/src/Modix.Web.Wasm/Pages/Tags.razor new file mode 100644 index 000000000..baec9925e --- /dev/null +++ b/src/Modix.Web.Wasm/Pages/Tags.razor @@ -0,0 +1,189 @@ +@page "/tags" +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Models.Core +@using Modix.Web.Shared.Models.Common +@using Modix.Web.Shared.Models.Tags +@using MudBlazor +@attribute [Authorize] + +Modix - Tags + + + Tags + @if (Data is not null && Roles is not null) + { + + + Create Tag + + + + + Preview + + + + + Save + + + Cancel + + + +
+
+ + Create + + Refresh +
+ + +
+ + + + Name + Last Modified + Owner + Content + Uses + + + @tag.Name + @tag.Created.ToString("MM/dd/yy, h:mm:ss tt") + @if (tag.IsOwnedByRole) + { + _ = Roles.TryGetValue(tag.OwnerId, out var role); + var roleColor = role?.Color ?? "currentColor"; + @@@tag.OwnerName + } + else + { + @tag.OwnerName + } + + + + @tag.Uses + + + + + + } +
+ +@code { + [Inject] + public required IDialogService DialogService { get; set; } + + [Inject] + public required ISnackbar Snackbar { get; set; } + + [Inject] + public required IHttpClientFactory HttpClientFactory { get; set; } + + [Parameter] + [SupplyParameterFromQuery] + public string? Query { get; set; } + + private Dictionary? Roles { get; set; } + private List? Data { get; set; } + + private string? _tagNameValue; + private string? _tagContentValue; + private bool _createDialogVisible; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + return; + + await FetchData(); + + StateHasChanged(); + } + + private async Task FetchData() + { + Data = []; + + using var client = HttpClientFactory.CreateClient("api"); + + var rolesTask = client.GetFromJsonAsync>("api/roles"); + + var tagData = client.GetFromJsonAsAsyncEnumerable("api/tags"); + + await foreach(var tag in tagData) + { + if (tag is null) + continue; + + Data.Add(tag); + StateHasChanged(); + } + + Roles = await rolesTask; + } + + private bool FilterFunction(TagData tag) + { + if (string.IsNullOrWhiteSpace(Query)) + return true; + + if(tag.OwnerName.Contains(Query, StringComparison.OrdinalIgnoreCase) || tag.OwnerId.ToString() == Query) + return true; + + if (tag.Name.Contains(Query, StringComparison.OrdinalIgnoreCase)) + return true; + + if (tag.Content.Contains(Query, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + private async Task SaveTag() + { + try + { + var tagCreationData = new TagCreationData(_tagNameValue, _tagContentValue); + + using var client = HttpClientFactory.CreateClient("api"); + using var response = await client.PutAsJsonAsync("api/tags", tagCreationData); + + var createdTag = await response.Content.ReadFromJsonAsync(); + + if (createdTag is null) + { + Snackbar.Add("Something went wrong while saving the tag.", Severity.Error); + return; + } + + Data?.Add(createdTag); + Snackbar.Add($"Tag '{_tagNameValue}' created.", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add(ex.Message, Severity.Error); + } + finally + { + _tagNameValue = null; + _tagContentValue = null; + + _createDialogVisible = false; + } + } + + private void ToggleDialog() + { + _createDialogVisible = !_createDialogVisible; + } +} \ No newline at end of file diff --git a/src/Modix.Web.Wasm/Pages/UnAuthorized.razor b/src/Modix.Web.Wasm/Pages/UnAuthorized.razor new file mode 100644 index 000000000..c3362eb07 --- /dev/null +++ b/src/Modix.Web.Wasm/Pages/UnAuthorized.razor @@ -0,0 +1,4 @@ +@page "/unauthorized" +@using MudBlazor + +Sorry, you don't have access to that page. \ No newline at end of file diff --git a/src/Modix.Web/Pages/UserLookup.razor b/src/Modix.Web.Wasm/Pages/UserLookup.razor similarity index 68% rename from src/Modix.Web/Pages/UserLookup.razor rename to src/Modix.Web.Wasm/Pages/UserLookup.razor index 30c3b7976..f2379735b 100644 --- a/src/Modix.Web/Pages/UserLookup.razor +++ b/src/Modix.Web.Wasm/Pages/UserLookup.razor @@ -1,20 +1,13 @@ @page "/userlookup" -@attribute [Authorize] -@using Discord; -@using Modix.Data.Repositories; -@using Modix.Services.Core; -@using Modix.Services.Utilities; -@using Modix.Web.Components -@using Modix.Web.Models; -@using Modix.Web.Models.UserLookup; -@using Modix.Web.Services +@using Humanizer +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Web.Shared.Models.Common +@using Modix.Web.Shared.Models.UserLookup +@using Modix.Web.Wasm.Components @using MudBlazor -@using Discord.WebSocket -@using System.Linq.Expressions; -@using MudBlazor.Charts -@using System.Globalization; -@using Humanizer; -@using Modix.Web.Models.Common; +@using System.Security.Claims +@attribute [Authorize] Modix - User Lookup @@ -25,7 +18,7 @@
@@ -89,7 +82,7 @@ - @foreach (var channel in userInformation.MessageCountsPerChannel) + @foreach (var channel in messageCountsPerChannel) { var channelColorStyle = $"border: 1px solid {channel.Color}"; @code { + MessageCountPerChannelInformation[] messageCountsPerChannel = Array.Empty(); MessageCountPerChannelInformation[] messageCountsPerChannelView = Array.Empty(); UserInformation? userInformation = null; [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; - - [Inject] - public IUserService UserService { get; set; } = null!; + public required IHttpClientFactory HttpClientFactory { get; set; } - [Inject] - public IMessageRepository MessageRepository { get; set; } = null!; + [CascadingParameter] + public Task AuthState { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; - var currentUser = DiscordHelper.GetCurrentUser(); - await SelectedUserChanged(ModixUser.FromIGuildUser(currentUser!)); + var authState = await AuthState; + var userId = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + var currentUser = await AutoCompleteAsync(userId); + await SelectedUserChanged(currentUser.FirstOrDefault()); StateHasChanged(); } @@ -142,41 +136,32 @@ private void SelectedChannelsChanged(MudChip[] chips) { var channels = chips.Select(x => x.Value).Cast(); - messageCountsPerChannelView = userInformation!.MessageCountsPerChannel + messageCountsPerChannelView = messageCountsPerChannel .Where(x => channels.Contains(x.ChannelName)) .ToArray(); } - private async Task SelectedUserChanged(ModixUser user) + private async Task> AutoCompleteAsync(string query) { - if (user is null) - return; + using var client = HttpClientFactory.CreateClient("api"); - var currentGuild = DiscordHelper.GetUserGuild(); + var users = await client.GetFromJsonAsync($"api/autocomplete/users/{query}"); - var ephemeralUser = await UserService.GetUserInformationAsync(currentGuild.Id, user.UserId); - - var userRank = await MessageRepository.GetGuildUserParticipationStatistics(currentGuild.Id, user.UserId); - var messages7 = await MessageRepository.GetGuildUserMessageCountByDate(currentGuild.Id, user.UserId, TimeSpan.FromDays(7)); - var messages30 = await MessageRepository.GetGuildUserMessageCountByDate(currentGuild.Id, user.UserId, TimeSpan.FromDays(30)); - - var roles = ephemeralUser!.RoleIds - .Select(x => currentGuild.GetRole(x)) - .OrderByDescending(x => x.IsHoisted) - .ThenByDescending(x => x.Position) - .ToArray(); + return users ?? []; + } - var timespan = DateTimeOffset.UtcNow - DateTimeOffset.MinValue; - var result = await MessageRepository.GetGuildUserMessageCountByChannel(currentGuild.Id, user.UserId, timespan); - var colors = ColorUtils.GetRainbowColors(result.Count); + private async Task SelectedUserChanged(ModixUser? user) + { + if (user is null) + return; - var messageCountsPerChannel = result - .Select((x, i) => new MessageCountPerChannelInformation(x.ChannelName, x.MessageCount, colors[i++].ToString())) - .OrderByDescending(x => x.Count) - .ToList(); + using var client = HttpClientFactory.CreateClient("api"); - userInformation = UserInformation.FromEphemeralUser(ephemeralUser, userRank, messages7, messages30, roles, messageCountsPerChannel); + var userInformationTask = client.GetFromJsonAsync($"api/userinformation/{user.UserId}"); + var messageCountsPerChannelTask = client.GetFromJsonAsync($"api/userinformation/{user.UserId}/messages"); - messageCountsPerChannelView = userInformation.MessageCountsPerChannel.ToArray(); + userInformation = await userInformationTask; + messageCountsPerChannel = (await messageCountsPerChannelTask) ?? []; + messageCountsPerChannelView = messageCountsPerChannel; } } diff --git a/src/Modix.Web.Wasm/Program.cs b/src/Modix.Web.Wasm/Program.cs new file mode 100644 index 000000000..5e02e05e3 --- /dev/null +++ b/src/Modix.Web.Wasm/Program.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Modix.Web.Models; +using Modix.Web.Shared.Services; +using Modix.Web.Wasm.Security; +using Modix.Web.Wasm.Services; +using MudBlazor; +using MudBlazor.Services; + +namespace Modix.Web.Wasm; + +public class Program +{ + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + + builder.Services + .AddScoped() + .AddScoped(); + + builder.Services.AddCascadingValue(sp => sp.GetRequiredService()); + + builder.Services.AddHttpClient("api", http => http.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)); + + builder.Services + .AddAuthorizationCore() + .AddCascadingAuthenticationState() + .AddSingleton(); + + builder.Services + .AddMudServices() + .AddMudMarkdownServices(); + + await builder.Build().RunAsync(); + } +} diff --git a/src/Modix.Web.Wasm/Properties/launchSettings.json b/src/Modix.Web.Wasm/Properties/launchSettings.json new file mode 100644 index 000000000..0bd25631f --- /dev/null +++ b/src/Modix.Web.Wasm/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7032", + "sslPort": 44347 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5066", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7153;http://localhost:5066", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Modix.Web.Wasm/Routes.razor b/src/Modix.Web.Wasm/Routes.razor new file mode 100644 index 000000000..107d1e10d --- /dev/null +++ b/src/Modix.Web.Wasm/Routes.razor @@ -0,0 +1,89 @@ +@using Microsoft.AspNetCore.Components.Authorization +@using Modix.Web.Models +@using Modix.Web.Wasm.Components +@using Modix.Web.Wasm.Shared +@using MudBlazor +@using System.Security.Claims + + + + + + + + Sorry, you don't have access to that page. + + + Please wait... + + + + + + + + +@code { + [Parameter] + public string? ShowInfractionState { get; set; } + + [Parameter] + public string? ShowDeletedInfractions { get; set; } + + [Parameter] + public string? ShowInactivePromotions { get; set; } + + [Parameter] + public string? UseDarkMode { get; set; } + + [CascadingParameter] + public required SessionState SessionState { get; set; } + + [CascadingParameter] + public required Task AuthenticationState { get; set; } + + [Inject] + public required PersistentComponentState State { get; set; } + + + protected override void OnParametersSet() + { + State.RegisterOnPersisting(OnPersisting, RenderMode.InteractiveAuto); + } + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationState; + if (authState.User.Identity?.IsAuthenticated is not true) + return; + + if (State.TryTakeFromJson(nameof(SessionState), out var sessionState) && sessionState is not null) + { + SessionState = sessionState; + return; + } + + var userId = authState.User.FindFirst(x => x.Type == ClaimTypes.NameIdentifier)?.Value; + var currentGuild = authState.User.FindFirst(ClaimTypes.PostalCode)?.Value; + + _ = ulong.TryParse(userId, out var userSnowflake); + _ = ulong.TryParse(currentGuild, out var selectedGuildId); + _ = bool.TryParse(ShowInfractionState, out var showInfractionState); + _ = bool.TryParse(ShowDeletedInfractions, out var showDeletedInfractions); + _ = bool.TryParse(ShowInactivePromotions, out var showInactivePromotions); + _ = bool.TryParse(UseDarkMode, out var useDarkMode); + + SessionState.CurrentUserId = userSnowflake; + SessionState.SelectedGuild = selectedGuildId; + SessionState.ShowInfractionState = showInfractionState; + SessionState.ShowDeletedInfractions = showDeletedInfractions; + SessionState.ShowInactivePromotions = showInactivePromotions; + SessionState.UseDarkMode = useDarkMode; + } + + private Task OnPersisting() + { + State.PersistAsJson(nameof(SessionState), SessionState); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Modix.Web.Wasm/Security/PersistentAuthenticationStateProvider.cs b/src/Modix.Web.Wasm/Security/PersistentAuthenticationStateProvider.cs new file mode 100644 index 000000000..fe74666ca --- /dev/null +++ b/src/Modix.Web.Wasm/Security/PersistentAuthenticationStateProvider.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Components; +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using Modix.Web.Shared.Models; + +namespace Modix.Web.Wasm.Security; + +public class PersistentAuthenticationStateProvider : AuthenticationStateProvider +{ + private static readonly Task DefaultUnauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + + private readonly Task _authenticationStateTask = DefaultUnauthenticatedTask; + + public PersistentAuthenticationStateProvider(PersistentComponentState state) + { + if (!state.TryTakeFromJson(nameof(DiscordUser), out var userInfo) || userInfo is null) + return; + + Claim[] claims = [ + new Claim(ClaimTypes.NameIdentifier, userInfo.UserId.ToString()), + new Claim(ClaimTypes.Name, userInfo.Name), + new Claim(nameof(DiscordUser.AvatarHash), userInfo.AvatarHash), + new Claim(ClaimTypes.PostalCode, userInfo.CurrentGuild.ToString()) + ]; + + var roles = userInfo.Claims.Select(role => new Claim(ClaimTypes.Role, role)); + + var claimsIdentity = new ClaimsIdentity([..claims, ..roles], authenticationType: nameof(PersistentAuthenticationStateProvider)); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + var authState = new AuthenticationState(claimsPrincipal); + _authenticationStateTask = Task.FromResult(authState); + } + + public override Task GetAuthenticationStateAsync() => _authenticationStateTask; +} diff --git a/src/Modix.Web.Wasm/Services/CookieService.cs b/src/Modix.Web.Wasm/Services/CookieService.cs new file mode 100644 index 000000000..2a1dd1e7c --- /dev/null +++ b/src/Modix.Web.Wasm/Services/CookieService.cs @@ -0,0 +1,41 @@ +using Microsoft.JSInterop; +using Modix.Web.Models; +using Modix.Web.Shared.Services; + +namespace Modix.Web.Wasm.Services; + +public class CookieService(IJSRuntime jsRuntime, SessionState sessionState) : ICookieService +{ + public async Task SetSelectedGuildAsync(ulong guildId) + { + await SetCookieAsync(CookieConstants.SelectedGuild, guildId); + sessionState.SelectedGuild = guildId; + } + + public async Task SetShowDeletedInfractionsAsync(bool showDeleted) + { + await SetCookieAsync(CookieConstants.ShowDeletedInfractions, showDeleted); + sessionState.ShowDeletedInfractions = showDeleted; + } + + public async Task SetShowInfractionStateAsync(bool showInfractionState) + { + await SetCookieAsync(CookieConstants.ShowInfractionState, showInfractionState); + sessionState.ShowInfractionState = showInfractionState; + } + + public async Task SetShowInactivePromotionsAsync(bool showInactivePromotions) + { + await SetCookieAsync(CookieConstants.ShowInactivePromotions, showInactivePromotions); + sessionState.ShowInactivePromotions = showInactivePromotions; + } + + public async Task SetUseDarkModeAsync(bool useDarkMode) + { + await SetCookieAsync(CookieConstants.UseDarkMode, useDarkMode); + sessionState.UseDarkMode = useDarkMode; + } + + private async Task SetCookieAsync(string key, T value) + => await jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{key}={value}; path=/\";"); +} diff --git a/src/Modix.Web/Shared/MainLayout.razor b/src/Modix.Web.Wasm/Shared/MainLayout.razor similarity index 65% rename from src/Modix.Web/Shared/MainLayout.razor rename to src/Modix.Web.Wasm/Shared/MainLayout.razor index a7d8ba2c3..6f5a83a99 100644 --- a/src/Modix.Web/Shared/MainLayout.razor +++ b/src/Modix.Web.Wasm/Shared/MainLayout.razor @@ -1,5 +1,5 @@ -@using Modix.Data.Models.Core; -@using Modix.Web.Models +@using Modix.Web.Models +@using Modix.Web.Wasm.Components @using MudBlazor @inherits LayoutComponentBase @@ -7,20 +7,19 @@ - - + - + - - @Body - + + @Body + - - + @code { - [Inject] + + [CascadingParameter] public required SessionState SessionState { get; set; } private MudTheme _theme = new() @@ -32,7 +31,7 @@ }, PaletteDark = new PaletteDark() { - Primary = new("#803788"), + Primary = new("#803788"), }, Typography = new() { diff --git a/src/Modix.Web.Wasm/_Imports.razor b/src/Modix.Web.Wasm/_Imports.razor new file mode 100644 index 000000000..81b332c12 --- /dev/null +++ b/src/Modix.Web.Wasm/_Imports.razor @@ -0,0 +1,9 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Modix.Web.Wasm diff --git a/src/Modix.Web/App.razor b/src/Modix.Web/App.razor index a3e8b7651..2fe181a47 100644 --- a/src/Modix.Web/App.razor +++ b/src/Modix.Web/App.razor @@ -1,84 +1,62 @@ @using Discord.WebSocket; @using Modix.Web.Models; @using Modix.Web.Services; +@using Modix.Web.Wasm @using MudBlazor @using System.Security.Claims; @using Modix.Services.Core; - - - - - - Sorry, you don't have access to that page. - - - Please wait... - - - - - - Not found - - Sorry, there's nothing at this address. - - - - + + + + + + + + + + + + + + + + + + + + +
+ @if(Env.IsDevelopment()) + { + + An unhandled exception has occurred. See browser dev tools for details. + + } + else + { + + An error has occurred. This application may no longer respond until reloaded. + + } +
+ + + + + + + @code { - [Parameter] - public string? SelectedGuild { get; set; } - - [Parameter] - public string? ShowInfractionState { get; set; } - - [Parameter] - public string? ShowDeletedInfractions { get; set; } - - [Parameter] - public string? ShowInactivePromotions { get; set; } - - [Parameter] - public string? UseDarkMode { get; set; } - - [Inject] - public SessionState SessionState { get; set; } = null!; - - [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; [Inject] - public AuthenticationStateProvider AuthenticationStateProvider { get; set; } = null!; - - [Inject] - public Modix.Services.Core.IAuthorizationService AuthorizationService { get; set; } = null!; - - protected override async Task OnInitializedAsync() - { - var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); - if (!authState.User.Identity?.IsAuthenticated ?? false) - return; - - var userId = authState.User.FindFirst(x => x.Type == ClaimTypes.NameIdentifier)?.Value; - - _ = ulong.TryParse(userId, out var userSnowflake); - _ = ulong.TryParse(SelectedGuild, out var selectedGuildId); - _ = bool.TryParse(ShowInfractionState, out var showInfractionState); - _ = bool.TryParse(ShowDeletedInfractions, out var showDeletedInfractions); - _ = bool.TryParse(ShowInactivePromotions, out var showInactivePromotions); - _ = bool.TryParse(UseDarkMode, out var useDarkMode); - - SessionState.CurrentUserId = userSnowflake; - SessionState.SelectedGuild = selectedGuildId; - SessionState.ShowInfractionState = showInfractionState; - SessionState.ShowDeletedInfractions = showDeletedInfractions; - SessionState.ShowInactivePromotions = showInactivePromotions; - SessionState.UseDarkMode = useDarkMode; - - var currentUser = DiscordHelper.GetCurrentUser(); + public required IWebHostEnvironment Env { get; set; } - await AuthorizationService.OnAuthenticatedAsync(currentUser!.Id, currentUser.Guild.Id, currentUser.Roles.Select(x => x.Id).ToList()); - } + [CascadingParameter] + public required HttpContext Context { get; set; } } \ No newline at end of file diff --git a/src/Modix.Web/Components/Configuration/Channels.razor b/src/Modix.Web/Components/Configuration/Channels.razor deleted file mode 100644 index f9521657a..000000000 --- a/src/Modix.Web/Components/Configuration/Channels.razor +++ /dev/null @@ -1,169 +0,0 @@ -@using Modix.Data.Models.Core; -@using Modix.Web.Models.Common; -@using Modix.Web.Models.Configuration; -@using Modix.Web.Services; -@using MudBlazor -@using Humanizer; -@using Modix.Services - -Modix - Channels -Channel Designations - - - @if (DesignatedChannelMappings is not null && DesignatedChannelTypes is not null) - { - - - Assign a Channel - - - - Designation - - @foreach (var designation in DesignatedChannelTypes) - { - - } - - - - - Assign - - - Cancel - - - - - - @foreach (var designatedChannelType in DesignatedChannelTypes.OrderBy(x => x.ToString())) - { - -
-
- - @designatedChannelType.ToString().Titleize() - - @if (!DesignatedChannelMappings.TryGetValue(designatedChannelType, out var channelDesignations) || !channelDesignations.Any()) - { - - NONE ASSIGNED - - } - else - { - @foreach (var designatedChannelMapping in channelDesignations) - { - - } - } -
- -
- - - -
-
-
- - } -
-
- } -
- -@code { - [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; - - [Inject] - public DesignatedChannelService DesignatedChannelService { get; set; } = null!; - - [Inject] - public ISnackbar Snackbar { get; set; } = null!; - - private Dictionary>? DesignatedChannelMappings { get; set; } - private DesignatedChannelType[]? DesignatedChannelTypes { get; set; } - - private bool _createDialogVisible; - private DesignatedChannelType? _selectedDesignatedChannelType; - private ChannelInformation? _selectedChannel; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; - - var currentGuild = DiscordHelper.GetUserGuild(); - var designatedChannels = await DesignatedChannelService.GetDesignatedChannels(currentGuild.Id); - - DesignatedChannelMappings = designatedChannels - .Select(d => new DesignatedChannelData(d.Id, d.Channel.Id, d.Type, currentGuild?.GetChannel(d.Channel.Id)?.Name ?? d.Channel.Name)) - .ToLookup(x => x.ChannelDesignation, x => x) - .ToDictionary(x => x.Key, x => x.ToList()); - - DesignatedChannelTypes = Enum.GetValues(); - - StateHasChanged(); - } - - public void ToggleCreateDialog() - { - _createDialogVisible = !_createDialogVisible; - if (_createDialogVisible) - { - _selectedChannel = null; - _selectedDesignatedChannelType = null; - } - } - - private void SelectedChannelChanged(ChannelInformation channel) - { - _selectedChannel = channel; - } - - public async Task SaveDesignation() - { - var currentGuild = DiscordHelper.GetUserGuild(); - var channel = (Discord.IMessageChannel)currentGuild.GetChannel(_selectedChannel!.Id); - - var id = await DesignatedChannelService.AddDesignatedChannel(currentGuild, channel, _selectedDesignatedChannelType!.Value); - - _createDialogVisible = false; - - if (!DesignatedChannelMappings!.ContainsKey(_selectedDesignatedChannelType.Value)) - { - DesignatedChannelMappings[_selectedDesignatedChannelType.Value] = new List(); - } - - DesignatedChannelMappings[_selectedDesignatedChannelType.Value].Add(new DesignatedChannelData(id, _selectedChannel.Id, _selectedDesignatedChannelType.Value, _selectedChannel.Name)); - - Snackbar.Add($"Added designation '{_selectedDesignatedChannelType}' to channel '{_selectedChannel.Name}'", Severity.Success); - } - - public async Task RemoveDesignation(long id, DesignatedChannelType designatedChannelType) - { - await DesignatedChannelService.RemoveDesignatedChannelById(id); - - var channelMappingsWithType = DesignatedChannelMappings![designatedChannelType]; - var removedChannelMapping = channelMappingsWithType.First(x => x.Id == id); - - channelMappingsWithType.Remove(removedChannelMapping); - - Snackbar.Add($"Removed designation '{designatedChannelType}' from channel '{removedChannelMapping.Name}'", Severity.Success); - } -} diff --git a/src/Modix.Web/Components/Configuration/IndividualDesignation.razor b/src/Modix.Web/Components/Configuration/IndividualDesignation.razor deleted file mode 100644 index f79267fa0..000000000 --- a/src/Modix.Web/Components/Configuration/IndividualDesignation.razor +++ /dev/null @@ -1,66 +0,0 @@ -@using Modix.Data.Models.Core; -@using Modix.Web.Models.Configuration; -@using MudBlazor - - - - - @NamePrefix@Name - - - @if (!_showConfirm) - { - - X - - } - else - { - Remove Designation? - - - Yes - - - No - - } - - - - -@code { - [Parameter, EditorRequired] - public EventCallback RemoveDesignation { get; set; } - - [Parameter, EditorRequired] - public string? AuthorizationRoleForDelete { get; set; } - - [Parameter, EditorRequired] - public string? NamePrefix { get; set; } - - [Parameter, EditorRequired] - public string? Name { get; set; } - - [Parameter, EditorRequired] - public long Id { get; set; } - - private bool _showConfirm; -} diff --git a/src/Modix.Web/Components/Configuration/Roles.razor b/src/Modix.Web/Components/Configuration/Roles.razor deleted file mode 100644 index b7241a06e..000000000 --- a/src/Modix.Web/Components/Configuration/Roles.razor +++ /dev/null @@ -1,171 +0,0 @@ -@using Modix.Data.Models.Core; -@using Modix.Services.Core; -@using Modix.Web.Models.Configuration; -@using Modix.Web.Models.UserLookup; -@using Modix.Web.Services; -@using MudBlazor -@using Humanizer; -@using System.Security.Claims; -@using Modix.Web.Models.Common; - -Modix - Roles -Role Designations - - - @if (DesignatedRoleMappings is not null && DesignatedRoleTypes is not null) - { - - - Assign a Role - - - - - Designation - - @foreach (var designation in DesignatedRoleTypes) - { - - } - - - - - Assign - - - Cancel - - - - - - @foreach (var designatedRoleType in DesignatedRoleTypes.OrderBy(x => x.ToString())) - { - -
-
- - @designatedRoleType.ToString().Titleize() - - @if (!DesignatedRoleMappings.TryGetValue(designatedRoleType, out var roleDesignations) || !roleDesignations.Any()) - { - - NONE ASSIGNED - - } - else - { - @foreach (var designatedRoleMapping in roleDesignations) - { - - } - } -
- -
- - - -
-
-
- - } -
-
- } -
- -@code { - [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; - - [Inject] - public IDesignatedRoleService DesignatedRoleService { get; set; } = null!; - - [Inject] - public ISnackbar Snackbar { get; set; } = null!; - - private Dictionary>? DesignatedRoleMappings { get; set; } - private DesignatedRoleType[]? DesignatedRoleTypes { get; set; } - - private bool _createDialogVisible; - private DesignatedRoleType? _selectedDesignatedRoleType; - private RoleInformation? _selectedRole; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; - - var currentGuild = DiscordHelper.GetUserGuild(); - var designatedRoles = await DesignatedRoleService.GetDesignatedRolesAsync(currentGuild.Id); - - DesignatedRoleMappings = designatedRoles - .Select(d => new DesignatedRoleData(d.Id, d.Role.Id, d.Type, currentGuild?.GetRole(d.Role.Id)?.Name ?? d.Role.Name)) - .ToLookup(x => x.RoleDesignation, x => x) - .ToDictionary(x => x.Key, x => x.ToList()); - - DesignatedRoleTypes = Enum.GetValues(); - - StateHasChanged(); - } - - public void ToggleCreateDialog() - { - _createDialogVisible = !_createDialogVisible; - if (_createDialogVisible) - { - _selectedRole = null; - _selectedDesignatedRoleType = null; - } - } - - private void SelectedRoleChanged(RoleInformation role) - { - _selectedRole = role; - } - - public async Task SaveDesignation() - { - var currentGuild = DiscordHelper.GetUserGuild(); - - var id = await DesignatedRoleService.AddDesignatedRoleAsync(currentGuild.Id, _selectedRole!.Id, _selectedDesignatedRoleType!.Value); - - _createDialogVisible = false; - - if (!DesignatedRoleMappings!.ContainsKey(_selectedDesignatedRoleType.Value)) - { - DesignatedRoleMappings[_selectedDesignatedRoleType.Value] = new List(); - } - - DesignatedRoleMappings[_selectedDesignatedRoleType.Value].Add(new DesignatedRoleData(id, _selectedRole.Id, _selectedDesignatedRoleType.Value, _selectedRole.Name)); - - Snackbar.Add($"Added designation '{_selectedDesignatedRoleType}' to role '{_selectedRole.Name}'", Severity.Success); - } - - public async Task RemoveDesignation(long id, DesignatedRoleType designatedRoleType) - { - await DesignatedRoleService.RemoveDesignatedRoleByIdAsync(id); - - var roleMappingsWithType = DesignatedRoleMappings![designatedRoleType]; - var removedRoleMapping = roleMappingsWithType.First(x => x.Id == id); - - roleMappingsWithType.Remove(removedRoleMapping); - - Snackbar.Add($"Removed designation '{designatedRoleType}' from role '{removedRoleMapping.Name}'", Severity.Success); - } -} diff --git a/src/Modix.Web/Components/Infractions.razor b/src/Modix.Web/Components/Infractions.razor deleted file mode 100644 index 5326bb96d..000000000 --- a/src/Modix.Web/Components/Infractions.razor +++ /dev/null @@ -1,458 +0,0 @@ -@using Modix.Data.Models; -@using Modix.Data.Models.Core; -@using Modix.Data.Models.Moderation; -@using Modix.Services.Moderation; -@using Modix.Web.Models; -@using Modix.Web.Models.Infractions; -@using Modix.Web.Services; -@using MudBlazor; -@using System.Security.Claims; -@using Modix.Web.Models.Common; - -Modix - Infractions - - - - - - Create Infraction - - - - - - @user.Name - - - - Infraction -
-
- - - - - - - - - - - - - - -
- -
- - @if (_infractionType == InfractionType.Mute) - { - Duration -
- - - - - -
- } -
- - - Save - - - Cancel - -
- - - - -
-
- Create - Refresh -
- -
- - -
-
- - - - - Id - - - - Type - - @foreach (var infractionType in Enum.GetValues()) - { - - } - - - - Created On - - - Subject - - - - Creator - - - Reason - @if (_showState) - { - State - } - @if (_canDeleteInfractions || _canRescind) - { - Actions - } - - - @infraction.Id - @infraction.Type - @infraction.CreateAction.Created.ToString("MM/dd/yy, h:mm:ss tt") - @(GetUsername(infraction.Subject)) - @(GetUsername(infraction.CreateAction.CreatedBy)) - @infraction.Reason - @if (_showState) - { - @(infraction.RescindAction != null ? "Rescinded" : infraction.DeleteAction != null ? "Deleted" : "Active") - } - @if (_canDeleteInfractions || _canRescind) - { - -
- @if (infraction.CanBeDeleted) - { - Delete - } - @if (infraction.CanBeRescind) - { - Rescind - } -
-
- } -
- - - -
-
-
-@code { - - [Parameter] - public string? Subject { get; set; } - - [Parameter] - public string? Id { get; set; } - - [Inject] - public ModerationService ModerationService { get; set; } = null!; - - [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; - - [Inject] - public ISnackbar Snackbar { get; set; } = null!; - - [Inject] - public IDialogService DialogService { get; set; } = null!; - - [Inject] - public SessionState SessionState { get; set; } = null!; - - [Inject] - public CookieService CookieService { get; set; } = null!; - - [CascadingParameter] - private Task? AuthState { get; set; } - - private MudTable? TableRef; - - private bool _showState; - - private bool _canRescind; - private bool _canDeleteInfractions; - - private ModixUser? _selectedUser; - private InfractionType _infractionType = InfractionType.Notice; - private string? _infractionReason; - private bool _createDialogVisible; - private int? _newInfractionMonths; - private int? _newInfractionDays; - private int? _newInfractionHours; - private int? _newInfractionMinutes; - private int? _newInfractionSeconds; - - private TableFilter _tableFilter = new(); - - protected override void OnInitialized() - { - _tableFilter.ShowDeleted = SessionState.ShowDeletedInfractions; - _showState = SessionState.ShowInfractionState; - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; - - if (AuthState is null) - return; - - var auth = await AuthState; - _canRescind = auth.User.HasClaim(ClaimTypes.Role, nameof(AuthorizationClaim.ModerationRescind)); - _canDeleteInfractions = auth.User.HasClaim(ClaimTypes.Role, nameof(AuthorizationClaim.ModerationDeleteInfraction)); - - _tableFilter.Subject = Subject; - _tableFilter.IdString = Id; - } - - private async Task ShowStateChanged(bool showState) - { - _showState = showState; - await CookieService.SetShowInfractionStateAsync(showState); - } - - private async Task ShowDeletedChanged(bool showDeleted) - { - await FilterChanged(() => _tableFilter.ShowDeleted = showDeleted); - await CookieService.SetShowDeletedInfractionsAsync(showDeleted); - } - - private void ToggleDialog() - { - _createDialogVisible = !_createDialogVisible; - } - - private void SelectedUserChanged(ModixUser user) - { - _selectedUser = user; - } - - private async Task FilterChanged(Action filterSetter) - { - filterSetter(); - await RefreshTable(); - } - - private static string GetUsername(GuildUserBrief userBrief) - { - return $"{userBrief.Username}{(userBrief.Discriminator == "0000" ? "" : "#" + userBrief.Discriminator)}"; - } - - private async Task RescindInfraction(InfractionData infraction) - { - try - { - var dialogParams = new DialogParameters - { - { x => x.Content, $"Are you sure you want to rescind infraction #{infraction.Id}?"} - }; - - var dialog = DialogService.Show("", dialogParams); - var result = await dialog.Result; - - if (result.Canceled) - { - Snackbar.Add("Action was cancelled", Severity.Info); - return; - } - - await ModerationService.RescindInfractionAsync(infraction.Id); - await RefreshTable(); - } - catch (Exception ex) - { - Snackbar.Add(ex.Message, Severity.Error); - } - } - - private async Task DeleteInfraction(InfractionData infraction) - { - try - { - var dialogParams = new DialogParameters - { - { x => x.Content, $"Are you sure you want to delete infraction #{infraction.Id}?"} - }; - - var dialog = DialogService.Show("", dialogParams); - var result = await dialog.Result; - - if (result.Canceled) - { - Snackbar.Add("Action was cancelled", Severity.Info); - return; - } - - await ModerationService.DeleteInfractionAsync(infraction.Id); - await RefreshTable(); - } - catch (Exception ex) - { - Snackbar.Add(ex.Message, Severity.Error); - } - } - - private async Task SaveInfraction() - { - _createDialogVisible = false; - - var duration = GetTimeSpan( - _newInfractionMonths, - _newInfractionDays, - _newInfractionHours, - _newInfractionMinutes, - _newInfractionSeconds); - - try - { - var currentUser = DiscordHelper.GetCurrentUser(); - await ModerationService.CreateInfractionAsync(currentUser!.Guild.Id, currentUser.Id, _infractionType, _selectedUser!.UserId, _infractionReason!, duration); - } - catch (InvalidOperationException ex) - { - Snackbar.Add(ex.Message, Severity.Error); - return; - } - - - Snackbar.Add($"Added infraction for user {_selectedUser!.Name}", Severity.Success); - - _selectedUser = null; - _newInfractionMonths = null; - _newInfractionDays = null; - _newInfractionHours = null; - _newInfractionMinutes = null; - _newInfractionSeconds = null; - _infractionReason = null; - - await RefreshTable(); - - TimeSpan? GetTimeSpan(int? months, int? days, int? hours, int? minutes, int? seconds) - { - if (months is null - && days is null - && hours is null - && minutes is null - && seconds is null) - return null; - - var now = DateTimeOffset.UtcNow; - var daysInMonth = DateTime.DaysInMonth(now.Year, now.Month); - - var monthSpan = months is null - ? TimeSpan.Zero - : TimeSpan.FromDays(months.Value * daysInMonth); - - var daySpan = days is null - ? TimeSpan.Zero - : TimeSpan.FromDays(days.Value); - - var hourSpan = hours is null - ? TimeSpan.Zero - : TimeSpan.FromHours(hours.Value); - - var minuteSpan = minutes is null - ? TimeSpan.Zero - : TimeSpan.FromMinutes(minutes.Value); - - var secondSpan = seconds is null - ? TimeSpan.Zero - : TimeSpan.FromSeconds(seconds.Value); - - return monthSpan + daySpan + hourSpan + minuteSpan + secondSpan; - } - } - - private async Task RefreshTable() - { - if (TableRef is null) - return; - - await TableRef.ReloadServerData(); - } - - private async Task> LoadInfractions(TableState tableState) - { - var currentUser = DiscordHelper.GetCurrentUser(); - - var sortingCriteria = new[] - { - new SortingCriteria() - { - PropertyName = tableState.SortLabel ?? nameof(InfractionData.Id), - Direction = tableState.SortDirection == MudBlazor.SortDirection.Ascending - ? Data.Models.SortDirection.Ascending - : Data.Models.SortDirection.Descending, - } - }; - - var searchCriteria = new InfractionSearchCriteria - { - GuildId = currentUser!.Guild.Id, - Id = _tableFilter.Id, - Types = _tableFilter.Types, - Subject = _tableFilter.Subject, - SubjectId = _tableFilter.SubjectId, - Creator = _tableFilter.Creator, - CreatedById = _tableFilter.CreatedById, - IsDeleted = _tableFilter.ShowDeleted ? null : false - }; - - var pagingCriteria = new PagingCriteria - { - FirstRecordIndex = tableState.Page * tableState.PageSize, - PageSize = tableState.PageSize, - }; - - var result = await ModerationService.SearchInfractionsAsync( - searchCriteria, - sortingCriteria, - pagingCriteria); - - var outranksValues = new Dictionary(); - - foreach (var (guildId, subjectId) in result.Records - .Select(x => (guildId: x.GuildId, subjectId: x.Subject.Id)) - .Distinct()) - { - outranksValues[subjectId] - = await ModerationService.DoesModeratorOutrankUserAsync(guildId, currentUser.Id, subjectId); - } - - var mapped = result.Records.Select(x => InfractionData.FromInfractionSummary(x, outranksValues)).ToArray(); - - return new TableData - { - Items = mapped, - TotalItems = mapped.Length - }; - } -} diff --git a/src/Modix.Web/Controllers/AutocompleteController.cs b/src/Modix.Web/Controllers/AutocompleteController.cs new file mode 100644 index 000000000..b97ef3f0d --- /dev/null +++ b/src/Modix.Web/Controllers/AutocompleteController.cs @@ -0,0 +1,88 @@ +using Discord; +using Discord.WebSocket; +using Microsoft.AspNetCore.Mvc; +using Modix.Data.Utilities; +using Modix.Services.Core; +using Modix.Services.Utilities; +using Modix.Web.Shared.Models.Common; + +namespace Modix.Web.Controllers; + +[Route("~/api/autocomplete")] +[ApiController] +public class AutocompleteController : ModixController +{ + private readonly IUserService _userService; + + public AutocompleteController(DiscordSocketClient discordSocketClient, IUserService userService, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _userService = userService; + } + + [HttpGet("users/{query}")] + public async Task> AutocompleteUsersAsync(string query) + { + var result = UserGuild.Users + .Where(d => d.Username.OrdinalContains(query) || d.Id.ToString() == query) + .Take(10) + .Select(FromIGuildUser); + + if (result.Any() || !ulong.TryParse(query, out var userId)) + return result; + + var user = await _userService.GetUserInformationAsync(UserGuild.Id, userId); + + if (user is not null) + return [ FromNonGuildUser(user) ]; + + return []; + } + + [HttpGet("channels/{query}")] + public IEnumerable AutoCompleteChannels(string query) + { + if (query.StartsWith('#')) + { + query = query[1..]; + } + + return UserGuild.Channels + .Where(d => d is SocketTextChannel + && d.Name.Contains(query, StringComparison.OrdinalIgnoreCase)) + .Take(10) + .Select(d => new ChannelInformation(d.Id, d.Name)); + } + + [HttpGet("roles/{query}")] + public IEnumerable AutoCompleteRoles(string query) + { + if (query.StartsWith('@')) + { + query = query[1..]; + } + + IEnumerable result = UserGuild.Roles; + + if (!string.IsNullOrWhiteSpace(query)) + { + result = result.Where(d => d.Name.Contains(query, StringComparison.OrdinalIgnoreCase)); + } + + return result.Take(10).Select(d => new RoleInformation(d.Id, d.Name, d.Color.ToString())); + } + + public static ModixUser FromIGuildUser(IGuildUser user) => new() + { + Name = user.GetDisplayName(), + UserId = user.Id, + AvatarUrl = user.GetDisplayAvatarUrl() ?? user.GetDefaultAvatarUrl() + }; + + public static ModixUser FromNonGuildUser(IUser user) => new() + { + Name = user.GetDisplayName(), + UserId = user.Id, + AvatarUrl = user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl() + }; +} diff --git a/src/Modix.Web/Controllers/CampaignController.cs b/src/Modix.Web/Controllers/CampaignController.cs new file mode 100644 index 000000000..d865b89ba --- /dev/null +++ b/src/Modix.Web/Controllers/CampaignController.cs @@ -0,0 +1,134 @@ +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Models.Core; +using Modix.Services.Promotions; +using Modix.Services.Utilities; +using Modix.Web.Shared.Models.Promotions; + +namespace Modix.Web.Controllers; + +[Route("~/api/campaigns")] +[ApiController] +[Authorize(Roles = nameof(AuthorizationClaim.PromotionsRead))] +public class CampaignController : ModixController +{ + private readonly IPromotionsService _promotionsService; + + public CampaignController(IPromotionsService promotionsService, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _promotionsService = promotionsService; + } + + [HttpGet] + public async IAsyncEnumerable GetCampaignsAsync() + { + var campaigns = await _promotionsService.SearchCampaignsAsync(new Data.Models.Promotions.PromotionCampaignSearchCriteria + { + GuildId = UserGuild.Id + }); + + foreach (var campaign in campaigns) + { + yield return new PromotionCampaignData + { + Id = campaign.Id, + SubjectId = campaign.Subject.Id, + SubjectName = campaign.Subject.GetFullUsername(), + TargetRoleId = campaign.TargetRole.Id, + TargetRoleName = campaign.TargetRole.Name, + Outcome = campaign.Outcome, + Created = campaign.CreateAction.Created, + IsCurrentUserCampaign = campaign.Subject.Id == SocketUser.Id, + ApproveCount = campaign.ApproveCount, + OpposeCount = campaign.OpposeCount, + IsClosed = campaign.CloseAction is not null + }; + } + } + + [HttpGet("{campaignId}")] + public async Task GetCampaignCommentsAsync(long campaignId) + { + var campaignDetails = await _promotionsService.GetCampaignDetailsAsync(campaignId); + + if (campaignDetails is null) + return NotFound(); + + var campaignComments = campaignDetails.Comments + .Where(x => x.ModifyAction is null) + .Select(x => new CampaignCommentData + ( + x.Id, + x.Sentiment, + x.Content, + x.CreateAction.Created, + x.CreateAction.CreatedBy.Id == SocketUser.Id + ) + ) + .ToDictionary(x => x.Id); + + return Ok(campaignComments); + } + + [HttpPut("{campaignId}/createcomment")] + public async Task CreateCommentAsync(long campaignId, [FromBody] CampaignCommentData campaignCommentData) + { + var promotionActionSummary = await _promotionsService.AddCommentAsync( + campaignId, + campaignCommentData.PromotionSentiment, + campaignCommentData.Content); + + var newComment = promotionActionSummary.NewComment; + + return new CampaignCommentData(newComment.Id, newComment.Sentiment, newComment.Content, promotionActionSummary.Created, true); + } + + [HttpPatch("updatecomment")] + public async Task UpdateCommentAsync([FromBody] CampaignCommentData campaignCommentData) + { + var promotionActionSummary = await _promotionsService.UpdateCommentAsync( + campaignCommentData.Id, + campaignCommentData.PromotionSentiment, + campaignCommentData.Content); + + var newComment = promotionActionSummary.NewComment; + + return new CampaignCommentData(newComment.Id, newComment.Sentiment, newComment.Content, promotionActionSummary.Created, true); + } + + [HttpPost("{campaignId}/accept/{force}")] + [Authorize(Roles = nameof(AuthorizationClaim.PromotionsCloseCampaign))] + public async Task AcceptCampaignAsync(long campaignId, bool force) + { + await _promotionsService.AcceptCampaignAsync(campaignId, force); + } + + [HttpPost("{campaignId}/reject")] + [Authorize(Roles = nameof(AuthorizationClaim.PromotionsCloseCampaign))] + public async Task RejectCampaignAsync(long campaignId) + { + await _promotionsService.RejectCampaignAsync(campaignId); + } + + [HttpGet("{subjectId}/nextrank")] + [Authorize(Roles = nameof(AuthorizationClaim.PromotionsCreateCampaign))] + public async Task GetNextRankRoleForUserAsync(ulong subjectId) + { + var nextRank = await _promotionsService.GetNextRankRoleForUserAsync(subjectId); + + if (nextRank is null) + return new NextRank("None", "#607d8b"); + + var color = UserGuild.Roles.First(r => r.Id == nextRank.Id).Color; + return new NextRank(nextRank.Name, color.ToString()); + } + + [HttpPut("create")] + [Authorize(Roles = nameof(AuthorizationClaim.PromotionsCreateCampaign))] + public async Task CreateAsync([FromBody] PromotionCreationData creationData) + { + await _promotionsService.CreateCampaignAsync(creationData.UserId, creationData.Comment); + } +} diff --git a/src/Modix.Web/Controllers/ClaimsController.cs b/src/Modix.Web/Controllers/ClaimsController.cs new file mode 100644 index 000000000..3b0ae1c5b --- /dev/null +++ b/src/Modix.Web/Controllers/ClaimsController.cs @@ -0,0 +1,42 @@ +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Data.Models.Core; +using Modix.Data.Repositories; +using Modix.Models.Core; +using Modix.Web.Shared.Models.Configuration; + +namespace Modix.Web.Controllers; + +[Route("~/api/config/claims")] +[ApiController] +[Authorize(Roles = nameof(AuthorizationClaim.AuthorizationConfigure))] +public class ClaimsController : ModixController +{ + private readonly IClaimMappingRepository _claimMappingRepository; + + public ClaimsController(IClaimMappingRepository claimMappingRepository, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _claimMappingRepository = claimMappingRepository; + } + + [HttpGet] + public async Task> GetMappedClaimsAsync() + { + var mappedClaims = await _claimMappingRepository.SearchBriefsAsync(new ClaimMappingSearchCriteria + { + IsDeleted = false, + GuildId = UserGuild.Id + }); + + return mappedClaims + .Select(x => new ClaimMappingData(x.RoleId, x.Claim, x.Type)); + } + + [HttpPatch("{roleId}/{authorizationClaim}/{claimMappingType?}")] + public async Task ModifyClaimMappingAsync(ulong roleId, AuthorizationClaim authorizationClaim, ClaimMappingType? claimMappingType) + { + await ModixAuth.ModifyClaimMappingAsync(roleId, authorizationClaim, claimMappingType); + } +} diff --git a/src/Modix.Web/Controllers/CommandsController.cs b/src/Modix.Web/Controllers/CommandsController.cs new file mode 100644 index 000000000..ef3e9ebff --- /dev/null +++ b/src/Modix.Web/Controllers/CommandsController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Modix.Services.CommandHelp; +using Modix.Services.Utilities; +using Modix.Web.Shared.Models.Commands; + +namespace Modix.Web.Controllers; + +[Route("~/api")] +[ApiController] +public class CommandsController : ControllerBase +{ + private readonly ICommandHelpService _commandHelpService; + + public CommandsController(ICommandHelpService commandHelpService) + { + _commandHelpService = commandHelpService; + } + + [HttpGet("commands")] + public IEnumerable Commands() + { + IEnumerable ModulesStream() + { + var modules = _commandHelpService.GetModuleHelpData(); + + var mapped = modules.Select(m => + { + var commands = m.Commands.Select(c => + { + var parameters = c.Parameters.Select(p => new Parameter(p.Name, p.Summary, p.Options, p.Type, p.IsOptional)); + + return new Command(c.Name, c.Summary, FormatUtilities.CollapsePlurals(c.Aliases), [.. parameters], c.IsSlashCommand); + }); + + return new Module(m.Name, m.Summary, commands); + }); + + foreach (var module in mapped) + { + yield return module; + } + } + + return ModulesStream(); + } +} diff --git a/src/Modix.Web/Controllers/DeletedMessagesController.cs b/src/Modix.Web/Controllers/DeletedMessagesController.cs new file mode 100644 index 000000000..2178b410a --- /dev/null +++ b/src/Modix.Web/Controllers/DeletedMessagesController.cs @@ -0,0 +1,143 @@ +using Discord; +using Discord.WebSocket; +using Humanizer.Bytes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Data.Models; +using Modix.Data.Models.Moderation; +using Modix.Models; +using Modix.Models.Core; +using Modix.Services.Moderation; +using Modix.Services.Utilities; +using Modix.Web.Shared.Models.DeletedMessages; + +namespace Modix.Web.Controllers; + +[Route("~/api/deletedmessages")] +[ApiController] +[Authorize(Roles = nameof(AuthorizationClaim.LogViewDeletedMessages))] +public class DeletedMessagesController : ModixController +{ + private readonly ModerationService _moderationService; + + public DeletedMessagesController(ModerationService moderationService, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _moderationService = moderationService; + } + + [HttpPut] + public async Task> GetDeletedMessagesBatchAsync(DeletedMessagesQuery deletedMessagesQuery) + { + var tableState = deletedMessagesQuery.TableState; + var tableFilter = deletedMessagesQuery.Filter; + + var searchCriteria = new DeletedMessageSearchCriteria + { + GuildId = UserGuild.Id, + Channel = tableFilter.Channel, + ChannelId = tableFilter.ChannelId, + Author = tableFilter.Author, + AuthorId = tableFilter.AuthorId, + CreatedBy = tableFilter.CreatedBy, + CreatedById = tableFilter.CreatedById, + Content = tableFilter.Content, + Reason = tableFilter.Reason, + BatchId = tableFilter.BatchId + }; + + var sortingCriteria = new SortingCriteria + { + PropertyName = tableState.SortLabel ?? nameof(DeletedMessageSummary.Created), + Direction = tableState.SortDirection == MudBlazor.SortDirection.Ascending + ? Data.Models.SortDirection.Ascending + : Data.Models.SortDirection.Descending + }; + + var pagingCriteria = new PagingCriteria + { + FirstRecordIndex = tableState.Page * tableState.PageSize, + PageSize = tableState.PageSize, + }; + + var deletedMessages = await _moderationService.SearchDeletedMessagesAsync(searchCriteria, [sortingCriteria], pagingCriteria); + + var deletedMessagesBatchInformation = deletedMessages.Records + .Select(x => new DeletedMessageBatchInformation( + x.Channel.Name, + x.Author.GetFullUsername(), + x.Created, + x.CreatedBy.GetFullUsername(), + x.Content, + x.Reason, + x.BatchId)) + .ToArray(); + + return new RecordsPage + { + FilteredRecordCount = deletedMessagesBatchInformation.Length, + Records = deletedMessagesBatchInformation, + TotalRecordCount = deletedMessages.TotalRecordCount + }; + } + + [HttpGet("{batchId}")] + public async Task GetDeletedMessageContext(long batchId) + { + + var searchCriteria = new DeletedMessageSearchCriteria + { + BatchId = batchId + }; + + var sortingCriteria = new SortingCriteria + { + PropertyName = nameof(DeletedMessageSummary.MessageId), + + //Sort ascending, so the earliest message is first + Direction = Data.Models.SortDirection.Ascending + }; + + var deletedMessages = await _moderationService.SearchDeletedMessagesAsync(searchCriteria, [sortingCriteria], new()); + var firstMessage = deletedMessages.Records.FirstOrDefault(); + + if (firstMessage is null) + return NotFound($"Couldn't find messages for batch id {batchId}"); + + var batchChannelId = firstMessage.Channel.Id; + if (SocketUser.Guild.GetChannel(batchChannelId) is not ISocketMessageChannel foundChannel) + return NotFound($"Couldn't recreate context - text channel with id {batchChannelId} not found"); + + if (!SocketUser.GetPermissions(foundChannel as IGuildChannel).ReadMessageHistory) + return Unauthorized($"You don't have read permissions for the channel this batch was deleted in (#{foundChannel.Name})"); + + var beforeMessages = await foundChannel.GetMessagesAsync(firstMessage.MessageId, Discord.Direction.Before, 25).FlattenAsync(); + var afterMessages = await foundChannel.GetMessagesAsync(firstMessage.MessageId, Discord.Direction.After, 25 + (int)deletedMessages.FilteredRecordCount).FlattenAsync(); + + var allMessages = new List(); + allMessages.AddRange(deletedMessages.Records.Select(d => new DeletedMessageInformation(d.MessageId, null, null, d.Author.GetFullUsername(), d.Content))); + allMessages.AddRange(beforeMessages.Select(FromIMessage)); + allMessages.AddRange(afterMessages.Select(FromIMessage)); + + return Ok(allMessages); + } + + private static DeletedMessageInformation FromIMessage(IMessage message) + { + var content = message.Content; + + if (string.IsNullOrWhiteSpace(content)) + { + if (message.Embeds.Count > 0) + { + content = $"Embed: {message.Embeds.First().Title}: {message.Embeds.First().Description}"; + } + else if (message.Attachments.Count > 0) + { + content = $"Attachment: {message.Attachments.First().Filename} {ByteSize.FromBytes(message.Attachments.First().Size)}"; + } + } + + return new DeletedMessageInformation(message.Id, message.CreatedAt, message.GetJumpUrl(), message.Author.GetDisplayName(), content); + } +} diff --git a/src/Modix.Web/Controllers/DesignatedChannelController.cs b/src/Modix.Web/Controllers/DesignatedChannelController.cs new file mode 100644 index 000000000..1ea0a2c89 --- /dev/null +++ b/src/Modix.Web/Controllers/DesignatedChannelController.cs @@ -0,0 +1,59 @@ +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Models.Core; +using Modix.Services; +using Modix.Web.Shared.Models.Configuration; + +namespace Modix.Web.Controllers; + +[Route("~/api/config/channels")] +[ApiController] +[Authorize] +public class DesignatedChannelController : ModixController +{ + private readonly DesignatedChannelService _designatedChannelService; + + public DesignatedChannelController(DesignatedChannelService designatedChannelService, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _designatedChannelService = designatedChannelService; + } + + [HttpGet] + [Authorize(Roles = nameof(AuthorizationClaim.DesignatedChannelMappingRead))] + public async Task>> GetChannelDesignationsAsync() + { + var designatedChannels = await _designatedChannelService.GetDesignatedChannels(UserGuild.Id); + + return designatedChannels + .Select(d => new DesignatedChannelData( + d.Id, + d.Channel.Id, + d.Type, + UserGuild.GetChannel(d.Channel.Id)?.Name ?? d.Channel.Name)) + .ToLookup(x => x.ChannelDesignation, x => x) + .ToDictionary(x => x.Key, x => x.ToList()); + } + + [HttpPut("{channelId}/{designatedChannelType}")] + [Authorize(Roles = nameof(AuthorizationClaim.DesignatedChannelMappingCreate))] + public async Task CreateDesignationAsync(ulong channelId, DesignatedChannelType designatedChannelType) + { + var foundChannel = UserGuild.GetChannel(channelId); + + if (foundChannel is not ISocketMessageChannel messageChannel) + return BadRequest($"A message channel was not found with id {channelId} in guild with id {UserGuild.Id}"); + + var id = await _designatedChannelService.AddDesignatedChannel(foundChannel.Guild, messageChannel, designatedChannelType); + + return Ok(id); + } + + [HttpDelete("{id}")] + [Authorize(Roles = nameof(AuthorizationClaim.DesignatedChannelMappingDelete))] + public async Task RemoveDesignationAsync(long id) + { + await _designatedChannelService.RemoveDesignatedChannelById(id); + } +} diff --git a/src/Modix.Web/Controllers/DesignatedRoleController.cs b/src/Modix.Web/Controllers/DesignatedRoleController.cs new file mode 100644 index 000000000..f4db94f13 --- /dev/null +++ b/src/Modix.Web/Controllers/DesignatedRoleController.cs @@ -0,0 +1,59 @@ +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Models.Core; +using Modix.Services.Core; +using Modix.Web.Shared.Models.Configuration; + +namespace Modix.Web.Controllers; + +[Route("~/api/config/roles")] +[ApiController] +[Authorize] +public class DesignatedRoleController : ModixController +{ + private readonly IDesignatedRoleService _designatedRoleService; + + public DesignatedRoleController(IDesignatedRoleService designatedRoleService, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _designatedRoleService = designatedRoleService; + } + + [HttpGet] + [Authorize(Roles = nameof(AuthorizationClaim.DesignatedRoleMappingRead))] + public async Task>> GetRoleDesignationsAsync() + { + var designatedRoles = await _designatedRoleService.GetDesignatedRolesAsync(UserGuild.Id); + + return designatedRoles + .Select(d => new DesignatedRoleData( + d.Id, + d.Role.Id, + d.Type, + UserGuild.GetRole(d.Role.Id)?.Name ?? d.Role.Name)) + .ToLookup(x => x.RoleDesignation, x => x) + .ToDictionary(x => x.Key, x => x.ToList()); + } + + [HttpPut("{roleId}/{designatedRoleType}")] + [Authorize(Roles = nameof(AuthorizationClaim.DesignatedRoleMappingCreate))] + public async Task CreateDesignationAsync(ulong roleId, DesignatedRoleType designatedRoleType) + { + var foundRole = UserGuild.GetRole(roleId); + + if (foundRole is null) + return BadRequest($"A role was not found with id {roleId} in guild with id {ModixAuth.CurrentGuildId}"); + + var id = await _designatedRoleService.AddDesignatedRoleAsync(UserGuild.Id, roleId, designatedRoleType); + + return Ok(id); + } + + [HttpDelete("{id}")] + [Authorize(Roles = nameof(AuthorizationClaim.DesignatedRoleMappingDelete))] + public async Task RemoveDesignationAsync(long id) + { + await _designatedRoleService.RemoveDesignatedRoleByIdAsync(id); + } +} diff --git a/src/Modix.Web/Controllers/GuildController.cs b/src/Modix.Web/Controllers/GuildController.cs new file mode 100644 index 000000000..b0bb7ec72 --- /dev/null +++ b/src/Modix.Web/Controllers/GuildController.cs @@ -0,0 +1,27 @@ +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Web.Shared.Models; + +namespace Modix.Web.Controllers; + +[Route("~/api/guild")] +[ApiController] +[Authorize] +public class GuildController : ModixController +{ + public GuildController(DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + } + + [HttpGet("guildoptions")] + public GuildOption[] GuildOptions() + { + return DiscordSocketClient + .Guilds + .Where(d => d.GetUser(SocketUser?.Id ?? 0) != null) + .Select(d => new GuildOption(d.Id, d.Name, d.IconUrl)) + .ToArray(); + } +} diff --git a/src/Modix.Web/Controllers/GuildStatsController.cs b/src/Modix.Web/Controllers/GuildStatsController.cs new file mode 100644 index 000000000..f2ca49b9b --- /dev/null +++ b/src/Modix.Web/Controllers/GuildStatsController.cs @@ -0,0 +1,36 @@ +using System.Security.Claims; +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Services.Core; +using Modix.Services.GuildStats; +using Modix.Web.Models; +using Modix.Web.Shared.Models.Stats; + +namespace Modix.Web.Controllers; + +[Route("~/api")] +[ApiController] +[Authorize] +public class GuildStatsController : ModixController +{ + private readonly IGuildStatService _guildStatService; + + public GuildStatsController(IGuildStatService guildStatService, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _guildStatService = guildStatService; + } + + [HttpGet("guildstats")] + public async Task GuildStatsAsync() + { + var roleCounts = await _guildStatService.GetGuildMemberDistributionAsync(UserGuild); + var messageCounts = await _guildStatService.GetTopMessageCounts(UserGuild, SocketUser.Id); + + var guildRoleCounts = roleCounts.Select(x => new GuildRoleMemberCount(x.Name, x.Count, x.Color)); + var topUserMessageCounts = messageCounts.Select(x => new PerUserMessageCount(x.Username, x.Discriminator, x.Rank, x.MessageCount, x.IsCurrentUser)); + + return new GuildStatData(UserGuild.Name, [..guildRoleCounts], [..topUserMessageCounts]); + } +} diff --git a/src/Modix.Web/Controllers/InfractionsController.cs b/src/Modix.Web/Controllers/InfractionsController.cs new file mode 100644 index 000000000..bccc7e6fa --- /dev/null +++ b/src/Modix.Web/Controllers/InfractionsController.cs @@ -0,0 +1,128 @@ +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Data.Models; +using Modix.Data.Models.Moderation; +using Modix.Models; +using Modix.Models.Core; +using Modix.Services.Moderation; +using Modix.Web.Shared.Models.Infractions; + +namespace Modix.Web.Controllers; + +[Route("~/api/infractions")] +[ApiController] +[Authorize] +public class InfractionsController : ModixController +{ + private readonly ModerationService _moderationService; + + public InfractionsController(ModerationService moderationService, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _moderationService = moderationService; + } + + [HttpPut] + [Authorize(Roles = nameof(AuthorizationClaim.ModerationRead))] + public async Task> GetInfractionsAsync(InfractionsQuery infractionsQuery) + { + var tableState = infractionsQuery.TableState; + var tableFilter = infractionsQuery.Filter; + + var sortingCriteria = new[] + { + new SortingCriteria + { + PropertyName = tableState.SortLabel ?? nameof(InfractionData.Id), + Direction = tableState.SortDirection == MudBlazor.SortDirection.Ascending + ? Data.Models.SortDirection.Ascending + : Data.Models.SortDirection.Descending, + } + }; + + var searchCriteria = new InfractionSearchCriteria + { + GuildId = UserGuild.Id, + Id = tableFilter.Id, + Types = tableFilter.Types, + Subject = tableFilter.Subject, + SubjectId = tableFilter.SubjectId, + Creator = tableFilter.Creator, + CreatedById = tableFilter.CreatedById, + IsDeleted = tableFilter.ShowDeleted ? null : false + }; + + var pagingCriteria = new PagingCriteria + { + FirstRecordIndex = tableState.Page * tableState.PageSize, + PageSize = tableState.PageSize, + }; + + var infractions = await _moderationService.SearchInfractionsAsync(searchCriteria, sortingCriteria, pagingCriteria); + var outranksValues = new Dictionary(); + + foreach (var (guildId, subjectId) in infractions.Records.Select(x => (guildId: x.GuildId, subjectId: x.Subject.Id))) + { + outranksValues[subjectId] = await _moderationService.DoesModeratorOutrankUserAsync(guildId, SocketUser.Id, subjectId); + } + + var infractionData = infractions.Records + .Select(x => new InfractionData( + x.Id, + x.GuildId, + x.Type, + x.Reason, + x.Duration, + x.Subject.Username, + x.CreateAction.CreatedBy.Username, + x.CreateAction.Created, + + x.RescindAction is not null, + x.DeleteAction is not null, + + x.RescindAction is null + && x.DeleteAction is null + && (x.Type == Modix.Models.Moderation.InfractionType.Mute || x.Type == Modix.Models.Moderation.InfractionType.Ban) + && outranksValues[x.Subject.Id], + + x.DeleteAction is null + && outranksValues[x.Subject.Id] + )) + .ToArray(); + + return new RecordsPage + { + FilteredRecordCount = infractions.Records.Count, + Records = infractionData, + TotalRecordCount = infractions.TotalRecordCount, + }; + } + + [Authorize(Roles = nameof(AuthorizationClaim.ModerationRescind))] + [HttpPost("rescind/{infractionId}")] + public async Task RescindInfractionAsync(long infractionId) + { + await _moderationService.RescindInfractionAsync(infractionId); + } + + [Authorize(Roles = nameof(AuthorizationClaim.ModerationDeleteInfraction))] + [HttpDelete("{infractionId}")] + public async Task DeleteInfractionAsync(long infractionId) + { + await _moderationService.DeleteInfractionAsync(infractionId); + } + + [Authorize(Roles = $"{nameof(AuthorizationClaim.ModerationNote)},{nameof(AuthorizationClaim.ModerationWarn)},{nameof(AuthorizationClaim.ModerationBan)},{nameof(AuthorizationClaim.ModerationMute)}")] + [HttpPost("create")] + public async Task CreateInfractionAsync(Shared.Models.Infractions.InfractionCreationData creationData) + { + await _moderationService.CreateInfractionAsync( + UserGuild.Id, + SocketUser.Id, + creationData.Type, + creationData.SubjectId, + creationData.Reason, + creationData.Duration); + } +} diff --git a/src/Modix.Web/Controllers/ModixController.cs b/src/Modix.Web/Controllers/ModixController.cs new file mode 100644 index 000000000..c745f9214 --- /dev/null +++ b/src/Modix.Web/Controllers/ModixController.cs @@ -0,0 +1,69 @@ +using System.Security.Claims; +using Discord.WebSocket; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Modix.Web.Models; + +namespace Modix.Web.Controllers; + +[Authorize] +public class ModixController : Controller +{ + protected DiscordSocketClient DiscordSocketClient { get; private set; } + protected SocketGuildUser SocketUser { get; private set; } + protected SocketGuild UserGuild => SocketUser.Guild; + + protected Modix.Services.Core.IAuthorizationService ModixAuth { get; private set; } + + public ModixController(DiscordSocketClient client, Modix.Services.Core.IAuthorizationService modixAuth) + { + DiscordSocketClient = client; + ModixAuth = modixAuth; + } + + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (!DiscordSocketClient.Guilds.Any()) + return; + + if (User is null) + { + await HttpContext.ChallengeAsync(); + return; + } + + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (!ulong.TryParse(userId, out var userSnowflake)) + { + await HttpContext.ChallengeAsync(); + return; + } + + var guildCookie = Request.Cookies[CookieConstants.SelectedGuild]; + SocketGuild guildToSearch; + + if (!string.IsNullOrWhiteSpace(guildCookie)) + { + var guildId = ulong.Parse(guildCookie); + guildToSearch = DiscordSocketClient.GetGuild(guildId); + } + else + { + guildToSearch = DiscordSocketClient.Guilds.First(); + } + + SocketUser = guildToSearch.GetUser(userSnowflake); + + if (SocketUser is null) + { + await HttpContext.ChallengeAsync(); + return; + } + + await ModixAuth.OnAuthenticatedAsync(SocketUser.Id, SocketUser.Guild.Id, [.. SocketUser.Roles.Select(x => x.Id)]); + + await next(); + } +} diff --git a/src/Modix.Web/Controllers/RolesController.cs b/src/Modix.Web/Controllers/RolesController.cs new file mode 100644 index 000000000..36b25b925 --- /dev/null +++ b/src/Modix.Web/Controllers/RolesController.cs @@ -0,0 +1,34 @@ +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Modix.Web.Shared.Models.Common; + +namespace Modix.Web.Controllers; + +[Route("~/api/roles")] +[ApiController] +[Authorize] +public class RolesController : ModixController +{ + private readonly IMemoryCache _memoryCache; + + public RolesController(IMemoryCache memoryCache, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _memoryCache = memoryCache; + } + + [HttpGet] + public Dictionary GetRoles() + { + return _memoryCache.GetOrCreate("roles", cacheEntry => + { + cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10); + + return UserGuild.Roles + .Select(x => new RoleInformation(x.Id, x.Name, x.Color.ToString())) + .ToDictionary(x => x.Id); + })!; + } +} diff --git a/src/Modix.Web/Controllers/TagsController.cs b/src/Modix.Web/Controllers/TagsController.cs new file mode 100644 index 000000000..e5466f464 --- /dev/null +++ b/src/Modix.Web/Controllers/TagsController.cs @@ -0,0 +1,67 @@ +using System.Diagnostics; +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Data.Models.Tags; +using Modix.Models.Core; +using Modix.Services.Tags; +using Modix.Web.Shared.Models.Tags; + +namespace Modix.Web.Controllers; + +[Route("~/api/tags")] +[ApiController] +[Authorize(Roles = nameof(AuthorizationClaim.UseTag))] +public class TagsController : ModixController +{ + private readonly ITagService _tagService; + + public TagsController(ITagService tagService, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _tagService = tagService; + } + + [HttpGet] + public async Task> GetTagsAsync() + { + var summaries = await _tagService.GetSummariesAsync(new TagSearchCriteria + { + GuildId = UserGuild.Id, + }); + + return summaries.Select(CreateFromSummary); + } + + [HttpPut] + [Authorize(Roles = nameof(AuthorizationClaim.CreateTag))] + public async Task CreateTagAsync([FromBody] TagCreationData tagCreationData) + { + try + { + await _tagService.CreateTagAsync(UserGuild.Id, SocketUser.Id, tagCreationData.Name, tagCreationData.Content); + var createdTag = await _tagService.GetTagAsync(UserGuild.Id, tagCreationData.Name); + + var tagSummary = CreateFromSummary(createdTag); + + return Ok(tagSummary); + } + catch (Exception) + { + return BadRequest(); + } + } + + private static TagData CreateFromSummary(TagSummary summary) + { + return new TagData( + summary.Name, + summary.CreateAction.Created, + summary.OwnerRole is not null, + summary.OwnerUser?.Id ?? summary.OwnerRole?.Id ?? throw new UnreachableException("No owner??"), + summary.OwnerUser?.Username ?? summary.OwnerRole?.Name ?? throw new UnreachableException("No owner??"), + summary.Content, + summary.Uses, + false); + } +} diff --git a/src/Modix.Web/Controllers/UserInformationController.cs b/src/Modix.Web/Controllers/UserInformationController.cs new file mode 100644 index 000000000..75f2f3bae --- /dev/null +++ b/src/Modix.Web/Controllers/UserInformationController.cs @@ -0,0 +1,97 @@ +using Discord; +using Discord.WebSocket; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Modix.Data.Models.Core; +using Modix.Data.Repositories; +using Modix.Services.Core; +using Modix.Services.Utilities; +using Modix.Web.Shared.Models.Common; +using Modix.Web.Shared.Models.UserLookup; + +namespace Modix.Web.Controllers; + +[Route("~/api/userinformation")] +[ApiController] +[Authorize] +public class UserInformationController : ModixController +{ + private readonly IUserService _userService; + private readonly IMessageRepository _messageRepository; + + public UserInformationController(IUserService userService, IMessageRepository messageRepository, DiscordSocketClient discordSocketClient, Modix.Services.Core.IAuthorizationService authorizationService) + : base(discordSocketClient, authorizationService) + { + _userService = userService; + _messageRepository = messageRepository; + } + + [HttpGet("{userIdString}")] + public async Task GetUserInformationAsync(string userIdString) + { + if (!ulong.TryParse(userIdString, out var userId)) + return null; + + var userInformation = await _userService.GetUserInformationAsync(UserGuild.Id, userId); + if (userInformation is null) + return null; + + var userRank = await _messageRepository.GetGuildUserParticipationStatistics(UserGuild.Id, userId); + var messages7 = await _messageRepository.GetGuildUserMessageCountByDate(UserGuild.Id, userId, TimeSpan.FromDays(7)); + var messages30 = await _messageRepository.GetGuildUserMessageCountByDate(UserGuild.Id, userId, TimeSpan.FromDays(30)); + + var roles = userInformation.RoleIds + .Select(x => UserGuild.GetRole(x)) + .OrderByDescending(x => x.IsHoisted) + .ThenByDescending(x => x.Position) + .ToArray(); + + return FromEphemeralUser(userInformation, userRank, messages7, messages30, roles); + } + + [HttpGet("{userIdString}/messages")] + public async Task GetUserMessagesPerChannelAsync(string userIdString, DateTimeOffset after = default) + { + if (!ulong.TryParse(userIdString, out var userId)) + return []; + + var timespan = DateTimeOffset.UtcNow - after; + var result = await _messageRepository.GetGuildUserMessageCountByChannel(UserGuild.Id, userId, timespan); + var colors = ColorUtils.GetRainbowColors(result.Count); + + return result.Select((x,i) => new MessageCountPerChannelInformation(x.ChannelName, x.MessageCount, colors[i].ToString())) + .OrderByDescending(x => x.Count) + .ToArray(); + } + + private static UserInformation FromEphemeralUser( + EphemeralUser ephemeralUser, + GuildUserParticipationStatistics userRank, + IReadOnlyList messages7, + IReadOnlyList messages30, + SocketRole[] roles) + { + return new UserInformation( + ephemeralUser.Id.ToString(), + ephemeralUser.Username, + ephemeralUser.Nickname, + ephemeralUser.Discriminator, + ephemeralUser.AvatarId != null ? ephemeralUser.GetAvatarUrl(ImageFormat.Auto, 256) : ephemeralUser.GetDefaultAvatarUrl(), + ephemeralUser.CreatedAt, + ephemeralUser.JoinedAt, + ephemeralUser.FirstSeen, + ephemeralUser.LastSeen, + userRank.Rank, + messages7.Sum(x => x.MessageCount), + messages30.Sum(x => x.MessageCount), + userRank.AveragePerDay, + userRank.Percentile, + roles + .Where(x => !x.IsEveryone) + .Select(x => new RoleInformation(x.Id, x.Name, x.Color.ToString())), + ephemeralUser.IsBanned, + ephemeralUser.BanReason, + ephemeralUser.GuildId != default + ); + } +} diff --git a/src/Modix.Web/Models/Commands/Command.cs b/src/Modix.Web/Models/Commands/Command.cs deleted file mode 100644 index 71438879c..000000000 --- a/src/Modix.Web/Models/Commands/Command.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Modix.Services.CommandHelp; - -namespace Modix.Web.Models.Commands; - -public record Command(string Name, string Summary, IReadOnlyCollection Aliases, IReadOnlyCollection Parameters, bool IsSlashCommand); diff --git a/src/Modix.Web/Models/Common/ModixUser.cs b/src/Modix.Web/Models/Common/ModixUser.cs deleted file mode 100644 index b110ab35c..000000000 --- a/src/Modix.Web/Models/Common/ModixUser.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Discord; -using Modix.Services.Utilities; - -namespace Modix.Web.Models.Common; - -public sealed class ModixUser : IAutoCompleteItem -{ - public string? Name { get; init; } - public ulong UserId { get; init; } - public string? AvatarUrl { get; init; } - - public static ModixUser FromIGuildUser(IGuildUser user) => new() - { - Name = user.GetDisplayName(), - UserId = user.Id, - AvatarUrl = user.GetDisplayAvatarUrl() ?? user.GetDefaultAvatarUrl() - }; - - public static ModixUser FromNonGuildUser(IUser user) => new() - { - Name = user.GetDisplayName(), - UserId = user.Id, - AvatarUrl = user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl() - }; -} diff --git a/src/Modix.Web/Models/DeletedMessages/DeletedMessageInformation.cs b/src/Modix.Web/Models/DeletedMessages/DeletedMessageInformation.cs deleted file mode 100644 index e87f73a0f..000000000 --- a/src/Modix.Web/Models/DeletedMessages/DeletedMessageInformation.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Discord; -using Humanizer.Bytes; -using Modix.Services.Utilities; - -namespace Modix.Web.Models.DeletedMessages; - -public record DeletedMessageInformation(ulong MessageId, DateTimeOffset? SentTime, string? Url, string Username, string Content) -{ - public static DeletedMessageInformation FromIMessage(IMessage message) - { - var content = message.Content; - - if (string.IsNullOrWhiteSpace(content)) - { - if (message.Embeds.Count > 0) - { - content = $"Embed: {message.Embeds.First().Title}: {message.Embeds.First().Description}"; - } - else if (message.Attachments.Count > 0) - { - content = $"Attachment: {message.Attachments.First().Filename} {ByteSize.FromBytes(message.Attachments.First().Size)}"; - } - } - - return new DeletedMessageInformation(message.Id, message.CreatedAt, message.GetJumpUrl(), message.Author.GetDisplayName(), content); - } -} diff --git a/src/Modix.Web/Models/Infractions/InfractionData.cs b/src/Modix.Web/Models/Infractions/InfractionData.cs deleted file mode 100644 index c1491ab7e..000000000 --- a/src/Modix.Web/Models/Infractions/InfractionData.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Modix.Data.Models.Core; -using Modix.Data.Models.Moderation; - -namespace Modix.Web.Models.Infractions; - -public record InfractionData( - long Id, - ulong GuildId, - InfractionType Type, - string Reason, - TimeSpan? Duration, - GuildUserBrief Subject, - ModerationActionBrief CreateAction, - ModerationActionBrief? RescindAction, - ModerationActionBrief? DeleteAction, - bool CanBeRescind, - bool CanBeDeleted -) -{ - public static InfractionData FromInfractionSummary(InfractionSummary summary, Dictionary outranksValues) - { - return new InfractionData( - summary.Id, - summary.GuildId, - summary.Type, - summary.Reason, - summary.Duration, - summary.Subject, - - summary.CreateAction, - summary.RescindAction, - summary.DeleteAction, - - summary.RescindAction is null - && summary.DeleteAction is null - && (summary.Type == InfractionType.Mute || summary.Type == InfractionType.Ban) - && outranksValues[summary.Subject.Id], - - summary.DeleteAction is null - && outranksValues[summary.Subject.Id] - ); - } -} diff --git a/src/Modix.Web/Models/Stats/GuildStatData.cs b/src/Modix.Web/Models/Stats/GuildStatData.cs deleted file mode 100644 index db3f09698..000000000 --- a/src/Modix.Web/Models/Stats/GuildStatData.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Modix.Data.Models.Core; -using Modix.Services.GuildStats; - -namespace Modix.Web.Models.Stats; - -public record GuildStatData(string GuildName, List GuildRoleCounts, IReadOnlyCollection TopUserMessageCounts); diff --git a/src/Modix.Web/Models/Tags/TagData.cs b/src/Modix.Web/Models/Tags/TagData.cs deleted file mode 100644 index 7e6fa3d57..000000000 --- a/src/Modix.Web/Models/Tags/TagData.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Modix.Data.Models.Core; -using Modix.Data.Models.Tags; - -namespace Modix.Web.Models.Tags; - -public record TagData( - string Name, - DateTimeOffset Created, - bool IsOwnedByRole, - GuildUserBrief? OwnerUser, - GuildRoleBrief? OwnerRole, - string? OwnerName, - string Content, - uint Uses, - bool CanMaintain, - TagSummary TagSummary) -{ - public static TagData CreateFromSummary(TagSummary summary) - { - return new TagData( - summary.Name, - summary.CreateAction.Created, - summary.OwnerRole is not null, - summary.OwnerUser, - summary.OwnerRole, - summary.OwnerRole?.Name ?? summary.OwnerUser?.Username, - summary.Content, - summary.Uses, - false, - summary); - } -} diff --git a/src/Modix.Web/Models/UserLookup/UserInformation.cs b/src/Modix.Web/Models/UserLookup/UserInformation.cs deleted file mode 100644 index b3bcdcea6..000000000 --- a/src/Modix.Web/Models/UserLookup/UserInformation.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Discord; -using Discord.WebSocket; -using Modix.Data.Models.Core; -using Modix.Web.Models.Common; - -namespace Modix.Web.Models.UserLookup; - -public record UserInformation( - string Id, - string? Username, - string? Nickname, - string? Discriminator, - string? AvatarUrl, - DateTimeOffset CreatedAt, - DateTimeOffset? JoinedAt, - DateTimeOffset? FirstSeen, - DateTimeOffset? LastSeen, - int Rank, - int Last7DaysMessages, - int Last30DaysMessages, - decimal AverageMessagesPerDay, - int Percentile, - IEnumerable Roles, - bool IsBanned, - string? BanReason, - bool IsGuildMember, - IReadOnlyList MessageCountsPerChannel -) -{ - public static UserInformation FromEphemeralUser( - EphemeralUser ephemeralUser, - GuildUserParticipationStatistics userRank, - IReadOnlyList messages7, - IReadOnlyList messages30, - SocketRole[] roles, - List messageCountsPerChannel) - { - return new UserInformation( - ephemeralUser.Id.ToString(), - ephemeralUser.Username, - ephemeralUser.Nickname, - ephemeralUser.Discriminator, - ephemeralUser.AvatarId != null ? ephemeralUser.GetAvatarUrl(ImageFormat.Auto, 256) : ephemeralUser.GetDefaultAvatarUrl(), - ephemeralUser.CreatedAt, - ephemeralUser.JoinedAt, - ephemeralUser.FirstSeen, - ephemeralUser.LastSeen, - userRank.Rank, - messages7.Sum(x => x.MessageCount), - messages30.Sum(x => x.MessageCount), - userRank.AveragePerDay, - userRank.Percentile, - roles - .Where(x => !x.IsEveryone) - .Select(x => new RoleInformation(x.Id, x.Name, x.Color.ToString())), - ephemeralUser.IsBanned, - ephemeralUser.BanReason, - ephemeralUser.GuildId != default, - messageCountsPerChannel - ); - } -} diff --git a/src/Modix.Web/Modix.Web.csproj b/src/Modix.Web/Modix.Web.csproj index e14aa2824..6f900b1f4 100644 --- a/src/Modix.Web/Modix.Web.csproj +++ b/src/Modix.Web/Modix.Web.csproj @@ -8,6 +8,7 @@ + @@ -15,6 +16,8 @@ + + diff --git a/src/Modix.Web/Pages/Configuration.razor b/src/Modix.Web/Pages/Configuration.razor deleted file mode 100644 index a763f58d1..000000000 --- a/src/Modix.Web/Pages/Configuration.razor +++ /dev/null @@ -1,61 +0,0 @@ -@page "/config" -@page "/config/{SubPage}" - -@attribute [Authorize( - Roles = $@" - {nameof(AuthorizationClaim.DesignatedRoleMappingRead)}, - {nameof(AuthorizationClaim.DesignatedChannelMappingRead)}, - {nameof(AuthorizationClaim.AuthorizationConfigure)}")] - -@using Modix.Data.Models.Core; -@using Modix.Web.Components -@using Modix.Web.Components.Configuration -@using MudBlazor - -Modix - Configuration - - - -
-
- Configuration - - - - - - - - - -
-
- @if (SubPage == "roles") - { - - - - } - else if (SubPage == "channels") - { - - - - } - else if (SubPage == "claims") - { - - - - } -
-
- -
- -@code { - - [Parameter] - public string? SubPage { get; set; } - -} diff --git a/src/Modix.Web/Pages/Error.cshtml b/src/Modix.Web/Pages/Error.cshtml deleted file mode 100644 index 3ff7e625d..000000000 --- a/src/Modix.Web/Pages/Error.cshtml +++ /dev/null @@ -1,42 +0,0 @@ -@page -@model Modix.Web.Pages.ErrorModel - - - - - - - - Error - - - - - -
-
-

Error.

-

An error occurred while processing your request.

- - @if (Model.ShowRequestId) - { -

- Request ID: @Model.RequestId -

- } - -

Development Mode

-

- Swapping to the Development environment displays detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

-
-
- - - diff --git a/src/Modix.Web/Pages/Error.cshtml.cs b/src/Modix.Web/Pages/Error.cshtml.cs deleted file mode 100644 index ad6c2eb22..000000000 --- a/src/Modix.Web/Pages/Error.cshtml.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace Modix.Web.Pages; - -[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] -[IgnoreAntiforgeryToken] -public class ErrorModel : PageModel -{ - public string? RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - public void OnGet() => RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; -} diff --git a/src/Modix.Web/Pages/Error.razor b/src/Modix.Web/Pages/Error.razor new file mode 100644 index 000000000..576cc2d2f --- /dev/null +++ b/src/Modix.Web/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/src/Modix.Web/Pages/Logs.razor b/src/Modix.Web/Pages/Logs.razor deleted file mode 100644 index 65f6cf747..000000000 --- a/src/Modix.Web/Pages/Logs.razor +++ /dev/null @@ -1,66 +0,0 @@ -@page "/logs/{SubPage}" -@page "/logs" -@page "/infractions" -@attribute [Authorize(Roles = nameof(AuthorizationClaim.ModerationRead))] -@using Modix.Data.Models.Core; -@using Modix.Web.Components -@using MudBlazor - -Modix - Logs - - - -
-
- Logs - - - - - - - -
-
- @if (SubPage == "infractions") - { - - - - } - else if (SubPage == "deletedMessages") - { - - - - } -
-
- -
- -@code { - [Parameter] - public string? SubPage { get; set; } - - [Inject] - public NavigationManager NavigationManager { get; set; } = null!; - - [Parameter] - [SupplyParameterFromQuery] - public string? Subject { get; set; } - - [Parameter] - [SupplyParameterFromQuery] - public string? Id { get; set; } - - protected override void OnAfterRender(bool firstRender) - { - var relativePath = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); - - if (relativePath.StartsWith("infractions")) - { - NavigationManager.NavigateTo($"/logs/{relativePath}", new NavigationOptions { ReplaceHistoryEntry = true, ForceLoad = false }); - } - } -} diff --git a/src/Modix.Web/Pages/Promotions.razor b/src/Modix.Web/Pages/Promotions.razor deleted file mode 100644 index 4a060df7c..000000000 --- a/src/Modix.Web/Pages/Promotions.razor +++ /dev/null @@ -1,365 +0,0 @@ -@page "/promotions" - -@attribute [Authorize(Roles = nameof(AuthorizationClaim.PromotionsRead))] - -@using Modix.Data.Models.Core; -@using Modix.Data.Models.Promotions; -@using Modix.Data.Utilities; -@using Modix.Services.Promotions; -@using Modix.Web.Components; -@using Modix.Web.Models.Promotions; -@using Modix.Web.Models; -@using Modix.Web.Services; -@using MudBlazor -@using Humanizer; -@using Modix.Services.Utilities; - -Modix - Promotions - - - - - Promotion Campaigns -
- - Start One -
- - @foreach (var (roleColor, campaign) in Campaigns - .Where(x => _showInactive ? true : (x.Campaign.Outcome is null)) - .OrderByDescending(x => x.Campaign.Outcome is null) - .ThenByDescending(x => x.Campaign.CreateAction.Created)) - { - var isCurrentUserCampaign = CurrentUserId == campaign.Subject.Id; - - var icon = campaign.Outcome switch - { - PromotionCampaignOutcome.Accepted => Icons.Material.Filled.Check, - PromotionCampaignOutcome.Rejected => Icons.Material.Filled.NotInterested, - PromotionCampaignOutcome.Failed => Icons.Material.Filled.Error, - _ => Icons.Material.Filled.HowToVote - }; - - var sentimentRatio = isCurrentUserCampaign ? 0d : (double)campaign.ApproveCount / (campaign.ApproveCount + campaign.OpposeCount); - var sentimentColor = sentimentRatio switch - { - _ when isCurrentUserCampaign => Color.Transparent, - > 0.67 => Color.Success, - > 0.33 => Color.Warning, - _ => Color.Error - }; - - - -
-
- - - - - @campaign.Subject.GetFullUsername() - - - @campaign.TargetRole.Name -
- -
- @if (campaign.Outcome is null) - { - - - - - - - } - -
- -
-
-
- - @(isCurrentUserCampaign ? "?" : campaign.ApproveCount.ToString()) -
-
- - @(isCurrentUserCampaign ? "?" : campaign.OpposeCount.ToString()) -
-
- -
-
-
- - Campaign started @campaign.CreateAction.Created.ToString("MM/dd/yy, h:mm:ss tt") - - @if (campaign.Subject.Id == CurrentUserId) - { - - Sorry, you aren't allowed to see comments on your own campaign. - - } - else if (!CampaignCommentData.ContainsKey(campaign.Id)) - { - - } - else - { - foreach (var comment in CampaignCommentData[campaign.Id].Values.OrderByDescending(x => x.CreatedAt)) - { - var sentimentIcon = comment.PromotionSentiment == PromotionSentiment.Approve ? Icons.Material.Filled.ThumbUp : Icons.Material.Filled.ThumbDown; -
- - @comment.Content - - @if (comment.IsFromCurrentUser && campaign.CloseAction is null) - { - - Edit - - } - @comment.CreatedAt.ToString("MM/dd/yy, h:mm:ss tt") -
- - } - - if (campaign.CloseAction is null && !CampaignCommentData[campaign.Id].Any(x => x.Value.IsFromCurrentUser)) - { - - } - } -
-
- } -
-
- -
- - - -@code { - [Inject] - public SessionState SessionState { get; set; } = null!; - - [Inject] - public CookieService CookieService { get; set; } = null!; - - [Inject] - public DiscordHelper DiscordHelper { get; set; } = null!; - - [Inject] - public IPromotionsService PromotionsService { get; set; } = null!; - - [Inject] - public IDialogService DialogService { get; set; } = null!; - - [Inject] - public ISnackbar Snackbar { get; set; } = null!; - - private ulong CurrentUserId { get; set; } - - private IReadOnlyCollection<(string RoleColor, PromotionCampaignSummary Campaign)> Campaigns = Array.Empty<(string RoleColor, PromotionCampaignSummary Campaign)>(); - private Dictionary> CampaignCommentData = new Dictionary>(); - - private bool _showInactive; - - protected override void OnInitialized() - { - _showInactive = SessionState.ShowInactivePromotions; - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; - - var currentUser = DiscordHelper.GetCurrentUser(); - var roleColors = currentUser!.Guild.Roles.ToDictionary(x => x.Id, x => x.Color.ToString()); - - Campaigns = (await PromotionsService.SearchCampaignsAsync(new PromotionCampaignSearchCriteria - { - GuildId = currentUser.Guild.Id - })) - .Select(campaign => (GetRoleColor(roleColors, campaign.TargetRole.Id), campaign)) - .ToArray(); - - CurrentUserId = currentUser.Id; - - StateHasChanged(); - } - - private string GetRoleColor(Dictionary roleColors, ulong roleId) - { - // In case the role has been deleted and we still have a campaign record for that role we serve a grey color. - if (!roleColors.TryGetValue(roleId, out var colorHex)) - { - return $"color: grey"; - } - - return $"color: {colorHex}"; - } - - private async Task ShowInactiveChanged(bool showInactive) - { - _showInactive = showInactive; - await CookieService.SetShowInactivePromotionsAsync(showInactive); - } - - private async Task CampaignExpanded(bool wasExpanded, long campaignId, ulong userId) - { - if (!wasExpanded) - return; - - if (CurrentUserId == userId) - return; - - if (CampaignCommentData.ContainsKey(campaignId)) - return; - - var result = await PromotionsService.GetCampaignDetailsAsync(campaignId); - if (result is null) - { - Snackbar.Add($"Unable to load campaign details for campaign id {campaignId}.", Severity.Error); - return; - } - - CampaignCommentData[campaignId] = result.Comments - .Where(x => x.ModifyAction is null) - .Select(c => new CampaignCommentData(c.Id, c.Sentiment, c.Content, c.CreateAction.Created, c.CreateAction.CreatedBy.Id == CurrentUserId)) - .ToDictionary(x => x.Id, x => x); - - StateHasChanged(); - } - - private async Task OnCampaignCommentCreation(long campaignId, GuildUserBrief campaignSubject, PromotionSentiment sentiment, string? content) - { - try - { - var promotionActionSummary = await PromotionsService.AddCommentAsync(campaignId, sentiment, content); - var newComment = promotionActionSummary.NewComment; - - CampaignCommentData[campaignId][newComment!.Id] = new CampaignCommentData(newComment.Id, newComment.Sentiment, newComment.Content, promotionActionSummary.Created, true); - } - catch (InvalidOperationException ex) - { - Snackbar.Add(ex.Message, Severity.Error); - return; - } - - var username = campaignSubject.GetFullUsername(); - Snackbar.Add($"Added comment to campaign for user {username}.", Severity.Success); - } - - private async Task ToggleEditDialog(long campaignId, long commentId, PromotionSentiment oldPromotionSentiment, string oldContent) - { - var dialogParams = new DialogParameters - { - { x => x.PromotionSentiment, oldPromotionSentiment }, - { x => x.Content, oldContent} - }; - - var dialog = DialogService.Show("", dialogParams); - var result = await dialog.Result; - - if (result.Canceled) - return; - - var (newPromotionSentiment, newContent) = ((PromotionSentiment, string))result.Data; - - try - { - var promotionActionSummary = await PromotionsService.UpdateCommentAsync(commentId, newPromotionSentiment, newContent); - var newComment = promotionActionSummary.NewComment; - - CampaignCommentData[campaignId].Remove(commentId); - CampaignCommentData[campaignId][newComment!.Id] = new CampaignCommentData(newComment.Id, newComment.Sentiment, newComment.Content, promotionActionSummary.Created, true); - } - catch (InvalidOperationException ex) - { - Snackbar.Add(ex.Message, Severity.Error); - return; - } - - Snackbar.Add("Campaign vote was updated.", Severity.Success); - } - - private async Task AcceptCampaign(PromotionCampaignSummary campaign) - { - var timeSince = DateTime.UtcNow - campaign.CreateAction.Created; - - var username = campaign.Subject.GetFullUsername(); - bool force = false; - if (timeSince < PromotionCampaignEntityExtensions.CampaignAcceptCooldown) - { - var timeLeftHumanized = campaign.GetTimeUntilCampaignCanBeClosed().Humanize(3); - var dialogParams = new DialogParameters - { - { x => x.Content, $"There is {timeLeftHumanized} left on the campaign. Do you want to force accept the campaign for {username}?" } - }; - - var dialog = DialogService.Show("", dialogParams); - var confirmationResult = await dialog.Result; - - if (confirmationResult.Canceled) - { - Snackbar.Add("Action was cancelled", Severity.Info); - return; - } - - force = true; - } - - try - { - await PromotionsService.AcceptCampaignAsync(campaign.Id, force); - } - catch (InvalidOperationException ex) - { - Snackbar.Add(ex.Message, Severity.Error); - return; - } - - campaign.Outcome = PromotionCampaignOutcome.Accepted; - Snackbar.Add($"Campaign for '{username}' was accepted.", Severity.Success); - } - - private async Task RejectCampaign(PromotionCampaignSummary campaign) - { - try - { - await PromotionsService.RejectCampaignAsync(campaign.Id); - - } - catch (InvalidOperationException ex) - { - Snackbar.Add(ex.Message, Severity.Error); - return; - } - - var username = campaign.Subject.GetFullUsername(); - campaign.Outcome = PromotionCampaignOutcome.Rejected; - Snackbar.Add($"Campaign for '{username}' was rejected.", Severity.Success); - } -} diff --git a/src/Modix.Web/Pages/Tags.razor b/src/Modix.Web/Pages/Tags.razor deleted file mode 100644 index 5bfe39af0..000000000 --- a/src/Modix.Web/Pages/Tags.razor +++ /dev/null @@ -1,190 +0,0 @@ -@page "/tags" -@attribute [Authorize] -@using Modix.Data.Models.Core; -@using Modix.Data.Models.Tags; -@using Modix.Services.Tags; -@using Modix.Web.Models; -@using Modix.Web.Models.Common; -@using Modix.Web.Models.Tags; -@using Modix.Web.Services; -@using MudBlazor -@using System.Globalization; - -Modix - Tags - - - - - Tags - @if (Data is not null && Roles is not null) - { - - - Create Tag - - - - - Preview - - - - - Save - - - Cancel - - - -
-
- - Create - - Refresh -
- - -
- - - - Name - Last Modified - Owner - Content - Uses - - - @tag.Name - @tag.Created.ToString("MM/dd/yy, h:mm:ss tt") - @if (tag.OwnerRole is not null) - { - _ = Roles.TryGetValue(tag.OwnerRole.Id, out var role); - var roleColor = role?.Color ?? "currentColor"; - @@@tag.OwnerName - } - else - { - @tag.OwnerName - } - - - - @tag.Uses - - - - - - } -
- -
- -@code { - [Inject] - private ITagService TagService { get; set; } = null!; - - [Inject] - private DiscordHelper DiscordHelper { get; set; } = null!; - - [Inject] - private IDialogService DialogService { get; set; } = null!; - - [Inject] - private ISnackbar Snackbar { get; set; } = null!; - - [Parameter] - [SupplyParameterFromQuery] - public string? Query { get; set; } - - private Dictionary? Roles { get; set; } - private TagData[]? Data { get; set; } - - private string? _tagNameValue; - private string? _tagContentValue; - private bool _createDialogVisible; - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) - return; - - await FetchData(); - - StateHasChanged(); - } - - private async Task FetchData() - { - var currentGuild = DiscordHelper.GetUserGuild(); - - var summaries = await TagService.GetSummariesAsync(new TagSearchCriteria - { - GuildId = currentGuild.Id, - }); - - Data = summaries - .Select(TagData.CreateFromSummary) - .ToArray(); - - Roles = currentGuild.Roles - .Select(x => new RoleInformation(x.Id, x.Name, x.Color.ToString())) - .ToDictionary(x => x.Id, x => x); - } - - private bool FilterFunction(TagData tag) - { - if (string.IsNullOrWhiteSpace(Query)) - return true; - - if (tag.OwnerUser is not null && (tag.OwnerUser.Username.Contains(Query, StringComparison.OrdinalIgnoreCase) || tag.OwnerUser.Id.ToString() == Query)) - return true; - - if (tag.OwnerRole is not null && (tag.OwnerRole.Name.Contains(Query, StringComparison.OrdinalIgnoreCase) || tag.OwnerRole.Id.ToString() == Query)) - return true; - - if (tag.Name.Contains(Query, StringComparison.OrdinalIgnoreCase)) - return true; - - if (tag.Content.Contains(Query, StringComparison.OrdinalIgnoreCase)) - return true; - - return false; - } - - private async Task SaveTag() - { - try - { - var currentUser = DiscordHelper.GetCurrentUser(); - - await TagService.CreateTagAsync(currentUser!.Guild.Id, currentUser.Id, _tagNameValue, _tagContentValue); - var createdTag = await TagService.GetTagAsync(currentUser.Guild.Id, _tagNameValue); - - Data = Data!.Append(TagData.CreateFromSummary(createdTag)).ToArray(); - Snackbar.Add($"Tag '{_tagNameValue}' created.", Severity.Success); - } - catch (Exception ex) - { - Snackbar.Add(ex.Message, Severity.Error); - } - finally - { - _tagNameValue = null; - _tagContentValue = null; - - _createDialogVisible = false; - } - } - - private void ToggleDialog() - { - _createDialogVisible = !_createDialogVisible; - } -} diff --git a/src/Modix.Web/Pages/_Host.cshtml b/src/Modix.Web/Pages/_Host.cshtml deleted file mode 100644 index 14ebc7c03..000000000 --- a/src/Modix.Web/Pages/_Host.cshtml +++ /dev/null @@ -1,47 +0,0 @@ -@page "/" -@using Microsoft.AspNetCore.Components.Web -@using Modix.Web.Models; -@namespace Modix.Web.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - - - - - - - - - - - - - - - - - - - - -
- - An error has occurred. This application may no longer respond until reloaded. - - - An unhandled exception has occurred. See browser dev tools for details. - - Reload - 🗙 -
- - - - - - diff --git a/src/Modix.Web/Security/ClaimsMiddleware.cs b/src/Modix.Web/Security/ClaimsMiddleware.cs index 20b89069b..3e745063d 100644 --- a/src/Modix.Web/Security/ClaimsMiddleware.cs +++ b/src/Modix.Web/Security/ClaimsMiddleware.cs @@ -16,15 +16,15 @@ public async Task InvokeAsync(HttpContext context, IAuthorizationService authori return; } - var selectedGuild = context.Request.Cookies[CookieConstants.SelectedGuild]; - _ = ulong.TryParse(selectedGuild, out var selectedGuildId); - if (context.User.Identity is not ClaimsIdentity claimsIdentity) { await next(context); return; } + var selectedGuild = context.Request.Cookies[CookieConstants.SelectedGuild]; + _ = ulong.TryParse(selectedGuild, out var selectedGuildId); + var currentGuild = discordClient.GetGuild(selectedGuildId) ?? discordClient.Guilds.First(); var currentUser = currentGuild.GetUser(userSnowflake); @@ -33,6 +33,9 @@ public async Task InvokeAsync(HttpContext context, IAuthorizationService authori claimsIdentity.AddClaims(claims); + // Look, I thought it was funny, okay? :D + claimsIdentity.AddClaim(new Claim(ClaimTypes.PostalCode, currentGuild.Id.ToString())); + await next(context); } } diff --git a/src/Modix.Web/Security/PersistingAuthenticationStateProvider.cs b/src/Modix.Web/Security/PersistingAuthenticationStateProvider.cs new file mode 100644 index 000000000..c8e394ba8 --- /dev/null +++ b/src/Modix.Web/Security/PersistingAuthenticationStateProvider.cs @@ -0,0 +1,62 @@ +using System.Diagnostics; +using System.Security.Claims; +using AspNet.Security.OAuth.Discord; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Components.Web; +using Modix.Web.Shared.Models; + +namespace Modix.Web.Security; + +public class PersistingAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable +{ + private readonly PersistentComponentState _state; + private readonly PersistingComponentStateSubscription _subscription; + private Task? _authenticationStateTask; + + public PersistingAuthenticationStateProvider(PersistentComponentState persistentComponentState) + { + _state = persistentComponentState; + _subscription = _state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveAuto); + + AuthenticationStateChanged += OnAuthenticationStateChanged; + } + + private void OnAuthenticationStateChanged(Task task) + { + _authenticationStateTask = task; + } + + private async Task OnPersistingAsync() + { + if(_authenticationStateTask is null) + throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}()."); + + var authState = await _authenticationStateTask; + var user = authState.User; + if (user.Identity?.IsAuthenticated is not true) + return; + + var userId = user.FindFirst(x => x.Type == ClaimTypes.NameIdentifier)?.Value; + var avatarHash = user.FindFirst(DiscordAuthenticationConstants.Claims.AvatarHash); + if (avatarHash is null || user.Identity.Name is null || !ulong.TryParse(userId, out var userSnowflake)) + return; + + _state.PersistAsJson(nameof(DiscordUser), new DiscordUser + { + UserId = userSnowflake, + Name = user.Identity.Name, + AvatarHash = avatarHash.Value, + CurrentGuild = ulong.Parse(user.FindFirstValue(ClaimTypes.PostalCode) ?? ""), + Claims = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value) + }); + } + + public void Dispose() + { + _authenticationStateTask?.Dispose(); + AuthenticationStateChanged -= OnAuthenticationStateChanged; + _subscription.Dispose(); + } +} diff --git a/src/Modix.Web/Services/CookieService.cs b/src/Modix.Web/Services/CookieService.cs index b9afd472c..2b5750971 100644 --- a/src/Modix.Web/Services/CookieService.cs +++ b/src/Modix.Web/Services/CookieService.cs @@ -1,9 +1,10 @@ using Microsoft.JSInterop; using Modix.Web.Models; +using Modix.Web.Shared.Services; namespace Modix.Web.Services; -public class CookieService(IJSRuntime jsRuntime, SessionState sessionState) +public class CookieService(IJSRuntime jsRuntime, SessionState sessionState) : ICookieService { public async Task SetSelectedGuildAsync(ulong guildId) { diff --git a/src/Modix.Web/Services/DiscordHelper.cs b/src/Modix.Web/Services/DiscordHelper.cs index 462a8408b..08d51efd5 100644 --- a/src/Modix.Web/Services/DiscordHelper.cs +++ b/src/Modix.Web/Services/DiscordHelper.cs @@ -2,7 +2,7 @@ using Discord.WebSocket; using Modix.Services.Core; using Modix.Web.Models; -using Modix.Web.Models.Common; +using Modix.Web.Shared.Models.Common; namespace Modix.Web.Services; @@ -16,67 +16,6 @@ public SocketGuild GetUserGuild() return client.Guilds.First(); } - public IEnumerable GetGuildOptions() - { - var currentUser = GetCurrentUser(); - if (currentUser is null) - return Array.Empty(); - - return client - .Guilds - .Where(d => d.GetUser(currentUser.Id) != null) - .Select(d => new GuildOption(d.Id, d.Name, d.IconUrl)); - } - - public SocketGuildUser? GetCurrentUser() - { - var currentGuild = GetUserGuild(); - return currentGuild.GetUser(sessionState.CurrentUserId); - } - - public async Task> AutoCompleteAsync(string query) - { - var userGuild = GetUserGuild(); - - if (userGuild?.Users is null) - return Array.Empty(); - - var result = userGuild.Users - .Where(d => d.Username.Contains(query, StringComparison.OrdinalIgnoreCase) || d.Id.ToString() == query) - .Take(10) - .Select(ModixUser.FromIGuildUser); - - if (!result.Any() && ulong.TryParse(query, out var userId)) - { - var user = await userService.GetUserInformationAsync(userGuild.Id, userId); - - if (user is not null) - { - result = result.Append(ModixUser.FromNonGuildUser(user)); - } - } - - return result; - } - - public IEnumerable AutoCompleteRoles(string query) - { - if (query.StartsWith('@')) - { - query = query[1..]; - } - - var currentGuild = GetUserGuild(); - IEnumerable result = currentGuild.Roles; - - if (!string.IsNullOrWhiteSpace(query)) - { - result = result.Where(d => d.Name.Contains(query, StringComparison.OrdinalIgnoreCase)); - } - - return result.Take(10).Select(d => new RoleInformation(d.Id, d.Name, d.Color.ToString())); - } - public IEnumerable AutocompleteChannels(string query) { if (query.StartsWith('#')) diff --git a/src/Modix.Web/Setup.cs b/src/Modix.Web/Setup.cs index 2a619ac92..d883d069e 100644 --- a/src/Modix.Web/Setup.cs +++ b/src/Modix.Web/Setup.cs @@ -1,9 +1,11 @@ using AspNet.Security.OAuth.Discord; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Components.Authorization; using Modix.Web.Models; using Modix.Web.Security; using Modix.Web.Services; +using Modix.Web.Shared.Services; using MudBlazor; using MudBlazor.Services; @@ -13,7 +15,11 @@ public static class Setup { public static WebApplication ConfigureBlazorApplication(this WebApplication app) { - if (!app.Environment.IsDevelopment()) + if (app.Environment.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + } + else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. @@ -26,29 +32,52 @@ public static WebApplication ConfigureBlazorApplication(this WebApplication app) app.UseRouting(); + app.UseAntiforgery(); + app.UseRequestLocalization("en-US"); app.UseMiddleware(); app.UseAuthorization(); - app.MapGet("/login", async (context) => await context.ChallengeAsync(DiscordAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" })); + app.MapControllers(); + + app.MapGet("/login", async (context) => + { + await context.ChallengeAsync(DiscordAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = context.Request.Query.TryGetValue(CookieAuthenticationDefaults.ReturnUrlParameter, out var returnUrl) + ? returnUrl.ToString() + : "/" + }); + }); app.MapGet("/logout", async (context) => await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" })); - app.MapBlazorHub(); - app.MapFallbackToPage("/_Host"); + app.MapRazorComponents() + //.AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Wasm._Imports).Assembly); return app; } public static IServiceCollection ConfigureBlazorServices(this IServiceCollection services) { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddMudServices(); - services.AddMudMarkdownServices(); - - services.AddRazorPages(); - services.AddServerSideBlazor(); + services.AddControllers(); + + services + .AddScoped() + .AddScoped() + .AddScoped() + .AddCascadingValue(sp => sp.GetRequiredService()) + .AddMudServices() + .AddMudMarkdownServices(); + + services.AddScoped(); + services.AddCascadingAuthenticationState(); + + services + .AddRazorComponents() + //.AddInteractiveServerComponents() + .AddInteractiveWebAssemblyComponents(); return services; } diff --git a/src/Modix.Web/Shared/NavMenu.razor b/src/Modix.Web/Shared/NavMenu.razor deleted file mode 100644 index e483be3cf..000000000 --- a/src/Modix.Web/Shared/NavMenu.razor +++ /dev/null @@ -1,65 +0,0 @@ -@using AspNet.Security.OAuth.Discord; -@using Discord.WebSocket; -@using Modix.Data.Models.Core; -@using Modix.Web.Models; -@using Modix.Web.Services; -@using MudBlazor; -@using System.Security.Claims; - - - - -
- -
- -
- -
- - - @* The color: inherit here is needed to keep the colors consistent between link icons and light/dark theme icons - I would suggest not thinking about it too much :) *@ - - - - - -
- - - - - - - -
- -@code { - [Inject] - public required CookieService CookieService { get; set; } - - [Parameter] - public bool DarkMode { get; set; } - - [Parameter] - public EventCallback DarkModeChanged { get; set; } - - private bool _drawerVisible; - - private void ToggleDrawer() => _drawerVisible = !_drawerVisible; - - private async Task ToggleDarkMode(bool toggled) - { - DarkMode = toggled; - await DarkModeChanged.InvokeAsync(DarkMode); - await CookieService.SetUseDarkModeAsync(DarkMode); - } -} diff --git a/src/Modix.Web/Shared/NavMenuLinks.razor b/src/Modix.Web/Shared/NavMenuLinks.razor deleted file mode 100644 index c2484441b..000000000 --- a/src/Modix.Web/Shared/NavMenuLinks.razor +++ /dev/null @@ -1,53 +0,0 @@ -@using AspNet.Security.OAuth.Discord; -@using Discord.WebSocket; -@using Modix.Data.Models.Core; -@using Modix.Web.Models; -@using Modix.Web.Services; -@using MudBlazor - - - - - Home - Stats - Commands - User Lookup - Tags - - - Promotions - - - - Logs - - - - Config - - - - -
- Home - Commands -
- -
- Log In -
-
-
-
- - \ No newline at end of file diff --git a/src/Modix/Authentication/DiscordAuthenticationSetup.cs b/src/Modix/Authentication/DiscordAuthenticationSetup.cs index 1cf996dec..db7d333ab 100644 --- a/src/Modix/Authentication/DiscordAuthenticationSetup.cs +++ b/src/Modix/Authentication/DiscordAuthenticationSetup.cs @@ -1,9 +1,11 @@ using AspNet.Security.OAuth.Discord; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Modix.Web.Shared.Models; namespace Modix.Authentication { @@ -22,7 +24,7 @@ public static AuthenticationBuilder AddDiscordAuthentication( options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always; - options.ClaimActions.MapJsonKey(claimType: "avatarHash", jsonKey: "avatar"); + options.ClaimActions.MapJsonKey(claimType: nameof(DiscordUser.AvatarHash), jsonKey: "avatar"); }); } } diff --git a/src/Modix/Modix.csproj b/src/Modix/Modix.csproj index cf0c3ee88..d67bfaf43 100644 --- a/src/Modix/Modix.csproj +++ b/src/Modix/Modix.csproj @@ -2,7 +2,6 @@ - diff --git a/src/Modix/Program.cs b/src/Modix/Program.cs index a54b600c2..a0baf6e61 100644 --- a/src/Modix/Program.cs +++ b/src/Modix/Program.cs @@ -21,7 +21,6 @@ using Modix.Services; using Modix.Services.Utilities; using Modix.Web; -using Newtonsoft.Json.Converters; using Serilog; using Serilog.Events; using Serilog.Formatting.Compact; @@ -36,7 +35,6 @@ public static int Main(string[] args) var configBuilder = builder.Configuration .AddEnvironmentVariables("MODIX_") - .AddJsonFile("developmentSettings.json", optional: true, reloadOnChange: false) .AddKeyPerFile("/run/secrets", true); if (builder.Environment.IsDevelopment()) @@ -121,8 +119,10 @@ private static void ConfigureServices(WebApplicationBuilder builder, IConfigurat builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { - options.LoginPath = "/api/unauthorized"; + options.LoginPath = "/login"; + options.AccessDeniedPath = "/unauthorized"; //options.LogoutPath = "/logout"; + options.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0); }) .AddDiscordAuthentication(); @@ -140,13 +140,6 @@ private static void ConfigureServices(WebApplicationBuilder builder, IConfigurat builder.Services .AddModixHttpClients() .AddModix(configuration); - - builder.Services.AddMvc(d => d.EnableEndpointRouting = false) - .AddNewtonsoftJson(options => - { - options.SerializerSettings.Converters.Add(new StringEnumConverter()); - options.SerializerSettings.Converters.Add(new StringULongConverter()); - }); } public static void ConfigureCommon(WebApplication app) diff --git a/src/Modix/StringULongConverter.cs b/src/Modix/StringULongConverter.cs deleted file mode 100644 index d77f72976..000000000 --- a/src/Modix/StringULongConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Modix -{ - public class StringULongConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ulong); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(value.ToString()); - } - - public override bool CanRead => false; - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - } -} diff --git a/test/Modix.Data.Test/TestData/ClaimMappings.cs b/test/Modix.Data.Test/TestData/ClaimMappings.cs index 5a66f8b56..6e7df9079 100644 --- a/test/Modix.Data.Test/TestData/ClaimMappings.cs +++ b/test/Modix.Data.Test/TestData/ClaimMappings.cs @@ -4,6 +4,7 @@ using Modix.Data.Models; using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Data.Test.TestData { diff --git a/test/Modix.Data.Test/TestData/DesignatedChannelMappings.cs b/test/Modix.Data.Test/TestData/DesignatedChannelMappings.cs index 4b9659cfc..eb717340e 100644 --- a/test/Modix.Data.Test/TestData/DesignatedChannelMappings.cs +++ b/test/Modix.Data.Test/TestData/DesignatedChannelMappings.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Data.Test.TestData { diff --git a/test/Modix.Data.Test/TestData/DesignatedRoleMappings.cs b/test/Modix.Data.Test/TestData/DesignatedRoleMappings.cs index 4130a5018..498265aec 100644 --- a/test/Modix.Data.Test/TestData/DesignatedRoleMappings.cs +++ b/test/Modix.Data.Test/TestData/DesignatedRoleMappings.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Modix.Data.Models.Core; +using Modix.Models.Core; namespace Modix.Data.Test.TestData { diff --git a/test/Modix.Services.Test/Core/DesignatedRoleServiceTests.cs b/test/Modix.Services.Test/Core/DesignatedRoleServiceTests.cs index 0dc9b0493..75c852840 100644 --- a/test/Modix.Services.Test/Core/DesignatedRoleServiceTests.cs +++ b/test/Modix.Services.Test/Core/DesignatedRoleServiceTests.cs @@ -14,6 +14,7 @@ using Modix.Services.Core; using Modix.Common.Test; +using Modix.Models.Core; namespace Modix.Services.Test.Core {