├── .config └── dotnet-tools.json ├── .github └── workflows │ ├── docker.yml │ └── dotnet.yml ├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── LICENSE ├── ModCore.sln ├── ModCore ├── .gitignore ├── Api │ ├── ApiStartup.cs │ ├── Controllers │ │ └── ApiController.cs │ └── Entities │ │ ├── ApiCommandData.cs │ │ ├── ApiInvite.cs │ │ ├── ApiMetadata.cs │ │ └── ApiPrefix.cs ├── Assets │ └── globe-showing-europe.gif ├── AutoComplete │ ├── DeepLLanguageAutoComplete.cs │ ├── OwnedTagAutoComplete.cs │ ├── ReminderIdAutoComplete.cs │ ├── SlashCommandAutoComplete.cs │ └── TagAutoComplete.cs ├── Commands │ ├── Config.cs │ ├── Info.cs │ ├── Interactive.cs │ ├── Level.cs │ ├── Main.cs │ ├── Moderation.cs │ ├── Purge.cs │ ├── Reminders.cs │ ├── Starboard.cs │ └── Tags.cs ├── Components │ ├── AutoEmbedComponents.cs │ ├── AutoRoleConfigComponents.cs │ ├── BotManagerComponents.cs │ ├── ConfigComponents.cs │ ├── LevelSystemConfigComponents.cs │ ├── LoggerConfigComponents.cs │ ├── NicknameApprovalComponents.cs │ ├── NicknameApprovalConfigComponents.cs │ ├── ReminderComponents.cs │ ├── RoleMenuConfigComponents.cs │ ├── RoleStateConfigComponents.cs │ ├── StarboardConfigComponents.cs │ ├── UtilComponents.cs │ └── WelcomerConfigComponents.cs ├── ContextMenu │ ├── MemberContextMenu.cs │ └── MessageContextMenu.cs ├── DataFixer │ └── Fixer.cs ├── Database │ ├── DatabaseContext.cs │ ├── DatabaseContextBuilder.cs │ ├── DatabaseEntities │ │ ├── DatabaseCommandId.cs │ │ ├── DatabaseGuildConfig.cs │ │ ├── DatabaseInfo.cs │ │ ├── DatabaseLevel.cs │ │ ├── DatabaseRolestateNick.cs │ │ ├── DatabaseRolestateOverride.cs │ │ ├── DatabaseRolestateRoles.cs │ │ ├── DatabaseStarData.cs │ │ ├── DatabaseTag.cs │ │ └── DatabaseTimer.cs │ ├── DesignTimeDatabaseContext.cs │ └── JsonEntities │ │ ├── GuildSettings.Migrations.cs │ │ ├── GuildSettings.cs │ │ └── TimerActionData.cs ├── Entities │ ├── Settings.cs │ ├── SharedData.cs │ └── StartTimes.cs ├── Extensions │ ├── Abstractions │ │ ├── BaseComponentModule.cs │ │ └── IModal.cs │ ├── AsyncListenerExtension.cs │ ├── Attributes │ │ ├── AsyncListenerAttribute.cs │ │ ├── ComponentAttribute.cs │ │ ├── ComponentPermissionsAttribute.cs │ │ ├── ModalAttribute.cs │ │ ├── ModalFieldAttribute.cs │ │ └── ModalHiddenFieldAttribute.cs │ ├── Enums │ │ └── EventType.cs │ ├── ExtensionStatics.cs │ ├── Handlers │ │ ├── ComponentHandler.cs │ │ └── ModalHandler.cs │ └── InteractionExtension.cs ├── HansTagImport │ ├── HansTag.cs │ └── Importer.cs ├── Integrations │ └── PronounDB.cs ├── LegacyCommands │ ├── Eval.cs │ └── Owner.cs ├── Listeners │ ├── EmbedMessageLinks.cs │ ├── JoinLog.cs │ ├── LevelUp.cs │ ├── Linkfilter.cs │ ├── Logging.cs │ ├── MessageSnipe.cs │ ├── NoBotFarm.cs │ ├── Reactions.cs │ ├── RoleState.cs │ ├── StarboardListeners.cs │ ├── Timers.cs │ └── UnbanTimerRemove.cs ├── Migrations │ ├── 20220509144633_BaseDatabase.Designer.cs │ ├── 20220509144633_BaseDatabase.cs │ ├── 20220509151704_RemoveBansWarninge.Designer.cs │ ├── 20220509151704_RemoveBansWarninge.cs │ ├── 20220514235804_UpdatedLeveler.Designer.cs │ ├── 20220514235804_UpdatedLeveler.cs │ ├── 20220516213245_UpdateTags.Designer.cs │ ├── 20220516213245_UpdateTags.cs │ ├── 20220718222104_RemoveModNotes.Designer.cs │ ├── 20220718222104_RemoveModNotes.cs │ ├── 20220719211854_RemoveUserData.Designer.cs │ ├── 20220719211854_RemoveUserData.cs │ └── DatabaseContextModelSnapshot.cs ├── ModCore.cs ├── ModCore.csproj ├── ModCoreShard.cs ├── Modals │ ├── FeedbackModal.cs │ ├── FeedbackResponseModal.cs │ ├── MassBanModal.cs │ ├── OverrideTagModal.cs │ ├── PollModal.cs │ ├── PostRoleMenuModal.cs │ ├── RoleMenuNameModal.cs │ ├── SetTagModal.cs │ ├── SnoozeModal.cs │ └── WelcomeMessageModal.cs ├── Program.cs └── Utils │ ├── AugmentedBoolConverter.cs │ ├── ConfigValueSerialization.cs │ ├── CustomDiscordMessageConverter.cs │ ├── DateLexer.cs │ ├── Dates.cs │ ├── EntityFramework │ ├── AttributeImpl │ │ ├── AlternateKeyAttribute.cs │ │ ├── AnnotationAttribute.cs │ │ ├── IgnoreIfProviderNotAttribute.cs │ │ └── IndexAttribute.cs │ ├── EfAttributeException.cs │ ├── EfExtension.cs │ ├── EfIndirectProcessorBaseAttribute.cs │ ├── EfProcessorContext.cs │ ├── EfPropertyBaseAttribute.cs │ ├── EfPropertyDefinition.cs │ ├── EfPropertyProcessorBaseAttribute.cs │ └── IEfCustomContext.cs │ ├── Extensions │ ├── Extensions.cs │ └── Sanitization.cs │ ├── IMultiValueDictionary.cs │ ├── InteractivityUtil.cs │ ├── MentionUlongConverter.cs │ ├── ModCoreSnowflake.cs │ ├── MultiValueDictionary.cs │ ├── Overwrite.cs │ └── StringTokenizer.cs ├── PostgreSQL.md ├── README.md └── dockerfile /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "6.0.4", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | push_to_registry: 8 | name: Publish Docker image 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out the repo 12 | uses: actions/checkout@v3 13 | with: 14 | submodules: 'recursive' 15 | 16 | - name: Get Previous tag 17 | id: previoustag 18 | uses: WyriHaximus/github-action-get-previous-tag@master 19 | 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v3 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | 26 | - name: Login to DockerHub 27 | uses: docker/login-action@v3 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.repository_owner }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Build and Push 34 | uses: docker/build-push-action@v3 35 | with: 36 | context: . 37 | file: ./Dockerfile 38 | push: true 39 | tags: | 40 | ghcr.io/naamloos/modcore:latest 41 | ghcr.io/naamloos/modcore:${{ steps.previoustag.outputs.tag }} 42 | build-args: | 43 | "BUILD_VER=${{ steps.previoustag.outputs.tag }}" 44 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET Build 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 8.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ModCore/tessdata_fast"] 2 | path = ModCore/tessdata_fast 3 | url = https://github.com/tesseract-ocr/tessdata_fast 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Opening a pull request or issue? Please follow these guidelines, they aren't very complex. 3 | 4 | ## Pull Requests 5 | Please specify: 6 | - Changes made 7 | - Whether it builds 8 | - Whether it runs 9 | - What has been tested 10 | - How it has been tested 11 | 12 | # Issues 13 | Please specify: 14 | - Changes requested 15 | - Problems found 16 | - If applicable, a proposal on how to implement it -------------------------------------------------------------------------------- /ModCore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32014.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModCore", "ModCore\ModCore.csproj", "{1D3B8DD2-5E0C-4435-8965-3B84BB06EFBD}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Readmes", "Readmes", "{D29852CD-D686-4A35-A4B2-75FF668A73C0}" 9 | ProjectSection(SolutionItems) = preProject 10 | LICENSE = LICENSE 11 | PostgreSQL.md = PostgreSQL.md 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {1D3B8DD2-5E0C-4435-8965-3B84BB06EFBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {1D3B8DD2-5E0C-4435-8965-3B84BB06EFBD}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {1D3B8DD2-5E0C-4435-8965-3B84BB06EFBD}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {1D3B8DD2-5E0C-4435-8965-3B84BB06EFBD}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(ExtensibilityGlobals) = postSolution 30 | SolutionGuid = {A60F49EF-3541-485A-BAAE-C8B7EAC2B577} 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /ModCore/.gitignore: -------------------------------------------------------------------------------- 1 | cummies.db 2 | example.db -------------------------------------------------------------------------------- /ModCore/Api/ApiStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace ModCore.Api 9 | { 10 | public class ApiStartup 11 | { 12 | public ApiStartup(IConfiguration configuration) 13 | { 14 | Configuration = configuration; 15 | } 16 | 17 | public IConfiguration Configuration { get; } 18 | 19 | // This method gets called by the runtime. Use this method to add services to the container. 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddOptions(); 23 | services.AddMemoryCache(); 24 | services.AddSingleton(); 25 | 26 | services.AddControllers() 27 | .AddNewtonsoftJson(); 28 | } 29 | 30 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 31 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 32 | { 33 | if (env.IsDevelopment()) 34 | { 35 | app.UseDeveloperExceptionPage(); 36 | } 37 | 38 | //app.UseStaticFiles(new StaticFileOptions() 39 | //{ 40 | // FileProvider = new EmbeddedFileProvider(typeof(Web.Pages.IndexModel).Assembly, "ModCore.Web.Pages") 41 | //}); 42 | 43 | app.UseRouting(); 44 | app.UseEndpoints(x => { 45 | x.MapControllers(); 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ModCore/Api/Controllers/ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Newtonsoft.Json; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using DSharpPlus; 6 | using DSharpPlus.CommandsNext; 7 | using DSharpPlus.CommandsNext.Attributes; 8 | using ModCore.Api.Entities; 9 | 10 | namespace ModCore.Api.Controllers 11 | { 12 | [Route("api")] 13 | public class ApiController : Controller 14 | { 15 | private ModCore ModCore; 16 | 17 | public ApiController(ModCore modcore) 18 | { 19 | ModCore = modcore; 20 | } 21 | 22 | public ActionResult Index() 23 | { 24 | return Content("ModCore API", "application/json"); 25 | } 26 | 27 | // GET ping 28 | [HttpGet("ping")] 29 | public ActionResult Ping() 30 | { 31 | return Content("pong!", "application/json"); 32 | } 33 | 34 | [HttpGet("prefix")] 35 | public ActionResult Dp() 36 | { 37 | var prefix = new ApiPrefix() { Prefix = ModCore.SharedData.DefaultPrefix }; 38 | 39 | return Content(JsonConvert.SerializeObject(prefix), "application/json"); 40 | } 41 | 42 | [HttpGet("invite")] 43 | public ActionResult Invite() 44 | { 45 | var invite = new ApiInvite() { Invite = "https://discordapp.com/oauth2/authorize?client_id=359828546719449109&scope=bot&permissions=8" }; 46 | return Content(JsonConvert.SerializeObject(invite), "application/json"); 47 | } 48 | 49 | [HttpGet("invite/redirect")] 50 | public string InviteRedirect() 51 | { 52 | Response.Redirect($"https://discordapp.com/oauth2/authorize?client_id=359828546719449109&scope=bot&permissions=8"); 53 | return "Redirecting to invite..."; 54 | } 55 | 56 | [HttpGet("permissions")] 57 | public ActionResult Perms() 58 | { 59 | List apiCmd = new List(); 60 | // TODO optimize (cache the info) 61 | var cmd = this.ModCore.Shards[0].Commands.RegisteredCommands.Values; 62 | 63 | foreach (var c in cmd) 64 | { 65 | if (!c.ExecutionChecks.Any(x => x.GetType() == typeof(RequireOwnerAttribute))) 66 | apiCmd.Add(new ApiCommandData() 67 | { 68 | FullName = c.QualifiedName, 69 | UserPerms = getUserPermissions(c).ToPermissionString(), 70 | BotPerms = getBotPermissions(c).ToPermissionString(), 71 | BothPerms = getCombinedPermissions(c).ToPermissionString() 72 | }); 73 | } 74 | 75 | return Content(JsonConvert.SerializeObject(apiCmd), "application/json"); 76 | } 77 | 78 | [HttpGet("metadata")] 79 | public ActionResult Guilds() 80 | { 81 | var metadata = new ApiMetadata() 82 | { 83 | Shards = ModCore.Shards.Count, 84 | Guilds = ModCore.Shards.Sum(x => x.Client.Guilds.Count) 85 | }; 86 | 87 | return Content(JsonConvert.SerializeObject(metadata), "application/json"); 88 | } 89 | 90 | private Permissions getCombinedPermissions(Command cmd) 91 | { 92 | Permissions perm = Permissions.None; 93 | if (cmd.Parent != null) 94 | perm = perm | getCombinedPermissions(cmd.Parent); 95 | if (cmd.ExecutionChecks.Any(x => x.GetType() == typeof(RequirePermissionsAttribute))) 96 | { 97 | var permchek = (RequirePermissionsAttribute)cmd.ExecutionChecks.First(x => x.GetType() == typeof(RequirePermissionsAttribute)); 98 | perm = perm | permchek.Permissions; 99 | } 100 | 101 | return perm; 102 | } 103 | 104 | private Permissions getBotPermissions(Command cmd) 105 | { 106 | Permissions perm = Permissions.None; 107 | if (cmd.Parent != null) 108 | perm = perm | getBotPermissions(cmd.Parent); 109 | if (cmd.ExecutionChecks.Any(x => x.GetType() == typeof(RequireBotPermissionsAttribute))) 110 | { 111 | var permchek = (RequireBotPermissionsAttribute)cmd.ExecutionChecks.First(x => x.GetType() == typeof(RequireBotPermissionsAttribute)); 112 | perm = perm | permchek.Permissions; 113 | } 114 | 115 | return perm; 116 | } 117 | 118 | private Permissions getUserPermissions(Command cmd) 119 | { 120 | Permissions perm = Permissions.None; 121 | if (cmd.Parent != null) 122 | perm = perm | getUserPermissions(cmd.Parent); 123 | if (cmd.ExecutionChecks.Any(x => x.GetType() == typeof(RequireUserPermissionsAttribute))) 124 | { 125 | var permchek = (RequireUserPermissionsAttribute)cmd.ExecutionChecks.First(x => x.GetType() == typeof(RequireUserPermissionsAttribute)); 126 | perm = perm | permchek.Permissions; 127 | } 128 | 129 | return perm; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ModCore/Api/Entities/ApiCommandData.cs: -------------------------------------------------------------------------------- 1 | namespace ModCore.Api.Entities 2 | { 3 | public struct ApiCommandData 4 | { 5 | public string FullName { get; set; } 6 | public string BotPerms { get; set; } 7 | public string UserPerms { get; set; } 8 | public string BothPerms { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ModCore/Api/Entities/ApiInvite.cs: -------------------------------------------------------------------------------- 1 | namespace ModCore.Api.Entities 2 | { 3 | public struct ApiInvite 4 | { 5 | public string Invite { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModCore/Api/Entities/ApiMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace ModCore.Api.Entities 2 | { 3 | public struct ApiMetadata 4 | { 5 | public int Shards { get; set; } 6 | public int Guilds { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ModCore/Api/Entities/ApiPrefix.cs: -------------------------------------------------------------------------------- 1 | namespace ModCore.Api.Entities 2 | { 3 | public struct ApiPrefix 4 | { 5 | public string Prefix { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ModCore/Assets/globe-showing-europe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naamloos/ModCore/88b3ac8788b28b81df895bdf1b94a0fe5b902a1d/ModCore/Assets/globe-showing-europe.gif -------------------------------------------------------------------------------- /ModCore/AutoComplete/DeepLLanguageAutoComplete.cs: -------------------------------------------------------------------------------- 1 | using DeepL; 2 | using DeepL.Model; 3 | using DSharpPlus.Entities; 4 | using DSharpPlus.SlashCommands; 5 | using ModCore.Database; 6 | using ModCore.Entities; 7 | using ModCore.Extensions; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace ModCore.AutoComplete 13 | { 14 | public class DeepLLanguageAutoComplete : IAutocompleteProvider 15 | { 16 | private static TargetLanguage[] _langs = null; 17 | 18 | public async Task> Provider(AutocompleteContext ctx) 19 | { 20 | var interactions = ctx.Client.GetInteractionExtension(); 21 | var settings = interactions.Services.GetService(typeof(Settings)) as Settings; 22 | 23 | if(_langs == null) 24 | { 25 | var translator = new Translator(settings.DeepLToken); 26 | _langs = await translator.GetTargetLanguagesAsync(); 27 | } 28 | 29 | return _langs 30 | .Select(x => new DiscordAutoCompleteChoice(x.Name, x.Code)) 31 | .Where(x => string.IsNullOrEmpty((string)ctx.OptionValue) || x.Name.ToLower().Contains(((string)ctx.OptionValue).ToLower()) 32 | || ((string)x.Value).ToLower().Contains(((string)ctx.OptionValue).ToLower())); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ModCore/AutoComplete/OwnedTagAutoComplete.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using DSharpPlus.SlashCommands; 3 | using ModCore.Commands; 4 | using ModCore.Database; 5 | using ModCore.Extensions; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace ModCore.AutoComplete 11 | { 12 | public class OwnedTagAutoComplete : IAutocompleteProvider 13 | { 14 | public async Task> Provider(AutocompleteContext ctx) 15 | { 16 | await Task.Yield(); 17 | 18 | var interactions = ctx.Client.GetInteractionExtension(); 19 | var database = interactions.Services.GetService(typeof(DatabaseContextBuilder)) as DatabaseContextBuilder; 20 | 21 | await using var db = database.CreateContext(); 22 | var tags = db.Tags.Where(x => x.GuildId == (long)ctx.Guild.Id && x.ChannelId < 1).ToList(); 23 | var channelTags = db.Tags.Where(x => x.ChannelId == (long)ctx.Channel.Id).ToList(); 24 | 25 | tags.RemoveAll(x => channelTags.Any(y => y.Name == x.Name)); 26 | tags.AddRange(channelTags); 27 | tags.Where(x => Tags.canManageTag(x, ctx.Channel, ctx.User as DiscordMember)); 28 | 29 | return tags.Where(x => x.Name.Contains((string)ctx.OptionValue)) 30 | .OrderBy(x => x.Name.IndexOf((string)ctx.OptionValue)) 31 | .Take(25) 32 | .Select(x => new DiscordAutoCompleteChoice($"{(x.ChannelId < 1 ? "🌍" : "💬")} {x.Name}", x.Name)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ModCore/AutoComplete/ReminderIdAutoComplete.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using DSharpPlus.SlashCommands; 3 | using ModCore.Database; 4 | using ModCore.Database.DatabaseEntities; 5 | using ModCore.Database.JsonEntities; 6 | using ModCore.Extensions; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace ModCore.AutoComplete 12 | { 13 | public class ReminderIdAutoComplete : IAutocompleteProvider 14 | { 15 | public async Task> Provider(AutocompleteContext ctx) 16 | { 17 | await Task.Yield(); 18 | 19 | var interactions = ctx.Client.GetInteractionExtension(); 20 | var database = interactions.Services.GetService(typeof(DatabaseContextBuilder)) as DatabaseContextBuilder; 21 | 22 | DatabaseTimer[] reminders; 23 | 24 | await using (var db = database.CreateContext()) 25 | reminders = db.Timers.Where(xt => 26 | xt.ActionType == TimerActionType.Reminder && 27 | xt.UserId == (long)ctx.User.Id).ToArray(); 28 | 29 | var selection = reminders.Select(x => (x.Id, x.GetData())); 30 | 31 | var value = (string)ctx.OptionValue; 32 | 33 | return selection.Where(x => x.Item2.ReminderText.Contains(value) || x.Id.ToString().Contains(value)) 34 | .OrderBy(x => x.Id.ToString().Contains(value) ? -2 : x.Item2.ReminderText.IndexOf(value)) 35 | .Take(25) 36 | .Select(x => new DiscordAutoCompleteChoice($"⏱️ (#{x.Id}) " + (x.Item2.ReminderText.Length > 25 ? x.Item2.ReminderText.Substring(0, 25) : x.Item2.ReminderText), x.Id.ToString())); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ModCore/AutoComplete/SlashCommandAutoComplete.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.SlashCommands; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace ModCore.AutoComplete 9 | { 10 | public class SlashCommandAutoComplete : IAutocompleteProvider 11 | { 12 | public async Task> Provider(AutocompleteContext ctx) 13 | { 14 | await Task.Yield(); 15 | 16 | var value = ctx.OptionValue as string; 17 | 18 | var slashies = ctx.Client.GetSlashCommands(); 19 | var registered = slashies.RegisteredCommands; 20 | var commands = registered.First(x => x.Key == null).Value.Where(x => x.Type == ApplicationCommandType.SlashCommand); 21 | Dictionary qualifiedCommands = new Dictionary(); 22 | 23 | foreach(var command in commands) 24 | { 25 | if(command.Options != null && command.Options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand)) 26 | { 27 | foreach(var option in buildSubCommands("/" + command.Name, command.Options)) 28 | { 29 | qualifiedCommands.Add(option, command.Id); 30 | } 31 | } 32 | else 33 | { 34 | qualifiedCommands.Add("/" + command.Name, command.Id); 35 | } 36 | } 37 | 38 | var qualified = qualifiedCommands.Where(x => x.Key.Substring(1).Contains(value)) 39 | .OrderBy(x => x.Key.IndexOf(value)) 40 | .Take(25) 41 | .Select(x => new DiscordAutoCompleteChoice(x.Key, $"")); 42 | return qualified.ToList(); 43 | } 44 | 45 | private string[] buildSubCommands(string parent, IEnumerable options) 46 | { 47 | List output = new List(); 48 | 49 | foreach(var option in options) 50 | { 51 | if(option.Options != null && option.Options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand)) 52 | { 53 | output.AddRange(buildSubCommands(parent + " " + option.Name, option.Options)); 54 | } 55 | else 56 | { 57 | output.Add(parent + " " + option.Name); 58 | } 59 | } 60 | 61 | return output.ToArray(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ModCore/AutoComplete/TagAutoComplete.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using DSharpPlus.SlashCommands; 3 | using ModCore.Database; 4 | using ModCore.Extensions; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace ModCore.AutoComplete 10 | { 11 | public class TagAutoComplete : IAutocompleteProvider 12 | { 13 | public async Task> Provider(AutocompleteContext ctx) 14 | { 15 | await Task.Yield(); 16 | 17 | var interactions = ctx.Client.GetInteractionExtension(); 18 | var database = interactions.Services.GetService(typeof(DatabaseContextBuilder)) as DatabaseContextBuilder; 19 | 20 | await using var db = database.CreateContext(); 21 | var tags = db.Tags.Where(x => x.GuildId == (long)ctx.Guild.Id && x.ChannelId < 1).ToList(); 22 | var channelTags = db.Tags.Where(x => x.ChannelId == (long)ctx.Channel.Id).ToList(); 23 | 24 | tags.RemoveAll(x => channelTags.Any(y => y.Name == x.Name)); 25 | tags.AddRange(channelTags); 26 | 27 | return tags.Where(x => x.Name.Contains((string)ctx.OptionValue)) 28 | .OrderBy(x => x.Name.IndexOf((string)ctx.OptionValue)) 29 | .Take(25) 30 | .Select(x => new DiscordAutoCompleteChoice($"{(x.ChannelId < 1? "🌍": "💬")} {x.Name}", x.Name)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ModCore/Commands/Config.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.SlashCommands; 3 | using ModCore.Components; 4 | using ModCore.Database; 5 | using ModCore.Database.DatabaseEntities; 6 | using ModCore.Database.JsonEntities; 7 | using ModCore.Utils.Extensions; 8 | using System.Threading.Tasks; 9 | 10 | namespace ModCore.Commands 11 | { 12 | [GuildOnly] 13 | public class Config : ApplicationCommandModule 14 | { 15 | public DatabaseContextBuilder Database { private get; set; } 16 | 17 | [SlashCommand("config", "Launches the ModCore Server Configuration Utility.")] 18 | [SlashCommandPermissions(Permissions.ManageGuild)] 19 | public async Task ConfigAsync(InteractionContext ctx) 20 | { 21 | var databaseContext = Database.CreateContext(); 22 | var settings = ctx.Guild.GetGuildSettings(databaseContext); 23 | if(settings == null) 24 | { 25 | // This seems like a great moment to generate a new config 26 | var dbConfig = new DatabaseGuildConfig() 27 | { 28 | GuildId = (long)ctx.Guild.Id 29 | }; 30 | dbConfig.SetSettings(new GuildSettings()); 31 | 32 | databaseContext.GuildConfig.Add(dbConfig); 33 | await databaseContext.SaveChangesAsync(); 34 | } 35 | 36 | await ConfigComponents.PostMenuAsync(ctx.Interaction, InteractionResponseType.ChannelMessageWithSource); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ModCore/Commands/Interactive.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.SlashCommands; 4 | using ModCore.Extensions; 5 | using ModCore.Modals; 6 | using ModCore.Utils; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace ModCore.Commands 13 | { 14 | [GuildOnly] 15 | public class Interactive : ApplicationCommandModule 16 | { 17 | // TODO these commands need to be migrated to use the timer system instead. 18 | [SlashCommand("poll", "Starts a poll in this channel.")] 19 | public async Task PollAsync(InteractionContext ctx) 20 | => await ctx.Client.GetInteractionExtension().RespondWithModalAsync(ctx.Interaction, "Create poll..."); 21 | 22 | [SlashCommand("raffle", "Starts a raffle / giveaway.")] 23 | public async Task RaffleAsync(InteractionContext ctx, 24 | [Option("prize", "Prize you are giving away.")]string prize, 25 | [Option("timespan", "How long to hold the giveaway for.")]string timespan) 26 | { 27 | await ctx.DeferAsync(); 28 | var trophy = DiscordEmoji.FromUnicode("🏆"); 29 | var bomb = DiscordEmoji.FromUnicode("💣"); 30 | 31 | var button = new DiscordButtonComponent(ButtonStyle.Success, "join", "Join raffle!", emoji: new DiscordComponentEmoji(trophy)); 32 | var leave = new DiscordButtonComponent(ButtonStyle.Danger, "leave", "Leave raffle", emoji: new DiscordComponentEmoji(bomb)); 33 | var (time, _) = Dates.ParseTime(timespan); 34 | 35 | var giveaway = await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder() 36 | .WithContent($"Hey! {ctx.User.Mention} is giving away `{prize.Replace('`', '\'')}`! Winner to be announced ") 37 | .AddComponents(button, leave)); 38 | 39 | List members = new List(); 40 | 41 | Task collectorTask(DiscordClient sender, DSharpPlus.EventArgs.ComponentInteractionCreateEventArgs e) 42 | { 43 | _ = Task.Run(async () => 44 | { 45 | if(e.Message.Id == giveaway.Id && e.User.Id != ctx.User.Id) 46 | { 47 | if(e.Interaction.Data.CustomId == "join" && members.All(x => x.Id != e.User.Id)) 48 | { 49 | members.Add(e.User); 50 | await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 51 | .WithContent("✅ You joined the raffle!").AsEphemeral()); 52 | } 53 | else if(e.Interaction.Data.CustomId == "leave") 54 | { 55 | members.RemoveAll(x => x.Id == e.User.Id); 56 | await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 57 | .WithContent("✅ You left the raffle.").AsEphemeral()); 58 | } 59 | else 60 | { 61 | await e.Interaction.CreateResponseAsync(InteractionResponseType.Pong); 62 | } 63 | } 64 | }); 65 | 66 | return Task.CompletedTask; 67 | } 68 | 69 | ctx.Client.ComponentInteractionCreated += collectorTask; 70 | await Task.Delay(time); 71 | ctx.Client.ComponentInteractionCreated -= collectorTask; 72 | 73 | if(members.Count < 1) 74 | { 75 | await ctx.EditFollowupAsync(giveaway.Id, new DiscordWebhookBuilder().WithContent($"😢 Nobody joined {ctx.User.Mention}'s raffle, so nobody won `{prize.Replace('`', '\'')}`...")); 76 | return; 77 | } 78 | 79 | var winnerindex = new Random().Next(0, members.Count - 1); 80 | var winner = members[winnerindex]; 81 | 82 | var tada = DiscordEmoji.FromUnicode("🎉"); 83 | await ctx.EditFollowupAsync(giveaway.Id, new DiscordWebhookBuilder().WithContent($"{tada}{trophy} " + 84 | $"{winner.Mention}, you won `{prize.Replace('`', '\'')}`! Contact {ctx.User.Mention} for your price! " + 85 | $"{trophy}{tada}")); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ModCore/Commands/Level.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using DSharpPlus.SlashCommands; 3 | using ModCore.Database; 4 | using ModCore.Utils.Extensions; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace ModCore.Commands 9 | { 10 | [SlashCommandGroup("level", "Commands relating to the level system.")] 11 | [GuildOnly] 12 | public class Level : ApplicationCommandModule 13 | { 14 | public DatabaseContextBuilder Database { private get; set; } 15 | 16 | [SlashCommand("info", "Information about you or someone else's level in this server.")] 17 | public async Task InfoAsync(InteractionContext ctx, [Option("user", "User to get level data from")]DiscordUser user = null) 18 | { 19 | var targetUser = user ?? ctx.User; 20 | var targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); 21 | 22 | var experience = 0; 23 | var level = 0; 24 | 25 | await using (var db = Database.CreateContext()) 26 | { 27 | if(!ctx.Guild.GetGuildSettings(db).Levels.Enabled) 28 | { 29 | await ctx.CreateResponseAsync("⛔ Levels are disabled in this server!"); 30 | return; 31 | } 32 | var data = db.Levels.FirstOrDefault(x => x.UserId == (long)targetUser.Id && x.GuildId == (long)ctx.Guild.Id); 33 | if (data != null) 34 | { 35 | level = Listeners.LevelUp.CalculateLevel(data.Experience); 36 | experience = data.Experience; 37 | } 38 | } 39 | 40 | var levelupxp = Listeners.LevelUp.CalculateRequiredXp(level + 1) - experience; 41 | 42 | await ctx.CreateResponseAsync($"💫 **Currently, {(user == null ? "you are" : $"{targetMember.DisplayName} is")} Level {level} with {experience} xp.**" + 43 | $"\n{levelupxp} more xp is required to reach level {level + 1}.", true); 44 | } 45 | 46 | [SlashCommand("leaderboard", "Shows this server's level leaderboard.")] 47 | public async Task LeaderboardAsync(InteractionContext ctx) 48 | { 49 | await using var db = Database.CreateContext(); 50 | if (!ctx.Guild.GetGuildSettings(db).Levels.Enabled) 51 | { 52 | await ctx.CreateResponseAsync("⛔ Levels are disabled in this server!"); 53 | return; 54 | } 55 | 56 | var top10 = db.Levels.Where(x => x.GuildId == (long)ctx.Guild.Id) 57 | .OrderByDescending(x => x.Experience) 58 | .Take(10) 59 | .ToList(); 60 | 61 | if(top10.Count == 0) 62 | { 63 | await ctx.CreateResponseAsync("⚠️ No level data was found for this server!", true); 64 | return; 65 | } 66 | 67 | var top10string = ""; 68 | int index = 1; 69 | foreach (var leveldata in top10) 70 | { 71 | top10string += $"{index}. <@{leveldata.UserId}>: Level " + 72 | $"{Listeners.LevelUp.CalculateLevel(leveldata.Experience)} ({leveldata.Experience} xp)\n"; 73 | index++; 74 | } 75 | 76 | var embed = new DiscordEmbedBuilder() 77 | .WithTitle($"{ctx.Guild.Name} Level Leaderboard") 78 | .WithDescription($"These are the users with the most activity!") 79 | .WithColor(new DiscordColor()) 80 | .AddField("Top 10", top10string); 81 | 82 | await ctx.CreateResponseAsync(embed, true); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ModCore/Components/AutoEmbedComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.EventArgs; 2 | using DSharpPlus; 3 | using ModCore.Extensions.Abstractions; 4 | using ModCore.Extensions.Attributes; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using DSharpPlus.Entities; 8 | using ModCore.Database; 9 | using ModCore.Extensions; 10 | using ModCore.Utils.Extensions; 11 | using ModCore.Database.JsonEntities; 12 | using System.Linq; 13 | using ModCore.Entities; 14 | 15 | namespace ModCore.Components 16 | { 17 | public class AutoEmbedComponents : BaseComponentModule 18 | { 19 | private DatabaseContextBuilder database; 20 | 21 | public AutoEmbedComponents(DatabaseContextBuilder database) 22 | { 23 | this.database = database; 24 | } 25 | 26 | [Component("ae.select", ComponentType.StringSelect)] 27 | public async Task AutoRoleConfig(ComponentInteractionCreateEventArgs e) 28 | { 29 | await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate, new DiscordInteractionResponseBuilder().AsEphemeral(true)); 30 | 31 | EmbedMessageLinksMode mode; 32 | 33 | switch(e.Interaction.Data.Values[0]) 34 | { 35 | default: 36 | mode = EmbedMessageLinksMode.Disabled; break; 37 | case "prefix": 38 | mode = EmbedMessageLinksMode.Prefixed; break; 39 | case "always": 40 | mode = EmbedMessageLinksMode.Always; break; 41 | } 42 | 43 | await using (var db = database.CreateContext()) 44 | { 45 | var guild = db.GuildConfig.First(x => x.GuildId == (long)e.Guild.Id); 46 | var settings = guild.GetSettings(); 47 | 48 | settings.EmbedMessageLinks = mode; 49 | guild.SetSettings(settings); 50 | db.GuildConfig.Update(guild); 51 | await db.SaveChangesAsync(); 52 | } 53 | 54 | string state; 55 | switch (mode) 56 | { 57 | default: 58 | state = "Disabled"; 59 | break; 60 | case EmbedMessageLinksMode.Prefixed: 61 | state = "Only when prefixed with `!`"; 62 | break; 63 | case EmbedMessageLinksMode.Always: 64 | state = "Always embed"; 65 | break; 66 | } 67 | 68 | await e.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"👍 New state for Auto Jump Link Embed: {state}.") 69 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "ae", "Back to Jump Link Embed config", emoji: new DiscordComponentEmoji("🏃")))); 70 | } 71 | 72 | public static async Task PostMenuAsync(DiscordInteraction interaction, InteractionResponseType responseType, DatabaseContext db) 73 | { 74 | await using (db) 75 | { 76 | var settings = interaction.Guild.GetGuildSettings(db); 77 | 78 | string state; 79 | var mode = settings.EmbedMessageLinks; 80 | 81 | switch(mode) 82 | { 83 | default: 84 | state = "Disabled"; 85 | break; 86 | case EmbedMessageLinksMode.Prefixed: 87 | state = "Only when prefixed with `!`"; 88 | break; 89 | case EmbedMessageLinksMode.Always: 90 | state = "Always embed"; 91 | break; 92 | } 93 | 94 | var embed = new DiscordEmbedBuilder() 95 | .WithTitle("🖇️ Jump Link Embed Configuration") 96 | .WithDescription("Jump Link Embed allows ModCore to automatically post embeds for jump links.") 97 | .AddField("Current State", state); 98 | 99 | await interaction.CreateResponseAsync(responseType, new DiscordInteractionResponseBuilder() 100 | .AddEmbed(embed) 101 | .WithContent("") 102 | .AddComponents(new DiscordSelectComponent("ae.select", "Change state", new List() 103 | { 104 | new DiscordSelectComponentOption("Disable", "off", "Disables jump link embeds", mode == EmbedMessageLinksMode.Disabled, new DiscordComponentEmoji("❌")), 105 | new DiscordSelectComponentOption("Prefixed with !", "prefix", "Create embeds when prefixed with !", 106 | mode == EmbedMessageLinksMode.Prefixed, new DiscordComponentEmoji("❕")), 107 | new DiscordSelectComponentOption("Always", "always", "Always create jump link embeds", mode == EmbedMessageLinksMode.Always, new DiscordComponentEmoji("✅")) 108 | })) 109 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "cfg", "Back to Config", emoji: new DiscordComponentEmoji("🏃")))); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ModCore/Components/AutoRoleConfigComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Database; 5 | using ModCore.Extensions; 6 | using ModCore.Extensions.Abstractions; 7 | using ModCore.Extensions.Attributes; 8 | using ModCore.Utils.Extensions; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Components 14 | { 15 | [ComponentPermissions(Permissions.ManageGuild)] 16 | public class AutoRoleConfigComponents : BaseComponentModule 17 | { 18 | private DatabaseContextBuilder database; 19 | 20 | public AutoRoleConfigComponents(DatabaseContextBuilder database) 21 | { 22 | this.database = database; 23 | } 24 | 25 | [Component("ar.toggle", ComponentType.Button)] 26 | public async Task ToggleAsync(ComponentInteractionCreateEventArgs e, IDictionary values) 27 | { 28 | var enabled = values["on"] == "true"; 29 | 30 | await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate, new DiscordInteractionResponseBuilder().AsEphemeral(true)); 31 | 32 | await using (var db = database.CreateContext()) 33 | { 34 | var guild = db.GuildConfig.First(x => x.GuildId == (long)e.Guild.Id); 35 | var settings = guild.GetSettings(); 36 | 37 | settings.AutoRole.Enable = enabled; 38 | guild.SetSettings(settings); 39 | db.GuildConfig.Update(guild); 40 | await db.SaveChangesAsync(); 41 | } 42 | 43 | await e.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"👍 {(enabled ? "Enabled" : "Disabled")} auto roles.") 44 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "ar", "Back to Auto Role config", emoji: new DiscordComponentEmoji("🏃")))); 45 | } 46 | 47 | [Component("ar.select", ComponentType.RoleSelect)] 48 | public async Task SelectRolesAsync(ComponentInteractionCreateEventArgs e) 49 | { 50 | var roles = e.Interaction.Data.Resolved.Roles; 51 | 52 | await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate, new DiscordInteractionResponseBuilder().AsEphemeral(true)); 53 | 54 | await using (var db = database.CreateContext()) 55 | { 56 | var guild = db.GuildConfig.First(x => x.GuildId == (long)e.Guild.Id); 57 | var settings = guild.GetSettings(); 58 | 59 | settings.AutoRole.RoleIds = roles.Keys.ToList(); 60 | guild.SetSettings(settings); 61 | db.GuildConfig.Update(guild); 62 | await db.SaveChangesAsync(); 63 | } 64 | 65 | await e.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"👍 The following roles will now automatically be assigned to new members:" + 66 | $"\n{string.Join(',', roles.Keys.Select(x => $"<@&{x}>"))}") 67 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "ar", "Back to Auto Role config", emoji: new DiscordComponentEmoji("🏃")))); 68 | } 69 | 70 | public static async Task PostMenuAsync(DiscordInteraction interaction, InteractionResponseType responseType, DatabaseContext db) 71 | { 72 | await using (db) 73 | { 74 | var settings = interaction.Guild.GetGuildSettings(db).AutoRole; 75 | 76 | var embed = new DiscordEmbedBuilder() 77 | .WithTitle("🤖 Auto Role Configuration") 78 | .WithDescription("Auto Role allows ModCore to assign new roles to members when they join the server.") 79 | .AddField("Enabled", $"{(settings.Enable ? "✅" : "⛔")} Auto Roles are currently **{(settings.Enable ? "enabled" : "disabled")}**.") 80 | .AddField("Selected Roles", settings.RoleIds.Count > 0? string.Join(',', settings.RoleIds.Select(x => $"<@&{x}>")) : "No roles selected."); 81 | 82 | var enableId = ExtensionStatics.GenerateIdString("ar.toggle", new Dictionary() { { "on", "true" } }); 83 | var disableId = ExtensionStatics.GenerateIdString("ar.toggle", new Dictionary() { { "on", "false" } }); 84 | 85 | await interaction.CreateResponseAsync(responseType, new DiscordInteractionResponseBuilder() 86 | .AddEmbed(embed) 87 | .WithContent("") 88 | .AddComponents(new DiscordComponent[] 89 | { 90 | new DiscordButtonComponent(settings.Enable? ButtonStyle.Danger : ButtonStyle.Success, settings.Enable? disableId : enableId, settings.Enable? "Disable Auto Role" : "Enable Auto Role") 91 | }) 92 | .AddComponents(new DiscordRoleSelectComponent("ar.select", "Select Auto Roles...", maxOptions: 5)) 93 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "cfg", "Back to Config", emoji: new DiscordComponentEmoji("🏃")))); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ModCore/Components/BotManagerComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Database; 5 | using ModCore.Extensions; 6 | using ModCore.Extensions.Abstractions; 7 | using ModCore.Extensions.Attributes; 8 | using ModCore.Modals; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Components 14 | { 15 | public class BotManagerComponents : BaseComponentModule 16 | { 17 | private DatabaseContextBuilder database; 18 | private DiscordClient client; 19 | 20 | public BotManagerComponents(DatabaseContextBuilder database, DiscordClient client) 21 | { 22 | this.database = database; 23 | this.client = client; 24 | } 25 | 26 | [Component("fb", ComponentType.Button)] 27 | public async Task RespondFeedbackAsync(ComponentInteractionCreateEventArgs e, IDictionary context) 28 | { 29 | if (Client.CurrentApplication.Owners.All(x => x.Id != e.User.Id)) 30 | return; 31 | 32 | if (ulong.TryParse(context["u"], out _) && ulong.TryParse(context["g"], out _)) 33 | { 34 | await Client.GetInteractionExtension().RespondWithModalAsync(e.Interaction, "Response to feedback.", 35 | new Dictionary() 36 | { 37 | { "g", context["g"] }, 38 | { "u", context["u"] } 39 | }); 40 | } 41 | } 42 | 43 | [Component("lguild", ComponentType.Button)] 44 | public async Task LeaveGuildAsync(ComponentInteractionCreateEventArgs e, IDictionary context) 45 | { 46 | if (Client.CurrentApplication.Owners.All(x => x.Id != e.User.Id)) 47 | return; 48 | 49 | if (ulong.TryParse(context["id"], out ulong id)) 50 | { 51 | var naughtyGuild = await client.GetGuildAsync(id); 52 | if (naughtyGuild is not null) 53 | { 54 | await naughtyGuild.LeaveAsync(); 55 | var response = new DiscordInteractionResponseBuilder() 56 | .WithContent("") 57 | .AsEphemeral() 58 | .AddEmbed(new DiscordEmbedBuilder().WithTitle($"Left naughty guild.") 59 | .WithDescription($"{id}: {naughtyGuild.Name}") 60 | .WithThumbnail(naughtyGuild.GetIconUrl(ImageFormat.Png)) 61 | .WithColor(DiscordColor.Red)); 62 | 63 | await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, response); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ModCore/Components/NicknameApprovalComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Extensions.Abstractions; 5 | using ModCore.Extensions.Attributes; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using ModCore.Utils.Extensions; 10 | 11 | namespace ModCore.Components 12 | { 13 | [ComponentPermissions(Permissions.ManageNicknames)] 14 | public class NicknameApprovalComponents : BaseComponentModule 15 | { 16 | [Component("nick.yes", ComponentType.Button)] 17 | public async Task ApproveNicknameAsync(ComponentInteractionCreateEventArgs e, IDictionary context) 18 | { 19 | string nickname = context["n"]; 20 | if (ulong.TryParse(context["u"], out ulong user_id)) 21 | { 22 | var guild = e.Guild; 23 | 24 | var targetMember = await guild.GetMemberAsync(user_id); 25 | 26 | await targetMember.ModifyAsync(x => x.Nickname = nickname); 27 | 28 | var embed = new DiscordEmbedBuilder() 29 | .WithAuthor(targetMember.GetDisplayUsername(), iconUrl: targetMember.GetAvatarUrl(ImageFormat.Png)) 30 | .WithDescription($"Nickname approved") 31 | .AddField("New nickname", nickname) 32 | .AddField("Old nickname", targetMember.DisplayName) 33 | .AddField("Responsible moderator", e.User.GetDisplayUsername()) 34 | .WithColor(DiscordColor.Green); 35 | 36 | await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(embed)); 37 | 38 | try 39 | { 40 | await targetMember.SendMessageAsync($"✅ Your request to change your nickname in {e.Interaction.Guild.Name} to {nickname} has been approved."); 41 | } 42 | catch (Exception) { } 43 | } 44 | } 45 | 46 | [Component("nick.no", ComponentType.Button)] 47 | public async Task DisapproveNicknameAsync(ComponentInteractionCreateEventArgs e, IDictionary context) 48 | { 49 | string nickname = context["n"]; 50 | if (ulong.TryParse(context["u"], out ulong user_id)) 51 | { 52 | var guild = e.Guild; 53 | 54 | var targetMember = await guild.GetMemberAsync(user_id); 55 | 56 | var embed = new DiscordEmbedBuilder() 57 | .WithAuthor(targetMember.GetDisplayUsername(), iconUrl: targetMember.GetAvatarUrl(ImageFormat.Png)) 58 | .WithDescription($"Nickname Denied") 59 | .AddField("New nickname", nickname) 60 | .AddField("Old nickname", targetMember.DisplayName) 61 | .AddField("Responsible moderator", e.User.GetDisplayUsername()) 62 | .WithColor(DiscordColor.Red); 63 | 64 | var msg = new DiscordMessageBuilder() 65 | .AddEmbed(embed); 66 | 67 | await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(embed)); 68 | 69 | try 70 | { 71 | await targetMember.SendMessageAsync($"⛔ Your request to change your nickname in {e.Interaction.Guild.Name} to {nickname} has been disapproved."); 72 | } 73 | catch (Exception) { } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ModCore/Components/NicknameApprovalConfigComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Database; 5 | using ModCore.Extensions; 6 | using ModCore.Extensions.Abstractions; 7 | using ModCore.Extensions.Attributes; 8 | using ModCore.Utils.Extensions; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Components 14 | { 15 | [ComponentPermissions(Permissions.ManageGuild)] 16 | public class NicknameApprovalConfigComponents : BaseComponentModule 17 | { 18 | private DatabaseContextBuilder database; 19 | 20 | public NicknameApprovalConfigComponents(DatabaseContextBuilder database) 21 | { 22 | this.database = database; 23 | } 24 | 25 | [Component("nick.toggle", ComponentType.Button)] 26 | public async Task ToggleAsync(ComponentInteractionCreateEventArgs e, IDictionary values) 27 | { 28 | var enabled = values["on"] == "true"; 29 | 30 | await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate, new DiscordInteractionResponseBuilder().AsEphemeral(true)); 31 | 32 | await using (var db = database.CreateContext()) 33 | { 34 | var guild = db.GuildConfig.First(x => x.GuildId == (long)e.Guild.Id); 35 | var settings = guild.GetSettings(); 36 | 37 | settings.NicknameConfirm.Enable = enabled; 38 | guild.SetSettings(settings); 39 | db.GuildConfig.Update(guild); 40 | await db.SaveChangesAsync(); 41 | } 42 | 43 | await e.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"👍 {(enabled ? "Enabled" : "Disabled")} the Nickname Approval module.") 44 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "nick", "Back to Nickname Approval config", emoji: new DiscordComponentEmoji("🏃")))); 45 | } 46 | 47 | [Component("nick.channel", ComponentType.ChannelSelect)] 48 | public async Task ChangeChannelAsync(ComponentInteractionCreateEventArgs e) 49 | { 50 | var value = e.Interaction.Data.Resolved.Channels.First(); 51 | await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate, new DiscordInteractionResponseBuilder().AsEphemeral(true)); 52 | 53 | await using (var db = database.CreateContext()) 54 | { 55 | var guild = db.GuildConfig.First(x => x.GuildId == (long)e.Guild.Id); 56 | var settings = guild.GetSettings(); 57 | 58 | settings.NicknameConfirm.ChannelId = value.Key; 59 | guild.SetSettings(settings); 60 | db.GuildConfig.Update(guild); 61 | await db.SaveChangesAsync(); 62 | } 63 | 64 | await e.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"👍 Set your nickname approval channel to <#{value.Key}>.") 65 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "nick", "Back to Nickname Approval config", emoji: new DiscordComponentEmoji("🏃")))); 66 | } 67 | 68 | public static async Task PostMenuAsync(DiscordInteraction interaction, InteractionResponseType responseType, DatabaseContext db) 69 | { 70 | await using (db) 71 | { 72 | var settings = interaction.Guild.GetGuildSettings(db).NicknameConfirm; 73 | 74 | var embed = new DiscordEmbedBuilder() 75 | .WithTitle("📝 Nickname Approval Configuration") 76 | .WithDescription("Nickname approval allows members to send nickname requests to moderators, who in turn can (dis)approve these nicknames. " + 77 | "For this system to work, the member should not have nickname permissions.") 78 | .AddField("Enabled", $"{(settings.Enable ? "✅" : "⛔")} This module is currently **{(settings.Enable ? "enabled" : "disabled")}**.") 79 | .AddField("Nickname Approval Channel", $"<#{settings.ChannelId}>"); 80 | 81 | var enableId = ExtensionStatics.GenerateIdString("nick.toggle", new Dictionary() { { "on", "true" } }); 82 | var disableId = ExtensionStatics.GenerateIdString("nick.toggle", new Dictionary() { { "on", "false" } }); 83 | 84 | var minimumOptions = new List(); 85 | for (int i = 1; i <= 10; i++) 86 | minimumOptions.Add(new DiscordSelectComponentOption($"{i}", $"{i}", $"Set Starboard minimum to {i}.")); 87 | 88 | await interaction.CreateResponseAsync(responseType, new DiscordInteractionResponseBuilder() 89 | .AddEmbed(embed) 90 | .WithContent("") 91 | .AddComponents(new DiscordButtonComponent(settings.Enable? ButtonStyle.Danger : ButtonStyle.Success, settings.Enable? disableId : enableId, settings.Enable? "Disable Nickname Approval" : "Enable Nickname Approval")) 92 | .AddComponents(new DiscordChannelSelectComponent("nick.channel", "Change Channel...", new List() { ChannelType.Text })) 93 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "cfg", "Back to Config", emoji: new DiscordComponentEmoji("🏃")))); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ModCore/Components/ReminderComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Extensions; 5 | using ModCore.Extensions.Abstractions; 6 | using ModCore.Extensions.Attributes; 7 | using ModCore.Modals; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace ModCore.Components 13 | { 14 | public class ReminderComponents : BaseComponentModule 15 | { 16 | [Component("snooze", ComponentType.Button)] 17 | public async Task SnoozeReminderAsync(ComponentInteractionCreateEventArgs e) 18 | { 19 | if (e.Message.MentionedUsers.Any(x => x.Id == e.User.Id)) 20 | { 21 | await Client.GetInteractionExtension().RespondWithModalAsync(e.Interaction, "Snooze reminder", new Dictionary() 22 | { 23 | { "msg", e.Message.Id.ToString() } 24 | }); 25 | } 26 | else 27 | { 28 | await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 29 | .WithContent("⛔ This is not your reminder! You can only snooze your own reminders!").AsEphemeral()); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ModCore/Components/RoleStateConfigComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Database; 5 | using ModCore.Extensions; 6 | using ModCore.Extensions.Abstractions; 7 | using ModCore.Extensions.Attributes; 8 | using ModCore.Utils.Extensions; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Components 14 | { 15 | [ComponentPermissions(Permissions.ManageGuild)] 16 | public class RoleStateConfigComponents : BaseComponentModule 17 | { 18 | private DatabaseContextBuilder database; 19 | 20 | public RoleStateConfigComponents(DatabaseContextBuilder database) 21 | { 22 | this.database = database; 23 | } 24 | 25 | [Component("rs.t_role", ComponentType.Button)] 26 | public async Task ToggleRoleAsync(ComponentInteractionCreateEventArgs e, IDictionary values) 27 | { 28 | var enabled = values["on"] == "true"; 29 | 30 | await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate, new DiscordInteractionResponseBuilder().AsEphemeral(true)); 31 | 32 | await using (var db = database.CreateContext()) 33 | { 34 | var guild = db.GuildConfig.First(x => x.GuildId == (long)e.Guild.Id); 35 | var settings = guild.GetSettings(); 36 | 37 | settings.RoleState.Enable = enabled; 38 | guild.SetSettings(settings); 39 | db.GuildConfig.Update(guild); 40 | await db.SaveChangesAsync(); 41 | } 42 | 43 | await e.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"👍 {(enabled ? "Enabled" : "Disabled")} restoring old member's roles.") 44 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "rs", "Back to Member State config", emoji: new DiscordComponentEmoji("🏃")))); 45 | } 46 | 47 | [Component("rs.t_nick", ComponentType.Button)] 48 | public async Task ToggleNicknameAsync(ComponentInteractionCreateEventArgs e, IDictionary values) 49 | { 50 | var enabled = values["on"] == "true"; 51 | 52 | await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate, new DiscordInteractionResponseBuilder().AsEphemeral(true)); 53 | 54 | await using (var db = database.CreateContext()) 55 | { 56 | var guild = db.GuildConfig.First(x => x.GuildId == (long)e.Guild.Id); 57 | var settings = guild.GetSettings(); 58 | 59 | settings.RoleState.Nickname = enabled; 60 | guild.SetSettings(settings); 61 | db.GuildConfig.Update(guild); 62 | await db.SaveChangesAsync(); 63 | } 64 | 65 | await e.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"👍 {(enabled ? "Enabled" : "Disabled")} restoring old member's nicknames.") 66 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "rs", "Back to Member State config", emoji: new DiscordComponentEmoji("🏃")))); 67 | } 68 | 69 | public static async Task PostMenuAsync(DiscordInteraction interaction, InteractionResponseType responseType, DatabaseContext db) 70 | { 71 | await using (db) 72 | { 73 | var settings = interaction.Guild.GetGuildSettings(db).RoleState; 74 | 75 | var embed = new DiscordEmbedBuilder() 76 | .WithTitle("🗿 Member State Configuration") 77 | .WithDescription("Member states allow ModCore to restore roles and nicknames for old members that rejoin the server.") 78 | .AddField("Roles", $"{(settings.Enable ? "✅" : "⛔")} Restoring roles is currently **{(settings.Enable ? "enabled" : "disabled")}**.") 79 | .AddField("Roles", $"{(settings.Nickname ? "✅" : "⛔")} Restoring nicknames is currently **{(settings.Enable ? "enabled" : "disabled")}**."); 80 | 81 | var enableRoleId = ExtensionStatics.GenerateIdString("rs.t_role", new Dictionary() { { "on", "true" } }); 82 | var disableRoleId = ExtensionStatics.GenerateIdString("rs.t_role", new Dictionary() { { "on", "false" } }); 83 | 84 | var enableNickId = ExtensionStatics.GenerateIdString("rs.t_nick", new Dictionary() { { "on", "true" } }); 85 | var disableNickId = ExtensionStatics.GenerateIdString("rs.t_nick", new Dictionary() { { "on", "false" } }); 86 | 87 | var minimumOptions = new List(); 88 | for (int i = 1; i <= 10; i++) 89 | minimumOptions.Add(new DiscordSelectComponentOption($"{i}", $"{i}", $"Set Starboard minimum to {i}.")); 90 | 91 | await interaction.CreateResponseAsync(responseType, new DiscordInteractionResponseBuilder() 92 | .AddEmbed(embed) 93 | .WithContent("") 94 | .AddComponents(new DiscordComponent[] 95 | { 96 | new DiscordButtonComponent(settings.Enable? ButtonStyle.Danger : ButtonStyle.Success, settings.Enable? disableRoleId : enableRoleId, settings.Enable? "Disable Role States" : "Enable Role States"), 97 | new DiscordButtonComponent(settings.Nickname? ButtonStyle.Danger : ButtonStyle.Success, settings.Nickname? disableNickId : enableNickId, settings.Nickname? "Disable Nickname States" : "Enable Nickname States"), 98 | }) 99 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "cfg", "Back to Config", emoji: new DiscordComponentEmoji("🏃")))); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ModCore/Components/UtilComponents.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Extensions.Abstractions; 5 | using ModCore.Extensions.Attributes; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace ModCore.Components 11 | { 12 | public class UtilComponents : BaseComponentModule 13 | { 14 | [Component("del", ComponentType.Button)] 15 | public async Task UnSnipeAsync(ComponentInteractionCreateEventArgs e, IDictionary context) 16 | { 17 | var allowedUsers = context["u"].Split('|').Select(x => ulong.TryParse(x, out ulong res)? res : 0); 18 | 19 | if(allowedUsers.Contains(e.User.Id)) 20 | { 21 | await e.Message.DeleteAsync(); 22 | await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, 23 | new DiscordInteractionResponseBuilder().WithContent($"✅ Alright, it's gone!").AsEphemeral()); 24 | } 25 | else 26 | { 27 | var allowedUsersMention = string.Join(", ", allowedUsers.Select(x => $"<@{x}>")); 28 | 29 | await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, 30 | new DiscordInteractionResponseBuilder().WithContent($"⚠️ Only {allowedUsersMention} can use this button to delete this message!\nIf you're a moderator, use Discord's delete message feature.").AsEphemeral()); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ModCore/ContextMenu/MemberContextMenu.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.SlashCommands; 4 | using ModCore.Integrations; 5 | using System.Threading.Tasks; 6 | 7 | namespace ModCore.ContextMenu 8 | { 9 | public class MemberContextMenu : ApplicationCommandModule 10 | { 11 | public PronounDB PronounDB { get; set; } 12 | 13 | // Member context menu commands here. Max 5. 14 | [ContextMenu(ApplicationCommandType.UserContextMenu, "Show Pronouns")] 15 | public async Task PronounsAsync(ContextMenuContext ctx) 16 | { 17 | var pronouns = await PronounDB.GetPronounsForDiscordUserAsync(ctx.TargetUser.Id); 18 | await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().AsEphemeral().WithContent($"According to [PronounDB](https://pronoundb.org/), {ctx.TargetUser.Mention}'s preferred pronouns are: `{pronouns}`.")); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using ModCore.Database.DatabaseEntities; 3 | using ModCore.Entities; 4 | using ModCore.Utils.EntityFramework; 5 | 6 | namespace ModCore.Database 7 | { 8 | public class DatabaseContext : DbContext, IEfCustomContext 9 | { 10 | public virtual DbSet Info { get; set; } 11 | public virtual DbSet GuildConfig { get; set; } 12 | public virtual DbSet RolestateOverrides { get; set; } 13 | public virtual DbSet RolestateRoles { get; set; } 14 | public virtual DbSet RolestateNicks { get; set; } 15 | public virtual DbSet Timers { get; set; } 16 | public virtual DbSet StarDatas { get; set; } 17 | public virtual DbSet Tags { get; set; } 18 | public virtual DbSet CommandIds { get; set; } 19 | public virtual DbSet Levels { get; set; } 20 | 21 | public DatabaseProvider Provider { get; } 22 | private string ConnectionString { get; } 23 | 24 | public DatabaseContext(DatabaseProvider provider, string cstring) 25 | { 26 | this.Provider = provider; 27 | this.ConnectionString = cstring; 28 | } 29 | 30 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 31 | { 32 | if (optionsBuilder.IsConfigured) return; 33 | 34 | #if DEBUG 35 | optionsBuilder.EnableSensitiveDataLogging(true); 36 | #endif 37 | 38 | switch (this.Provider) 39 | { 40 | case DatabaseProvider.PostgreSql: 41 | optionsBuilder.UseNpgsql(this.ConnectionString); 42 | break; 43 | case DatabaseProvider.Sqlite: 44 | optionsBuilder.UseSqlite(this.ConnectionString); 45 | break; 46 | case DatabaseProvider.InMemory: 47 | optionsBuilder.UseInMemoryDatabase("modcore"); 48 | break; 49 | } 50 | } 51 | 52 | protected override void OnModelCreating(ModelBuilder model) 53 | { 54 | // https://www.learnentityframeworkcore.com/configuration/data-annotation-attributes 55 | // this site is GOLD for finding out the data annotation equivalents to fluent API methods, and finding out 56 | // whether or not they even exist 57 | 58 | model.BuildCustomAttributes(this); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseContextBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ModCore.Entities; 3 | 4 | namespace ModCore.Database 5 | { 6 | public class DatabaseContextBuilder 7 | { 8 | private string ConnectionString { get; } 9 | private DatabaseProvider Provider { get; } 10 | 11 | public DatabaseContextBuilder(DatabaseProvider provider, string cstr) 12 | { 13 | this.Provider = provider; 14 | this.ConnectionString = cstr; 15 | } 16 | 17 | public DatabaseContext CreateContext() 18 | { 19 | try 20 | { 21 | return new DatabaseContext(this.Provider, this.ConnectionString); 22 | } 23 | catch (Exception e) 24 | { 25 | Console.WriteLine("Error during database initialization:"); 26 | Console.WriteLine(e); 27 | throw; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseCommandId.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using ModCore.Utils.EntityFramework.AttributeImpl; 5 | 6 | namespace ModCore.Database.DatabaseEntities 7 | { 8 | [Table("mcore_cmd_state")] 9 | public class DatabaseCommandId 10 | { 11 | [Column("id")] 12 | [AlternateKey("id")] 13 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 14 | [Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn)] 15 | [Annotation("Npgsql:ValueGeneratedOnAdd", true)] 16 | [Annotation("Sqlite:Autoincrement", true)] 17 | public short Id { get; set; } 18 | 19 | [Key] 20 | [Column("command_qualified")] 21 | public string Command { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseGuildConfig.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using ModCore.Database.JsonEntities; 4 | using ModCore.Utils.EntityFramework.AttributeImpl; 5 | using Newtonsoft.Json; 6 | 7 | namespace ModCore.Database.DatabaseEntities 8 | { 9 | [Table("mcore_guild_config")] 10 | public class DatabaseGuildConfig 11 | { 12 | [Column("id")] 13 | public int Id { get; set; } 14 | 15 | [Column("guild_id")] 16 | [Index("guild_id_key", IsUnique = true, IsLocal = true)] 17 | public long GuildId { get; set; } 18 | 19 | [Column("settings", TypeName = "jsonb")] 20 | [Required] 21 | public string Settings { get; set; } 22 | 23 | public GuildSettings GetSettings() => 24 | JsonConvert.DeserializeObject(Settings); 25 | 26 | public void SetSettings(GuildSettings settings) => 27 | Settings = JsonConvert.SerializeObject(settings); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseInfo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using ModCore.Utils.EntityFramework.AttributeImpl; 4 | 5 | namespace ModCore.Database.DatabaseEntities 6 | { 7 | [Table("mcore_database_info")] 8 | public class DatabaseInfo 9 | { 10 | [Column("id")] 11 | public int Id { get; set; } 12 | 13 | [Index("meta_key_key", IsUnique = true, IsLocal = true)] 14 | [Column("meta_key")] 15 | [Required] 16 | public string MetaKey { get; set; } 17 | 18 | [Column("meta_value")] 19 | [Required] 20 | public string MetaValue { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using ModCore.Utils.EntityFramework.AttributeImpl; 4 | 5 | namespace ModCore.Database.DatabaseEntities 6 | { 7 | [Table("mcore_levels")] 8 | public class DatabaseLevel 9 | { 10 | #region Index 11 | [Index("channel_id_user_id_key", IsUnique = true, IsLocal = true)] 12 | [Column("channel_id")] 13 | public long GuildId { get; set; } 14 | 15 | [Index("channel_id_user_id_key", IsUnique = true, IsLocal = true)] 16 | [Column("user_id")] 17 | public long UserId { get; set; } 18 | #endregion 19 | 20 | /// 21 | /// Ignore me. Indexed by guild/user. 22 | /// 23 | [Column("id")] 24 | public int Id { get; set; } 25 | 26 | [Column("experience")] 27 | public int Experience { get; set; } 28 | 29 | [Column("last_xp_grant", TypeName = "timestamptz")] 30 | public DateTime LastXpGrant { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseRolestateNick.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using ModCore.Utils.EntityFramework.AttributeImpl; 3 | 4 | namespace ModCore.Database.DatabaseEntities 5 | { 6 | [Table("mcore_rolestate_nicks")] 7 | public class DatabaseRolestateNick 8 | { 9 | #region Index 10 | [Index("member_id_guild_id_key", IsUnique = true, IsLocal = true)] 11 | [Column("member_id")] 12 | public long MemberId { get; set; } 13 | 14 | [Index("member_id_guild_id_key", IsUnique = true, IsLocal = true)] 15 | [Column("guild_id")] 16 | public long GuildId { get; set; } 17 | #endregion 18 | 19 | [Column("id")] 20 | public int Id { get; set; } 21 | 22 | [Column("nickname")] 23 | public string Nickname { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseRolestateOverride.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using ModCore.Utils.EntityFramework.AttributeImpl; 3 | 4 | namespace ModCore.Database.DatabaseEntities 5 | { 6 | [Table("mcore_rolestate_overrides")] 7 | public class DatabaseRolestateOverride 8 | { 9 | #region Index 10 | [Index("member_id_guild_id_channel_id_key", IsUnique = true, IsLocal = true)] 11 | [Column("member_id")] 12 | public long MemberId { get; set; } 13 | 14 | [Index("member_id_guild_id_channel_id_key", IsUnique = true, IsLocal = true)] 15 | [Column("guild_id")] 16 | public long GuildId { get; set; } 17 | 18 | [Index("member_id_guild_id_channel_id_key", IsUnique = true, IsLocal = true)] 19 | [Column("channel_id")] 20 | public long ChannelId { get; set; } 21 | #endregion 22 | 23 | [Column("id")] 24 | public int Id { get; set; } 25 | 26 | [Column("perms_allow")] 27 | public long? PermsAllow { get; set; } 28 | 29 | [Column("perms_deny")] 30 | public long? PermsDeny { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseRolestateRoles.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using ModCore.Entities; 3 | using ModCore.Utils.EntityFramework.AttributeImpl; 4 | 5 | namespace ModCore.Database.DatabaseEntities 6 | { 7 | [Table("mcore_rolestate_roles")] 8 | public class DatabaseRolestateRoles 9 | { 10 | #region Index 11 | [Index("member_id_guild_id_key", IsUnique = true, IsLocal = true)] 12 | [Column("member_id")] 13 | public long MemberId { get; set; } 14 | 15 | [Index("member_id_guild_id_key", IsUnique = true, IsLocal = true)] 16 | [Column("guild_id")] 17 | public long GuildId { get; set; } 18 | #endregion 19 | 20 | [Column("id")] 21 | public int Id { get; set; } 22 | 23 | [Column("role_ids")] 24 | [IgnoreIfProviderNot(DatabaseProvider.PostgreSql)] 25 | public long[] RoleIds { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseStarData.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using ModCore.Utils.EntityFramework.AttributeImpl; 3 | 4 | namespace ModCore.Database.DatabaseEntities 5 | { 6 | [Table("mcore_stars")] 7 | public class DatabaseStarData 8 | { 9 | #region Index 10 | [Index("member_id_guild_id_key", IsUnique = true, IsLocal = true)] 11 | [Column("message_id")] 12 | public long MessageId { get; set; } // Message Id 13 | 14 | [Index("member_id_guild_id_key", IsUnique = true, IsLocal = true)] 15 | [Column("channel_id")] 16 | public long ChannelId { get; set; } // Channel this was sent in 17 | 18 | [Index("member_id_guild_id_key", IsUnique = true, IsLocal = true)] 19 | [Column("stargazer_id")] 20 | public long StargazerId { get; set; } // Member that starred 21 | #endregion 22 | 23 | [Column("id")] 24 | public int Id { get; set; } 25 | 26 | [Column("starboard_entry_id")] 27 | public long StarboardMessageId { get; set; } // Id for starboard entry message 28 | 29 | [Column("author_id")] 30 | public long AuthorId { get; set; } // Author Id 31 | 32 | [Column("guild_id")] 33 | public long GuildId { get; set; } // Guild this belongs to 34 | } 35 | } -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseTag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using ModCore.Utils.EntityFramework.AttributeImpl; 4 | 5 | namespace ModCore.Database.DatabaseEntities 6 | { 7 | [Table("mcore_tags")] 8 | public class DatabaseTag 9 | { 10 | #region Index 11 | [Index("guild_id_channel_id_name_key", IsUnique = true, IsLocal = true)] 12 | [Column("guild_id")] 13 | public long GuildId { get; set; } = 0; 14 | 15 | [Index("guild_id_channel_id_name_key", IsUnique = true, IsLocal = true)] 16 | [Column("channel_id")] 17 | public long ChannelId { get; set; } 18 | 19 | [Index("guild_id_channel_id_name_key", IsUnique = true, IsLocal = true)] 20 | [Column("tagname")] 21 | public string Name { get; set; } 22 | #endregion 23 | 24 | [Column("id")] 25 | public int Id { get; set; } 26 | 27 | [Column("owner_id")] 28 | public long OwnerId { get; set; } 29 | 30 | [Column("created_at", TypeName = "timestamptz")] 31 | public DateTime CreatedAt { get; set; } 32 | 33 | [Column("contents")] 34 | public string Contents { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ModCore/Database/DatabaseEntities/DatabaseTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using ModCore.Database.JsonEntities; 5 | using Newtonsoft.Json; 6 | 7 | namespace ModCore.Database.DatabaseEntities 8 | { 9 | [Table("mcore_timers")] 10 | public class DatabaseTimer 11 | { 12 | [Column("id")] 13 | public int Id { get; set; } 14 | 15 | [Column("guild_id")] 16 | public long GuildId { get; set; } 17 | 18 | [Column("channel_id")] 19 | public long ChannelId { get; set; } 20 | 21 | [Column("user_id")] 22 | public long UserId { get; set; } 23 | 24 | [Column("dispatch_at", TypeName = "timestamptz")] 25 | public DateTime DispatchAt { get; set; } 26 | 27 | [Column("action_type")] 28 | public TimerActionType ActionType { get; set; } 29 | 30 | [Column("action_data", TypeName = "jsonb")] 31 | [Required] 32 | public string ActionData { get; set; } 33 | 34 | public T GetData() where T : class, ITimerData => 35 | JsonConvert.DeserializeObject(ActionData); 36 | 37 | public void SetData(T data) where T : class, ITimerData => 38 | ActionData = JsonConvert.SerializeObject(data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ModCore/Database/DesignTimeDatabaseContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Design; 2 | using ModCore.Entities; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.IO; 6 | using System.Text; 7 | 8 | namespace ModCore.Database 9 | { 10 | /// 11 | /// 12 | /// For SQLite migration creation. To use: 13 | /// 14 | /// dotnet ef migrations add MyModCoreMigration 15 | /// dotnet ef database update 16 | /// 17 | /// Then simply copy the example.db file to your configured path. 18 | /// To update, just copy it back and do the same thing. 19 | /// 20 | public class DesignTimeDatabaseContext : IDesignTimeDbContextFactory 21 | { 22 | public DatabaseContext CreateDbContext(string[] args) 23 | { 24 | if (!File.Exists("settings.json")) 25 | { 26 | var json = JsonConvert.SerializeObject(new Settings(), Formatting.Indented); 27 | File.WriteAllText("settings.json", json, new UTF8Encoding(false)); 28 | throw new Exception("Generated a new config file. Please fill this out with your DB info."); 29 | } 30 | 31 | var input = File.ReadAllText("settings.json", new UTF8Encoding(false)); 32 | var settings = JsonConvert.DeserializeObject(input); 33 | var cstring = settings.Database.BuildConnectionString(); 34 | 35 | return new DatabaseContext(DatabaseProvider.PostgreSql, cstring); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /ModCore/Database/JsonEntities/GuildSettings.Migrations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace ModCore.Database.JsonEntities 5 | { 6 | public partial class GuildSettings 7 | { 8 | /// 9 | /// Allows deserializing obsolete InviteBlocker setting, but not serialization. 10 | /// 11 | [JsonProperty("invite_blocker")] 12 | [Obsolete("Use " + nameof(Linkfilter) + " instead!")] 13 | public GuildLinkfilterSettings InviteBlocker 14 | { 15 | // no getter! 16 | set => Linkfilter = value; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /ModCore/Database/JsonEntities/TimerActionData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using DSharpPlus; 5 | using ModCore.Database.DatabaseEntities; 6 | using ModCore.Entities; 7 | using Newtonsoft.Json; 8 | 9 | namespace ModCore.Database.JsonEntities 10 | { 11 | /// 12 | /// Base interface representing all data that can be attached to a timer. 13 | /// 14 | public interface ITimerData { } 15 | 16 | /// 17 | /// Represents reminder data for a timer. 18 | /// 19 | public class TimerReminderData : ITimerData 20 | { 21 | /// 22 | /// Gets or sets the reminder text set by the user. 23 | /// 24 | [JsonProperty("text")] 25 | public string ReminderText { get; set; } 26 | 27 | /// 28 | /// Gets or sets the original message ID set for this reminder. 29 | /// 30 | [JsonProperty("message")] 31 | public ulong? MessageId { get; set; } = null; 32 | 33 | [JsonProperty("snoozed")] 34 | public bool Snoozed { get; set; } = false; 35 | 36 | [JsonProperty("originalUnix")] 37 | public long OriginalUnix { get; set; } = DateTimeOffset.Now.ToUnixTimeMilliseconds(); 38 | 39 | [JsonProperty("snoozecontext")] 40 | public string SnoozedContext { get; set; } = ""; 41 | } 42 | 43 | /// 44 | /// Represents unban data for a timer. 45 | /// 46 | public class TimerUnbanData : ITimerData 47 | { 48 | /// 49 | /// Gets or sets the unbanned user's Id. 50 | /// 51 | [JsonProperty("user_id")] 52 | public long UserId { get; set; } 53 | 54 | [JsonProperty("displayname")] 55 | public string DisplayName { get; set; } 56 | } 57 | 58 | /// 59 | /// Represents timer action type. 60 | /// 61 | public enum TimerActionType 62 | { 63 | Unknown = 0, // Action type that is not known 64 | Reminder = 1, // Reminders 65 | Unban = 2, // Temp ban unban action 66 | [Obsolete("Timeouts are now a built-in Discord feature.")] 67 | Unmute = 3, // Temp mute unmute action 68 | [Obsolete("Nobody used this.")] 69 | Pin = 4, // Timed pin action 70 | [Obsolete("Nobody used this.")] 71 | Unpin = 5, // Temporary pin unpin action 72 | } 73 | 74 | /// 75 | /// Represents information for the timer dispatcher. 76 | /// 77 | public class TimerData 78 | { 79 | /// 80 | /// Gets the current timer task. 81 | /// 82 | public Task Timer { get; } 83 | 84 | /// 85 | /// Gets the database timer instance. 86 | /// 87 | public DatabaseTimer DbTimer { get; } 88 | 89 | /// 90 | /// Gets the time at which this timer is to be dispatched. 91 | /// 92 | public DateTimeOffset DispatchTime => 93 | DbTimer.DispatchAt; 94 | 95 | /// 96 | /// Gets the client for which this timer is to be dispatched. 97 | /// 98 | public DiscordClient Context { get; } 99 | 100 | /// 101 | /// Gets the database context builder for this timer. 102 | /// 103 | public DatabaseContextBuilder Database { get; } 104 | 105 | /// 106 | /// Gets the shard shared data. 107 | /// 108 | public SharedData Shared { get; } 109 | 110 | /// 111 | /// Gets the cancel method for this timer. 112 | /// 113 | public CancellationTokenSource Cancel { get; } 114 | 115 | /// 116 | /// Gets the cancel token for this timer. 117 | /// 118 | public CancellationToken CancelToken => Cancel.Token; 119 | 120 | /// 121 | /// Creates new timer dispatcher information model. 122 | /// 123 | /// Task which will be dispatched. 124 | /// Database model with data about the timer. 125 | /// Context in which this timer is to be dispatched. 126 | /// Database connection builder for this timer. 127 | /// Data shared across the shard. 128 | /// Cancellation token source for this timer. 129 | public TimerData(Task task, DatabaseTimer dbtimer, DiscordClient context, DatabaseContextBuilder db, SharedData shared, CancellationTokenSource cts) 130 | { 131 | Timer = task; 132 | DbTimer = dbtimer; 133 | Context = context; 134 | Database = db; 135 | Shared = shared; 136 | Cancel = cts; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /ModCore/Entities/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ModCore.Database; 3 | using Newtonsoft.Json; 4 | using Npgsql; 5 | 6 | namespace ModCore.Entities 7 | { 8 | public class Settings 9 | { 10 | [JsonProperty("token")] 11 | internal string Token { get; private set; } 12 | 13 | [JsonProperty("prefix")] 14 | public string DefaultPrefix { get; private set; } 15 | 16 | [JsonProperty("shard_count")] 17 | public int ShardCount { get; private set; } 18 | 19 | [JsonProperty("database")] 20 | internal DatabaseSettings Database { get; private set; } 21 | 22 | [JsonProperty("bot_id")] 23 | public ulong BotId { get; private set; } = 359828546719449109; 24 | 25 | [JsonProperty("contact_channel_id")] 26 | public ulong ContactChannelId { get; private set; } = 998624821653033051; 27 | 28 | [JsonProperty("sus_guild_channel_id")] 29 | public ulong SusGuildChannelId { get; private set; } = 1040374549293838376; 30 | 31 | [JsonProperty("deepl-token")] 32 | public string DeepLToken { get; private set; } = ""; 33 | } 34 | 35 | public struct DatabaseSettings 36 | { 37 | [JsonProperty("provider")] 38 | public DatabaseProvider Provider { get; private set; } 39 | 40 | /// 41 | /// Allows deserializing obsolete UseInMemoryProvider setting, but not serialization. 42 | /// 43 | [JsonProperty("in_memory")] 44 | [Obsolete("Use " + nameof(Provider) + " instead!")] 45 | public bool UseInMemoryProvider 46 | { 47 | // no getter! 48 | set { if (value) Provider = DatabaseProvider.InMemory; } 49 | } 50 | 51 | [JsonProperty("hostname")] 52 | public string Hostname { get; private set; } 53 | 54 | [JsonProperty("port")] 55 | public int Port { get; private set; } 56 | 57 | [JsonProperty("database")] 58 | public string Database { get; private set; } 59 | 60 | [JsonProperty("username")] 61 | public string Username { get; private set; } 62 | 63 | [JsonProperty("password")] 64 | public string Password { get; private set; } 65 | 66 | [JsonProperty("data_source")] 67 | public string DataSource { get; private set; } 68 | 69 | public string BuildConnectionString() 70 | { 71 | return this.Provider switch 72 | { 73 | DatabaseProvider.InMemory => null, 74 | DatabaseProvider.Sqlite => "Data Source=" + this.DataSource, 75 | _ => new NpgsqlConnectionStringBuilder 76 | { 77 | Host = this.Hostname, 78 | Port = this.Port, 79 | Database = this.Database, 80 | Username = this.Username, 81 | Password = this.Password, 82 | SslMode = SslMode.Prefer, 83 | TrustServerCertificate = true, 84 | Pooling = true 85 | }.ConnectionString 86 | }; 87 | } 88 | 89 | public DatabaseContextBuilder CreateContextBuilder() => 90 | new DatabaseContextBuilder(this.Provider, this.BuildConnectionString()); 91 | } 92 | 93 | public enum DatabaseProvider : byte 94 | { 95 | PostgreSql, 96 | InMemory, 97 | Sqlite 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ModCore/Entities/SharedData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using DSharpPlus; 6 | using DSharpPlus.Entities; 7 | using ModCore.Database.JsonEntities; 8 | 9 | namespace ModCore.Entities 10 | { 11 | public class SharedData 12 | { 13 | public CancellationTokenSource CancellationTokenSource { get; internal set; } 14 | public DateTime ProcessStartTime { get; internal set; } 15 | public SemaphoreSlim TimerSempahore { get; internal set; } 16 | public TimerData TimerData { get; internal set; } 17 | public string DefaultPrefix { get; internal set; } 18 | public int ReadysReceived { get; internal set; } = 0; 19 | public List AllPermissions { get; internal set; } = new List(); 20 | public ConcurrentDictionary DeletedMessages = new ConcurrentDictionary(); 21 | public ConcurrentDictionary EditedMessages = new ConcurrentDictionary(); 22 | 23 | public ModCore ModCore; 24 | 25 | public SharedData() 26 | { 27 | this.TimerSempahore = new SemaphoreSlim(1, 1); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /ModCore/Entities/StartTimes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ModCore.Entities 4 | { 5 | public class StartTimes 6 | { 7 | public DateTimeOffset ProcessStartTime { get; private set; } 8 | public DateTimeOffset SocketStartTime { get; set; } 9 | 10 | public StartTimes(DateTime processStartTime, DateTime socketStartTime) 11 | { 12 | this.ProcessStartTime = processStartTime; 13 | this.SocketStartTime = socketStartTime; 14 | } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ModCore/Extensions/Abstractions/BaseComponentModule.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | 3 | namespace ModCore.Extensions.Abstractions 4 | { 5 | public abstract class BaseComponentModule 6 | { 7 | public DiscordClient Client { protected get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ModCore/Extensions/Abstractions/IModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using System.Threading.Tasks; 3 | 4 | namespace ModCore.Extensions.Abstractions 5 | { 6 | public interface IModal 7 | { 8 | Task HandleAsync(DiscordInteraction interaction); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ModCore/Extensions/AsyncListenerExtension.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using ModCore.Extensions.Attributes; 3 | using System; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace ModCore.Extensions 8 | { 9 | public class AsyncListenerExtension : BaseExtension 10 | { 11 | private IServiceProvider services; 12 | private DiscordClient client; 13 | 14 | public AsyncListenerExtension(IServiceProvider services) 15 | { 16 | this.services = services; 17 | } 18 | 19 | protected override void Setup(DiscordClient client) 20 | { 21 | this.client = client; 22 | } 23 | 24 | public void RegisterListeners(Assembly assembly) 25 | { 26 | var methods = assembly.DefinedTypes 27 | .SelectMany(x => x.GetMethods()) 28 | .Where(x => x.GetCustomAttribute() != null) 29 | .Select(x => new ListenerMethod { Attribute = x.GetCustomAttribute(), Method = x }); 30 | 31 | foreach (var listener in methods) 32 | { 33 | listener.Attribute.Register(this.client, listener.Method, this.services); 34 | } 35 | } 36 | 37 | public override void Dispose() 38 | { 39 | 40 | } 41 | } 42 | 43 | class ListenerMethod 44 | { 45 | public MethodInfo Method { get; internal set; } 46 | public AsyncListenerAttribute Attribute { get; internal set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ModCore/Extensions/Attributes/ComponentAttribute.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using System; 3 | 4 | namespace ModCore.Extensions.Attributes 5 | { 6 | [AttributeUsage(AttributeTargets.Method)] 7 | public class ComponentAttribute : Attribute 8 | { 9 | public string Id { get; init; } 10 | public ComponentType ComponentType { get; init; } 11 | 12 | public ComponentAttribute(string id, ComponentType componentType) 13 | { 14 | this.Id = id; 15 | this.ComponentType = componentType; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ModCore/Extensions/Attributes/ComponentPermissionsAttribute.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using System; 3 | 4 | namespace ModCore.Extensions.Attributes 5 | { 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] 7 | public class ComponentPermissionsAttribute : Attribute 8 | { 9 | public Permissions Permissions { get; init; } 10 | 11 | public ComponentPermissionsAttribute(Permissions permissions) 12 | { 13 | Permissions = permissions; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ModCore/Extensions/Attributes/ModalAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ModCore.Extensions.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 6 | public class ModalAttribute : Attribute 7 | { 8 | public string ModalId { get; set; } 9 | 10 | public ModalAttribute(string modalId) 11 | { 12 | ModalId = modalId; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ModCore/Extensions/Attributes/ModalFieldAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DSharpPlus; 3 | 4 | namespace ModCore.Extensions.Attributes 5 | { 6 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] 7 | public class ModalFieldAttribute : Attribute 8 | { 9 | public string DisplayText { get; set; } 10 | public string FieldName { get; set; } 11 | public string Placeholder { get; set; } 12 | public string Prefill { get; set; } 13 | public bool Required { get; set; } 14 | public TextInputStyle InputStyle { get; set; } 15 | public int MinLength { get; set; } 16 | public int? MaxLength { get; set; } 17 | 18 | public ModalFieldAttribute(string displaytext, string fieldName, string placeholder = null, 19 | string prefill = null, bool required = false, TextInputStyle style = TextInputStyle.Short, 20 | int min_length = 0, int max_length = -1) 21 | { 22 | DisplayText = displaytext; 23 | FieldName = fieldName; 24 | Placeholder = placeholder; 25 | Required = required; 26 | Prefill = prefill; 27 | InputStyle = style; 28 | MinLength = min_length; 29 | MaxLength = max_length < 0 ? null : max_length; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ModCore/Extensions/Attributes/ModalHiddenFieldAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ModCore.Extensions.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | public class ModalHiddenFieldAttribute : Attribute 7 | { 8 | public string FieldName { get; private set; } 9 | 10 | public ModalHiddenFieldAttribute(string fieldName) 11 | { 12 | FieldName = fieldName; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ModCore/Extensions/Enums/EventType.cs: -------------------------------------------------------------------------------- 1 | namespace ModCore.Extensions.Enums 2 | { 3 | public enum EventType 4 | { 5 | ClientErrored, // AsyncEventHandler 6 | SocketErrored, // AsyncEventHandler 7 | SocketOpened, // AsyncEventHandler 8 | SocketClosed, // AsyncEventHandler 9 | Ready, // AsyncEventHandler 10 | Resumed, // AsyncEventHandler 11 | ChannelCreated, // AsyncEventHandler 12 | DmChannelCreated, // AsyncEventHandler 13 | ChannelUpdated, // AsyncEventHandler 14 | ChannelDeleted, // AsyncEventHandler 15 | DmChannelDeleted, // AsyncEventHandler 16 | ChannelPinsUpdated, // AsyncEventHandler 17 | GuildCreated, // AsyncEventHandler 18 | GuildAvailable, // AsyncEventHandler 19 | GuildUpdated, // AsyncEventHandler 20 | GuildDeleted, // AsyncEventHandler 21 | GuildUnavailable, // AsyncEventHandler 22 | MessageCreated, // AsyncEventHandler 23 | PresenceUpdated, // AsyncEventHandler 24 | GuildBanAdded, // AsyncEventHandler 25 | GuildBanRemoved, // AsyncEventHandler 26 | GuildEmojisUpdated, // AsyncEventHandler 27 | GuildIntegrationsUpdated, // AsyncEventHandler 28 | GuildMemberAdded, // AsyncEventHandler 29 | GuildMemberRemoved, // AsyncEventHandler 30 | GuildMemberUpdated, // AsyncEventHandler 31 | GuildRoleCreated, // AsyncEventHandler 32 | GuildRoleUpdated, // AsyncEventHandler 33 | GuildRoleDeleted, // AsyncEventHandler 34 | MessageAcknowledged, // AsyncEventHandler 35 | MessageUpdated, // AsyncEventHandler 36 | MessageDeleted, // AsyncEventHandler 37 | MessagesBulkDeleted, // AsyncEventHandler 38 | TypingStarted, // AsyncEventHandler 39 | UserSettingsUpdated, // AsyncEventHandler 40 | UserUpdated, // AsyncEventHandler 41 | VoiceStateUpdated, // AsyncEventHandler 42 | VoiceServerUpdated, // AsyncEventHandler 43 | GuildMembersChunked, // AsyncEventHandler 44 | UnknownEvent, // AsyncEventHandler 45 | MessageReactionAdded, // AsyncEventHandler 46 | MessageReactionRemoved, // AsyncEventHandler 47 | MessageReactionsCleared, // AsyncEventHandler 48 | MessageReactionEmojiRemoved, 49 | WebhooksUpdated, // AsyncEventHandler 50 | Heartbeated, // AsyncEventHandler 51 | CommandExecuted, 52 | CommandErrored, 53 | InviteCreate 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ModCore/Extensions/ExtensionStatics.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Web; 6 | 7 | namespace ModCore.Extensions 8 | { 9 | public static class ExtensionStatics 10 | { 11 | public static InteractionExtension UseInteractions(this DiscordClient client, IServiceProvider services) 12 | { 13 | var extension = new InteractionExtension(services); 14 | client.AddExtension(extension); 15 | return extension; 16 | } 17 | 18 | public static InteractionExtension GetInteractionExtension(this DiscordClient client) 19 | => client.GetExtension(); 20 | 21 | public static AsyncListenerExtension UseAsyncListeners(this DiscordClient client, IServiceProvider services) 22 | { 23 | var extension = new AsyncListenerExtension(services); 24 | client.AddExtension(extension); 25 | return extension; 26 | } 27 | 28 | public static AsyncListenerExtension GetAsyncListenerExtension(this DiscordClient client) 29 | => client.GetExtension(); 30 | 31 | public static string GenerateIdString(string Id, IDictionary values) 32 | { 33 | if (values == null) 34 | return Id; 35 | 36 | List data = new List { Id }; 37 | 38 | foreach (var val in values) 39 | { 40 | data.Add($"{HttpUtility.UrlEncode(val.Key)}={HttpUtility.UrlEncode(val.Value)}"); 41 | } 42 | 43 | return string.Join(' ', data); 44 | } 45 | 46 | public static (string Id, Dictionary Values) DecipherIdString(string input) 47 | { 48 | IEnumerable data = input.Split(' ').ToList(); 49 | var id = data.First(); 50 | data = data.Skip(1); 51 | 52 | var values = new Dictionary(); 53 | foreach (var val in data) 54 | { 55 | var split = val.Split('='); 56 | values.Add(HttpUtility.UrlDecode(split[0]), HttpUtility.UrlDecode(split[1])); 57 | } 58 | 59 | return (id, values); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ModCore/Extensions/Handlers/ComponentHandler.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Extensions.Abstractions; 5 | using ModCore.Extensions.Attributes; 6 | using System; 7 | using System.Collections.Concurrent; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Extensions.Handlers 14 | { 15 | public class ComponentHandler 16 | { 17 | private ConcurrentDictionary handlerMethods; 18 | private BaseComponentModule instance; 19 | private Permissions requiredPermissions; 20 | 21 | public ComponentHandler(Type type, IServiceProvider services, DiscordClient client) 22 | { 23 | handlerMethods = new ConcurrentDictionary(); 24 | 25 | var methods = type.GetMethods().Where(x => x.GetCustomAttribute() != null).ToList(); 26 | var validMethods = methods.Where(x => 27 | { 28 | if (x.ReturnType != typeof(Task)) 29 | return false; 30 | 31 | var parameters = x.GetParameters(); 32 | 33 | if(parameters.Length == 2) 34 | { 35 | return parameters[0].ParameterType == typeof(ComponentInteractionCreateEventArgs) 36 | && parameters[1].ParameterType == typeof(IDictionary); 37 | } 38 | 39 | return parameters.Length == 1 40 | && parameters[0].ParameterType == typeof(ComponentInteractionCreateEventArgs); 41 | }); 42 | 43 | foreach(var valid in validMethods) 44 | { 45 | handlerMethods.TryAdd(valid.GetCustomAttribute(), valid); 46 | } 47 | 48 | var constructor = type.GetConstructors()[0]; 49 | var constructorParameters = constructor.GetParameters(); 50 | var parameters = new object[constructorParameters.Length]; 51 | for(var i = 0; i < constructorParameters.Length; i++) 52 | { 53 | parameters[i] = services.GetService(constructorParameters[i].ParameterType); 54 | } 55 | 56 | var perms = type.GetCustomAttribute(); 57 | if(perms != null) 58 | { 59 | this.requiredPermissions = perms.Permissions; 60 | } 61 | 62 | instance = (BaseComponentModule)Activator.CreateInstance(type, parameters); 63 | instance.Client = client; 64 | } 65 | 66 | public bool HasHandlerFor(string id, ComponentType type) 67 | { 68 | return handlerMethods.Any(x => x.Key.Id == id && x.Key.ComponentType == type); 69 | } 70 | 71 | public async Task HandleAsync(ComponentInteractionCreateEventArgs e, string command, IDictionary commandArgs) 72 | { 73 | if (!e.Channel.PermissionsFor(e.User as DiscordMember).HasPermission(requiredPermissions)) 74 | return; 75 | 76 | var handlerKey = this.handlerMethods.Keys.FirstOrDefault(x => x.Id == command && x.ComponentType == e.Interaction.Data.ComponentType); 77 | if (handlerKey == null) 78 | return; 79 | var handler = this.handlerMethods[handlerKey]; 80 | 81 | var perms = handler.GetCustomAttribute(); 82 | if (perms != null) 83 | { 84 | if (!e.Channel.PermissionsFor(e.User as DiscordMember).HasPermission(perms.Permissions)) 85 | return; 86 | } 87 | 88 | if (handler.GetParameters().Length == 2) 89 | await (Task)(handler.Invoke(instance, new object[] { e, commandArgs })); 90 | await (Task)(handler.Invoke(instance, new object[] { e })); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ModCore/Extensions/Handlers/ModalHandler.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Extensions.Abstractions; 5 | using ModCore.Extensions.Attributes; 6 | using System; 7 | using System.Collections.Concurrent; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Extensions.Handlers 14 | { 15 | public class ModalHandler 16 | { 17 | public Type Type { get; private set; } 18 | 19 | public ModalAttribute Info { get; private set; } 20 | 21 | private readonly ConcurrentDictionary fields; 22 | 23 | private readonly ConcurrentDictionary hiddenFields; 24 | 25 | public ModalHandler(Type type, ModalAttribute info) 26 | { 27 | Type = type; 28 | Info = info; 29 | hiddenFields = new ConcurrentDictionary(); 30 | fields = new ConcurrentDictionary(); 31 | 32 | var properties = type.GetProperties().Where(x => x.GetSetMethod() != null); 33 | var hidden = properties.Where(x => x.GetCustomAttribute() != null); 34 | var visible = properties.Where(x => x.GetCustomAttribute() != null); 35 | 36 | foreach (var property in hidden) 37 | hiddenFields.TryAdd(property.GetCustomAttribute(), property); 38 | 39 | foreach (var property in visible) 40 | fields.TryAdd(property.GetCustomAttribute(), property); 41 | } 42 | 43 | public async Task CreateAsync(DiscordInteraction interaction, string title, IDictionary hiddenValues, IDictionary prefill = null) 44 | { 45 | var modalString = ExtensionStatics.GenerateIdString(Info.ModalId, hiddenValues); 46 | 47 | var interactionResp = new DiscordInteractionResponseBuilder() 48 | .WithTitle(title) 49 | .WithCustomId(modalString); 50 | 51 | foreach (var field in fields) 52 | { 53 | var attr = field.Key; 54 | string prefillValue = null; 55 | if(prefill != null && prefill.Keys.Contains(field.Value.Name)) 56 | { 57 | prefillValue = prefill[field.Value.Name]; 58 | } 59 | interactionResp.AddComponents( 60 | new TextInputComponent(attr.DisplayText, attr.FieldName, 61 | attr.Placeholder, attr.Prefill ?? prefillValue ?? null, attr.Required, attr.InputStyle, attr.MinLength, attr.MaxLength)); 62 | } 63 | 64 | await interaction.CreateResponseAsync(InteractionResponseType.Modal, interactionResp); 65 | } 66 | 67 | public async Task ExecuteAsync(ModalSubmitEventArgs e, IDictionary hiddenValues, IServiceProvider services) 68 | { 69 | // Construct new modal with dependencies injected into constructor 70 | IEnumerable constructorValues = Type.GetConstructors()[0].GetParameters().Select(x => services.GetService(x.ParameterType)); 71 | var modal = (IModal)Activator.CreateInstance(Type, constructorValues.ToArray()); 72 | 73 | // Inject hidden values into attributes 74 | if (hiddenValues != null) 75 | { 76 | foreach (var hidden in hiddenValues) 77 | { 78 | hiddenFields.First(x => x.Key.FieldName == hidden.Key).Value.SetValue(modal, hidden.Value); 79 | } 80 | } 81 | 82 | // Inject visible modal fields 83 | foreach (var visible in e.Values) 84 | { 85 | fields.First(x => x.Key.FieldName == visible.Key).Value.SetValue(modal, visible.Value); 86 | } 87 | 88 | await modal.HandleAsync(e.Interaction); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ModCore/HansTagImport/HansTag.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace ModCore.HansTagImport 5 | { 6 | public struct HansTag 7 | { 8 | [JsonProperty("name")] 9 | public string Name { get; set; } 10 | 11 | [JsonProperty("guild")] 12 | public ulong Guild { get; set; } 13 | 14 | [JsonProperty("channel")] 15 | public ulong? Channel { get; set; } 16 | 17 | // unknown 18 | [JsonProperty("kind")] 19 | public int Kind { get; set; } 20 | 21 | [JsonProperty("owner")] 22 | public ulong Owner { get; set; } 23 | 24 | [JsonProperty("hidden")] 25 | public bool Hidden { get; set; } 26 | 27 | [JsonProperty("latestRevision")] 28 | public DateTimeOffset LatestRevision { get; set; } 29 | 30 | [JsonProperty("aliases")] 31 | public string[] Aliases { get; set; } 32 | 33 | [JsonProperty("revisions")] 34 | public HansTagRevision[] Revisions { get; set; } 35 | } 36 | 37 | public struct HansTagRevision 38 | { 39 | [JsonProperty("contents")] 40 | public string Contents { get; set; } 41 | 42 | [JsonProperty("created")] 43 | public DateTimeOffset Created { get; set; } 44 | 45 | [JsonProperty("user")] 46 | public ulong User { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ModCore/HansTagImport/Importer.cs: -------------------------------------------------------------------------------- 1 | using ModCore.Database; 2 | using ModCore.Database.DatabaseEntities; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace ModCore.HansTagImport 11 | { 12 | public class Importer 13 | { 14 | private List _loadedTags = new List(); 15 | 16 | public void LoadTags(Stream tagStream) 17 | { 18 | var serializer = new JsonSerializer(); 19 | using var streamReader = new StreamReader(tagStream); 20 | using var jsonReader = new JsonTextReader(streamReader); 21 | _loadedTags = serializer.Deserialize>(jsonReader); 22 | } 23 | 24 | public async Task DumpToDatabase(DatabaseContext database) 25 | { 26 | if(_loadedTags == default(List)) 27 | { 28 | throw new InvalidOperationException("No tags were loaded!"); 29 | } 30 | 31 | foreach(var tag in _loadedTags) 32 | { 33 | var latestRevision = tag.Revisions.FirstOrDefault(x => x.Created == tag.LatestRevision); 34 | 35 | // check if tag exists 36 | var existsTag = database.Tags.Any(x => x.Name == tag.Name && x.GuildId == (long)tag.Guild && x.ChannelId == (long)(tag.Channel ?? 0)); 37 | 38 | var modcoreTag = existsTag? database.Tags.FirstOrDefault(x => x.Name == tag.Name && x.GuildId == (long)tag.Guild && x.ChannelId == (long)(tag.Channel ?? 0)) 39 | : new DatabaseTag(); 40 | 41 | modcoreTag.Name = tag.Name; 42 | modcoreTag.ChannelId = (long)(tag.Channel ?? 0); 43 | modcoreTag.Contents = latestRevision.Contents; 44 | modcoreTag.CreatedAt = latestRevision.Created.DateTime; 45 | modcoreTag.GuildId = (long)tag.Guild; 46 | modcoreTag.OwnerId = (long)tag.Owner; 47 | 48 | if(existsTag) 49 | database.Tags.Update(modcoreTag); 50 | else 51 | database.Tags.Add(modcoreTag); 52 | } 53 | 54 | await database.SaveChangesAsync(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ModCore/Integrations/PronounDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text.Json.Nodes; 5 | using System.Threading.Tasks; 6 | 7 | namespace ModCore.Integrations 8 | { 9 | public class PronounDB 10 | { 11 | private HttpClient _client; 12 | 13 | public PronounDB() 14 | { 15 | this._client = new HttpClient(); 16 | _client.BaseAddress = new Uri("https://pronoundb.org/api/v1/"); 17 | } 18 | 19 | public async Task GetPronounsForDiscordUserAsync(ulong id) 20 | { 21 | var response = await _client.GetAsync($"lookup?platform=discord&id={id}"); 22 | var responseObject = JsonObject.Parse(await response.Content.ReadAsStringAsync()); 23 | 24 | return _pronounMapping[responseObject["pronouns"].GetValue()]; 25 | } 26 | 27 | // thankies uwu https://github.com/Captain8771/PronounDBLib/blob/master/PronounDBLib/PronounDBClient.cs#L20-L40 28 | private static readonly IReadOnlyDictionary _pronounMapping = new Dictionary() 29 | { 30 | { "unspecified", "Unspecified" }, 31 | { "hh", "He/Him" }, 32 | { "hi", "He/It" }, 33 | { "hs", "He/She" }, 34 | { "ht", "He/They" }, 35 | { "ih", "It/Him" }, 36 | { "ii", "It/Its" }, 37 | { "is", "It/She" }, 38 | { "it", "It/They" }, 39 | { "shh", "She/He" }, 40 | { "sh", "She/Her" }, 41 | { "si", "She/It" }, 42 | { "st", "She/They" }, 43 | { "th", "They/He" }, 44 | { "ti", "They/It" }, 45 | { "ts", "They/She" }, 46 | { "tt", "They/Them" }, 47 | { "any", "Any Pronouns" }, 48 | { "other", "Other Pronouns" }, 49 | { "ask", "Ask Me My Pronouns" }, 50 | { "avoid", "Avoid Pronouns, Use My Name" } 51 | }.AsReadOnly(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ModCore/LegacyCommands/Eval.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DSharpPlus; 5 | using DSharpPlus.CommandsNext; 6 | using DSharpPlus.CommandsNext.Attributes; 7 | using DSharpPlus.Entities; 8 | using Microsoft.CodeAnalysis.CSharp.Scripting; 9 | using Microsoft.CodeAnalysis.Scripting; 10 | using ModCore.Entities; 11 | using ModCore.Utils.Extensions; 12 | 13 | namespace ModCore.LegacyCommands 14 | { 15 | public class Eval : BaseCommandModule 16 | { 17 | public SharedData shared; 18 | 19 | public Eval(SharedData shared) 20 | { 21 | this.shared = shared; 22 | } 23 | 24 | [Command("eval"), Aliases("evalcs", "cseval", "roslyn"), Description("Evaluates C# code."), Hidden, RequireOwner] 25 | public async Task EvalCS(CommandContext context, [RemainingText] string code) 26 | { 27 | var message = context.Message; 28 | 29 | var code_start = code.IndexOf("```") + 3; 30 | code_start = code.IndexOf('\n', code_start) + 1; 31 | var code_end = code.LastIndexOf("```"); 32 | 33 | if (code_start == -1 || code_end == -1) 34 | throw new ArgumentException("⚠️ You need to wrap the code into a code block."); 35 | 36 | var cs = code.Substring(code_start, code_end - code_start); 37 | 38 | message = await context.ElevatedRespondAsync(embed: new DiscordEmbedBuilder() 39 | .WithColor(new DiscordColor("#FF007F")) 40 | .WithDescription("💭 Evaluating...") 41 | .Build()).ConfigureAwait(false); 42 | 43 | try 44 | { 45 | var globals = new TestVariables(context.Message, context.Client, context, shared.ModCore, shared); 46 | 47 | var scriptoptions = ScriptOptions.Default; 48 | scriptoptions = scriptoptions.WithImports("System", "System.Collections.Generic", "System.Linq", "System.Text", "System.Threading.Tasks", "DSharpPlus", "DSharpPlus.CommandsNext", "DSharpPlus.Interactivity", "ModCore"); 49 | scriptoptions = scriptoptions.WithReferences(AppDomain.CurrentDomain.GetAssemblies().Where(xa => !xa.IsDynamic && !string.IsNullOrWhiteSpace(xa.Location))); 50 | 51 | var script = CSharpScript.Create(cs, scriptoptions, typeof(TestVariables)); 52 | script.Compile(); 53 | var result = await script.RunAsync(globals).ConfigureAwait(false); 54 | 55 | if (result != null && result.ReturnValue != null && !string.IsNullOrWhiteSpace(result.ReturnValue.ToString())) 56 | await message.ModifyAsync(embed: new DiscordEmbedBuilder { Title = "✅ Evaluation Result", Description = result.ReturnValue.ToString(), Color = new DiscordColor("#089FDF") }.Build()).ConfigureAwait(false); 57 | else 58 | await message.ModifyAsync(embed: new DiscordEmbedBuilder { Title = "✅ Evaluation Successful", Description = "No result was returned.", Color = new DiscordColor("#089FDF") }.Build()).ConfigureAwait(false); 59 | } 60 | catch (Exception ex) 61 | { 62 | await message.ModifyAsync(embed: new DiscordEmbedBuilder { Title = "⚠️ Evaluation Failure", Description = string.Concat("**", ex.GetType().ToString(), "**: ", ex.Message), Color = new DiscordColor("#FF0000") }.Build()).ConfigureAwait(false); 63 | } 64 | } 65 | } 66 | 67 | public class TestVariables 68 | { 69 | public DiscordMessage Message { get; set; } 70 | public DiscordChannel Channel { get; set; } 71 | public DiscordGuild Guild { get; set; } 72 | public DiscordUser User { get; set; } 73 | public DiscordMember Member { get; set; } 74 | public CommandContext Context { get; set; } 75 | 76 | public TestVariables(DiscordMessage msg, DiscordClient client, CommandContext ctx, ModCore core, SharedData share) 77 | { 78 | this.Client = client; 79 | 80 | this.Message = msg; 81 | this.Channel = msg.Channel; 82 | this.Guild = this.Channel.Guild; 83 | this.User = this.Message.Author; 84 | if (this.Guild != null) 85 | this.Member = this.Guild.GetMemberAsync(this.User.Id).ConfigureAwait(false).GetAwaiter().GetResult(); 86 | this.Context = ctx; 87 | this.ModCore = core; 88 | this.SharedData = share; 89 | } 90 | 91 | public DiscordClient Client; 92 | public ModCore ModCore; 93 | public SharedData SharedData; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ModCore/Listeners/EmbedMessageLinks.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.EventArgs; 2 | using DSharpPlus; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using ModCore.Database; 5 | using ModCore.Extensions.Attributes; 6 | using ModCore.Extensions.Enums; 7 | using System.Threading.Tasks; 8 | using ModCore.Database.JsonEntities; 9 | using ModCore.Utils.Extensions; 10 | using System.Text.RegularExpressions; 11 | using System.Collections.Generic; 12 | using System; 13 | using DSharpPlus.Entities; 14 | using Humanizer; 15 | using System.Linq; 16 | using System.IO; 17 | 18 | namespace ModCore.Listeners 19 | { 20 | public class EmbedMessageLinks 21 | { 22 | public static Regex AlwaysEmbedRegex = new Regex(@"https?:\/\/.*?discord.com\/channels\/([0-9]+)\/([0-9]+)\/([0-9]+)", RegexOptions.Compiled); 23 | public static Regex PrefixedEmbedRegex = new Regex(@"!https?:\/\/.*?discord.com\/channels\/([0-9]+)\/([0-9]+)\/([0-9]+)", RegexOptions.Compiled); 24 | 25 | [AsyncListener(EventType.MessageCreated)] 26 | public static async Task ReactionAddedAsync(MessageCreateEventArgs eventargs, DatabaseContextBuilder database, 27 | DiscordClient client, IMemoryCache cache) 28 | { 29 | using (DatabaseContext ctx = database.CreateContext()) 30 | { 31 | GuildSettings settings; 32 | settings = eventargs.Guild.GetGuildSettings(ctx); 33 | 34 | if (settings == null) 35 | return; // No guild settings so starboard is disabled. 36 | 37 | if (settings.EmbedMessageLinks == EmbedMessageLinksMode.Disabled) 38 | return;// embedding is disabled. 39 | 40 | Regex rex = settings.EmbedMessageLinks == EmbedMessageLinksMode.Always? AlwaysEmbedRegex : PrefixedEmbedRegex; 41 | var matches = rex.Matches(eventargs.Message.Content); 42 | 43 | var embeds = new List(); 44 | 45 | foreach(Match match in matches.Take(10)) 46 | { 47 | var guildId = ulong.Parse(match.Groups[1].Value); 48 | var channelId = ulong.Parse(match.Groups[2].Value); 49 | var messageId = ulong.Parse(match.Groups[3].Value); 50 | 51 | if (guildId != eventargs.Guild.Id) return; // not same guild 52 | if (!eventargs.Guild.Channels.ContainsKey(channelId)) return; // channel not found 53 | 54 | var channel = eventargs.Guild.Channels[channelId]; 55 | try 56 | { 57 | var message = await channel.GetMessageAsync(messageId); 58 | // no exception = exists 59 | 60 | var truncatedText = message.Content.Length > 250 ? message.Content.Truncate(250) + "..." : message.Content; 61 | var embed = new DiscordEmbedBuilder() 62 | .WithDescription($"{truncatedText}\n") 63 | .WithAuthor(message.Author.GetDisplayUsername(), iconUrl: message.Author.GetAvatarUrl(ImageFormat.Gif)); 64 | 65 | var count = message.Attachments.Count(); 66 | 67 | var imageFiles = message.Attachments.Where(x => 68 | { 69 | var uri = new Uri(x.Url); 70 | return StarboardListeners.validFileExts.Contains(Path.GetExtension(uri.AbsolutePath)); 71 | }); 72 | 73 | var more = ""; 74 | 75 | if (imageFiles.Any()) 76 | { 77 | embed.WithThumbnail(imageFiles.First().Url); 78 | count--; 79 | more = " more"; 80 | } 81 | 82 | if (count > 0) 83 | { 84 | embed.WithDescription(embed.Description + $"\n_Contains ({count}){more} attachments._"); 85 | } 86 | 87 | embed.WithDescription(embed.Description + $"\n[Jump to message]({match.Value.Replace("!", "")})"); 88 | 89 | embeds.Add(embed.Build()); 90 | } 91 | catch (Exception) { } 92 | } 93 | 94 | if(embeds.Count > 0) 95 | await eventargs.Channel.SendMessageAsync(x => x.AddEmbeds(embeds)); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ModCore/Listeners/JoinLog.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using System.Threading.Tasks; 4 | using DSharpPlus.Entities; 5 | using DSharpPlus.EventArgs; 6 | using ModCore.Database; 7 | using ModCore.Database.JsonEntities; 8 | using ModCore.Extensions.Attributes; 9 | using ModCore.Extensions.Enums; 10 | using ModCore.Utils.Extensions; 11 | 12 | namespace ModCore.Listeners 13 | { 14 | public static class JoinLog 15 | { 16 | private static readonly Regex WelcomeRegex = new("{{(.*?)}}", RegexOptions.Compiled); 17 | 18 | [AsyncListener(EventType.GuildMemberAdded)] 19 | public static async Task LogNewMember(GuildMemberAddEventArgs eventargs, DatabaseContextBuilder database) 20 | { 21 | GuildSettings config; 22 | await using (var db = database.CreateContext()) 23 | config = eventargs.Guild.GetGuildSettings(db); 24 | 25 | if(config == null) 26 | { 27 | return; 28 | } 29 | 30 | DiscordChannel channel; 31 | if (config.Logging.JoinLog_Enable) 32 | { 33 | var m = eventargs.Member; 34 | channel = eventargs.Guild.GetChannel(config.Logging.ChannelId); 35 | if (channel != null) 36 | { 37 | var newUser = DateTimeOffset.Now.Subtract(m.CreationTimestamp.DateTime).TotalDays < 30; 38 | var embed = new DiscordEmbedBuilder() 39 | .WithTitle("New member joined") 40 | .WithDescription($"ID: ({m.Id})" + (newUser? "\n\n⚠️ This is a very new user!" : "")) 41 | .WithAuthor($"{m.GetDisplayUsername()}", 42 | iconUrl: string.IsNullOrEmpty(m.AvatarHash) ? m.DefaultAvatarUrl : m.AvatarUrl) 43 | .AddField("Join Date", m.JoinedAt == default? "Unknown" : m.JoinedAt.ToString()) 44 | .AddField("Register Date", m.CreationTimestamp == default? "Unknown" : m.CreationTimestamp.ToString()) 45 | .WithColor(newUser ? DiscordColor.Red : DiscordColor.Green) 46 | .AddField("IDs", $"```ini\nUser = {eventargs.Member.Id}```"); ; 47 | await channel.ElevatedMessageAsync(embed); 48 | } 49 | } 50 | 51 | if (!config.Welcome.Enable) 52 | return; 53 | 54 | if (config.Welcome.ChannelId == 0) 55 | return; 56 | 57 | channel = eventargs.Guild.GetChannel(config.Welcome.ChannelId); 58 | 59 | if (channel == null) 60 | return; 61 | 62 | var message = config.Welcome.Message; 63 | if (string.IsNullOrEmpty(message)) 64 | return; 65 | 66 | string attachment = null; 67 | string embedtitle = null; 68 | var isEmbed = config.Welcome.IsEmbed; 69 | 70 | message = WelcomeRegex.Replace(message, match => 71 | { 72 | var welcome = match.Groups[1].Value; 73 | 74 | switch (welcome) 75 | { 76 | case "username": 77 | return eventargs.Member.Username; 78 | 79 | case "mention": 80 | return eventargs.Member.Mention; 81 | 82 | case "userid": 83 | return eventargs.Member.Id.ToString(); 84 | 85 | case "guildname": 86 | return eventargs.Guild.Name; 87 | 88 | case "channelname": 89 | return channel.Name; 90 | 91 | case "membercount": 92 | return eventargs.Guild.MemberCount.ToString(); 93 | 94 | case "owner-username": 95 | return eventargs.Guild.Owner.Username; 96 | 97 | case "guild-icon-url": 98 | return eventargs.Guild.IconUrl; 99 | 100 | case "channel-count": 101 | return eventargs.Guild.Channels.Count.ToString(); 102 | 103 | case "role-count": 104 | return eventargs.Guild.Roles.Count.ToString(); 105 | 106 | default: 107 | if (welcome.StartsWith("image:")) 108 | attachment = welcome.Substring("image:".Length); 109 | else if (welcome.StartsWith("embed-title:")) 110 | embedtitle = welcome.Substring("embed-title:".Length); 111 | return ""; 112 | } 113 | }); 114 | 115 | if (!isEmbed) 116 | { 117 | await channel.SafeMessageAsync(privileged: false, s: $"{message}\n\n{attachment}"); 118 | } 119 | else 120 | { 121 | var embed = new DiscordEmbedBuilder() 122 | .WithDescription(message); 123 | 124 | if (!string.IsNullOrWhiteSpace(embedtitle)) 125 | embed.WithTitle(embedtitle); 126 | if (!string.IsNullOrWhiteSpace(attachment)) 127 | embed.WithImageUrl(attachment); 128 | 129 | await channel.ElevatedMessageAsync(embed); 130 | } 131 | } 132 | 133 | [AsyncListener(EventType.GuildMemberRemoved)] 134 | public static async Task LogLeaveMember(GuildMemberRemoveEventArgs eventargs, DatabaseContextBuilder database) 135 | { 136 | GuildSettings config; 137 | await using (var db = database.CreateContext()) 138 | config = eventargs.Guild.GetGuildSettings(db); 139 | 140 | if (config == null || !config.Logging.JoinLog_Enable) 141 | return; 142 | 143 | var member = eventargs.Member; 144 | var channel = eventargs.Guild.GetChannel(config.Logging.ChannelId); 145 | 146 | if (channel == null) 147 | return; 148 | 149 | var embed = new DiscordEmbedBuilder() 150 | .WithTitle("Member left") 151 | .WithDescription($"ID: ({member.Id})") 152 | .WithAuthor($"{member.GetDisplayUsername()}", 153 | iconUrl: string.IsNullOrEmpty(member.AvatarHash) ? member.DefaultAvatarUrl : member.AvatarUrl) 154 | .AddField("IDs", $"```ini\nUser = {eventargs.Member.Id}\n```"); ; 155 | 156 | if (member.JoinedAt.DateTime == DateTime.MinValue) 157 | embed.AddField("Join Date", member.JoinedAt == default? "Unknown" : member.JoinedAt.ToString()); 158 | embed 159 | .AddField("Leave Date", $"{DateTime.Now}") 160 | .AddField("Register Date", member.CreationTimestamp == default? "Unknown" : member.CreationTimestamp.ToString()) 161 | .WithColor(DiscordColor.LightGray); 162 | 163 | await channel.ElevatedMessageAsync(embed); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /ModCore/Listeners/LevelUp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DSharpPlus.Entities; 5 | using DSharpPlus.EventArgs; 6 | using ModCore.Database; 7 | using ModCore.Database.DatabaseEntities; 8 | using ModCore.Database.JsonEntities; 9 | using ModCore.Entities; 10 | using ModCore.Extensions.Attributes; 11 | using ModCore.Extensions.Enums; 12 | using ModCore.Utils.Extensions; 13 | 14 | namespace ModCore.Listeners 15 | { 16 | public static class LevelUp 17 | { 18 | [AsyncListener(EventType.MessageCreated)] 19 | public static async Task CheckLevelUpdates(MessageCreateEventArgs eventargs, DatabaseContextBuilder database, Settings settings) 20 | { 21 | // Storing vars for quick reference 22 | var server = eventargs.Guild; 23 | var message = eventargs.Message; 24 | var user = eventargs.Message.Author as DiscordMember; 25 | 26 | if (user == null || user.IsBot) 27 | return; 28 | 29 | GuildSettings config; 30 | DatabaseLevel leveldata = null; 31 | 32 | await using var db = database.CreateContext(); 33 | config = eventargs.Guild.GetGuildSettings(db); 34 | if (config == null) 35 | return; 36 | if (!config.Levels.Enabled) 37 | return; 38 | 39 | // Get level xp data and assign new when null 40 | if (db.Levels.Any(x => x.UserId == (long)user.Id && x.GuildId == (long)server.Id)) 41 | { 42 | leveldata = db.Levels.First(x => x.UserId == (long)user.Id && x.GuildId == (long)server.Id); 43 | // do level stuff 44 | #if !DEBUG 45 | if (DateTime.Now.Subtract(leveldata.LastXpGrant).TotalMinutes < 5) 46 | return; 47 | #endif 48 | } 49 | 50 | leveldata ??= new DatabaseLevel() 51 | { 52 | UserId = (long)user.Id, 53 | GuildId = (long)server.Id, 54 | Experience = 0, 55 | LastXpGrant = DateTime.Now 56 | }; 57 | 58 | // Set new last xp datetime 59 | leveldata.LastXpGrant = DateTime.Now; 60 | 61 | // Getting old XP value and new XP value 62 | var previousxp = leveldata.Experience; 63 | leveldata.Experience += new Random().Next(10, 50); 64 | 65 | // Do checks for message 66 | if (config.Levels.MessagesEnabled) 67 | { 68 | var msgchannel = eventargs.Channel; 69 | if (config.Levels.RedirectMessages) 70 | { 71 | // Check whether channel exists and then change channel 72 | if (server.Channels.ContainsKey(config.Levels.ChannelId)) 73 | { 74 | msgchannel = server.Channels[config.Levels.ChannelId]; 75 | } 76 | } 77 | 78 | // Calculate old and new level from XPs 79 | var oldlevel = CalculateLevel(previousxp); 80 | var newlevel = CalculateLevel(leveldata.Experience); 81 | 82 | if (newlevel > oldlevel) 83 | { 84 | // Leveled up! congratulate user. 85 | var embed = new DiscordEmbedBuilder() 86 | .WithTitle($"🏆 Congratulations, {user.Nickname ?? user.Username}! You've leveled up!") 87 | .WithDescription($"Level {oldlevel} ➡ Level {newlevel}") 88 | .AddField("Experience", $"You currently have {leveldata.Experience} xp. " + 89 | $"{CalculateRequiredXp(newlevel + 1) - leveldata.Experience} needed to level up."); 90 | await msgchannel.SendMessageAsync(embed); 91 | } 92 | } 93 | 94 | // setting new data and updating 95 | db.Levels.Update(leveldata); 96 | await db.SaveChangesAsync(); 97 | } 98 | 99 | public static int CalculateLevel(int xp) 100 | { 101 | // XP formula: 150 * (x^2) 102 | // Level formula: sqrt(x / 150) 103 | // (and ofc floor to get round integer levels) 104 | return (int)Math.Floor(Math.Sqrt(xp / 150)); 105 | } 106 | 107 | public static int CalculateRequiredXp(int level) 108 | { 109 | return 150 * (int)Math.Pow(level, 2); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ModCore/Listeners/Linkfilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using System.Threading.Tasks; 5 | using DSharpPlus; 6 | using DSharpPlus.Entities; 7 | using DSharpPlus.EventArgs; 8 | using ModCore.Database; 9 | using ModCore.Database.JsonEntities; 10 | using ModCore.Extensions.Attributes; 11 | using ModCore.Extensions.Enums; 12 | using ModCore.Utils.Extensions; 13 | 14 | namespace ModCore.Listeners 15 | { 16 | public static class Linkfilter 17 | { 18 | public static Regex InviteRegex { get; } = 19 | new Regex(@"discord(?:\.gg|app\.com\/invite)\/([\w\-]+)", RegexOptions.Compiled); 20 | 21 | public static ConcurrentDictionary InviteCache { get; } = 22 | new ConcurrentDictionary(); 23 | 24 | [AsyncListener(EventType.MessageCreated)] 25 | public static async Task RemoveSuspiciousLinks(MessageCreateEventArgs eventargs, DatabaseContextBuilder database, DiscordClient client) 26 | { 27 | if (eventargs.Author == null || eventargs.Channel == null) 28 | return; 29 | 30 | if (eventargs.Message.WebhookMessage) 31 | return; 32 | 33 | if ((eventargs.Channel.PermissionsFor(eventargs.Author as DiscordMember) & Permissions.ManageMessages) != 0) return; 34 | 35 | if (eventargs.Channel.Guild == null) 36 | return; 37 | 38 | GuildSettings config; 39 | await using (var db = database.CreateContext()) 40 | config = eventargs.Guild.GetGuildSettings(db); 41 | if (config == null) 42 | return; 43 | 44 | var lfSettings = config.Linkfilter; 45 | 46 | if (lfSettings.ExemptUserIds.Contains(eventargs.Message.Author.Id)) 47 | return; 48 | 49 | if (eventargs.Message.Author is DiscordMember member && 50 | member.Roles.Select(xr => xr.Id).Intersect(lfSettings.ExemptRoleIds).Any()) 51 | return; 52 | 53 | if (lfSettings.BlockInviteLinks) 54 | await FindAndPurgeInvites(client, eventargs, lfSettings); 55 | } 56 | 57 | private static async Task FindAndPurgeInvites(DiscordClient client, MessageCreateEventArgs eventargs, 58 | GuildLinkfilterSettings linkfilter) 59 | { 60 | var matches = InviteRegex.Matches(eventargs.Message.Content); 61 | if (!matches.Any()) 62 | return; 63 | 64 | foreach (Match match in matches) 65 | { 66 | var invk = match.Groups[1].Value; 67 | if (!InviteCache.TryGetValue(invk, out var inv)) 68 | { 69 | inv = await client.GetInviteByCodeAsync(invk); 70 | InviteCache.TryAdd(invk, inv); 71 | } 72 | 73 | if (linkfilter.ExemptInviteGuildIds.Contains(inv.Guild.Id)) 74 | continue; 75 | 76 | await eventargs.Message.DeleteAsync($"Discovered invite <{inv.Code}> and deleted message"); 77 | break; 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /ModCore/Listeners/MessageSnipe.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using DSharpPlus.EventArgs; 3 | using Humanizer; 4 | using Microsoft.Extensions.Caching.Memory; 5 | using ModCore.Database; 6 | using ModCore.Entities; 7 | using ModCore.Extensions.Attributes; 8 | using ModCore.Extensions.Enums; 9 | using ModCore.Utils.Extensions; 10 | using System; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Listeners 14 | { 15 | public static class MessageSnipe 16 | { 17 | [AsyncListener(EventType.MessageDeleted)] 18 | public static async Task MessageSniped(MessageDeleteEventArgs eventargs, SharedData sharedData, DatabaseContextBuilder database, IMemoryCache cache) 19 | { 20 | await Task.Yield(); 21 | 22 | if (eventargs.Message is null) 23 | return; 24 | if (eventargs.Message.WebhookMessage) 25 | return; 26 | 27 | if (((!string.IsNullOrEmpty(eventargs.Message?.Content)) || eventargs.Message.Attachments.Count > 0) && !eventargs.Message.Author.IsBot) 28 | { 29 | cache.Set($"snipe_{eventargs.Channel.Id}", eventargs.Message, TimeSpan.FromHours(12)); 30 | } 31 | 32 | await using var db = database.CreateContext(); 33 | var cfg = eventargs.Guild.GetGuildSettings(db); 34 | if (cfg.Logging.EditLog_Enable) 35 | { 36 | var channel = eventargs.Guild.GetChannel(cfg.Logging.ChannelId); 37 | if (channel == null) 38 | return; 39 | if (eventargs.Message != null && eventargs.Message.Author != null) 40 | { 41 | var embed = new DiscordEmbedBuilder() 42 | .WithTitle("Message Deleted") 43 | .WithAuthor($"{eventargs.Message.Author.Username}", 44 | iconUrl: eventargs.Message.Author.GetAvatarUrl(DSharpPlus.ImageFormat.Auto)) 45 | .AddField("Content", string.IsNullOrEmpty(eventargs.Message?.Content) ? eventargs.Message.Content.Truncate(1000) : "Original Content Unknown.") 46 | .AddField("Channel", eventargs.Message.Channel.Mention) 47 | .WithColor(DiscordColor.Orange) 48 | .AddField("IDs", $"```ini\nUser = {eventargs.Message.Author.Id}\nChannel = {eventargs.Channel.Id}\nMessage = {eventargs.Message.Id}```"); 49 | await channel.ElevatedMessageAsync(embed); 50 | } 51 | } 52 | } 53 | 54 | [AsyncListener(EventType.MessageUpdated)] 55 | public static async Task MessageEdited(MessageUpdateEventArgs eventargs, SharedData sharedData, DatabaseContextBuilder database, IMemoryCache cache) 56 | { 57 | if (eventargs.Message is null) 58 | return; 59 | if (eventargs.Message.WebhookMessage) 60 | return; 61 | if (eventargs.MessageBefore is null) 62 | return; 63 | 64 | await Task.Yield(); 65 | 66 | if (((!string.IsNullOrEmpty(eventargs.MessageBefore?.Content)) || eventargs.MessageBefore.Attachments.Count > 0) && !eventargs.Message.Author.IsBot) 67 | { 68 | cache.Set($"esnipe_{eventargs.Channel.Id}", eventargs.MessageBefore, TimeSpan.FromHours(12)); 69 | } 70 | 71 | await using var db = database.CreateContext(); 72 | var cfg = eventargs.Guild.GetGuildSettings(db); 73 | if(cfg != null && cfg.Logging.EditLog_Enable) 74 | { 75 | var channel = eventargs.Guild.GetChannel(cfg.Logging.ChannelId); 76 | if (channel is null) 77 | return; 78 | 79 | if (eventargs.Message.Content != eventargs.MessageBefore.Content) 80 | { 81 | var embed = new DiscordEmbedBuilder() 82 | .WithTitle("Message Edited") 83 | .WithAuthor($"{eventargs.Message.Author.Username}", 84 | iconUrl: eventargs.Author.GetAvatarUrl(DSharpPlus.ImageFormat.Auto)) 85 | .AddField("Original Message", string.IsNullOrEmpty(eventargs.Message?.Content) ? eventargs.MessageBefore.Content.Truncate(1000) : "Original Content Unknown.") 86 | .AddField("Edited Message", eventargs.Message.Content.Truncate(1000)) 87 | .AddField("Channel", eventargs.Message.Channel.Mention) 88 | .WithColor(DiscordColor.Orange) 89 | .AddField("IDs", $"```ini\nUser = {eventargs.Message.Author.Id}\nChannel = {eventargs.Channel.Id}\nMessage = {eventargs.Message.Id}```"); 90 | 91 | var msg = new DiscordMessageBuilder() 92 | .WithEmbed(embed) 93 | .AddComponents(new DiscordLinkButtonComponent(eventargs.Message.JumpLink.ToString(), "Go to message")); 94 | await channel.ElevatedMessageAsync(embed); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ModCore/Listeners/NoBotFarm.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using DSharpPlus.EventArgs; 4 | using Microsoft.Extensions.Logging; 5 | using ModCore.Entities; 6 | using ModCore.Extensions.Attributes; 7 | using ModCore.Extensions.Enums; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace ModCore.Listeners 12 | { 13 | public class NoBotFarm 14 | { 15 | [AsyncListener(EventType.GuildCreated)] 16 | public static async Task NoBotFarmsPleaseAsync(GuildCreateEventArgs e, DiscordClient client, Settings settings) 17 | { 18 | client.Logger.LogInformation($"New guild joined: {e.Guild.Name}. Starting sus-guild check..."); 19 | int botCount = -1; 20 | 21 | // pre-fetch logging channel 22 | var channel = await client.GetChannelAsync(settings.SusGuildChannelId); 23 | 24 | if(e.Guild.MemberCount <= 2000) 25 | { 26 | var allMembers = await e.Guild.GetAllMembersAsync(); 27 | botCount = allMembers.Count(x => x.IsBot); 28 | } 29 | 30 | double botRatio = ((double)botCount / (double)e.Guild.MemberCount) * 100d; 31 | 32 | var embed = new DiscordEmbedBuilder() 33 | .WithTitle("Joined new guild!") 34 | .WithDescription($"Guild Name: {e.Guild.Name}\nID: {e.Guild.Id}") 35 | .AddField("Members", e.Guild.MemberCount.ToString(), true) 36 | .AddField("Bots", botCount >= 0 ? botCount.ToString() : "LargeGuild", true) 37 | .AddField("Bot Ratio", botCount >= 0 ? $"{botRatio}%" : "LargeGuild", true); 38 | if(!string.IsNullOrEmpty(e.Guild.IconHash)) 39 | embed.WithThumbnail(e.Guild.GetIconUrl(ImageFormat.Auto)); 40 | 41 | var messageBuilder = new DiscordMessageBuilder(); 42 | if (botRatio > 25) 43 | { 44 | var owners = client.CurrentApplication.Owners; 45 | messageBuilder.WithContent($"This guild has an extraordinarily large bot ratio! Please assess this guild's legitimacy!\n" 46 | + $"{string.Join(", ", owners.Select(x => x.Mention))}"); 47 | embed.WithColor(DiscordColor.Red); 48 | messageBuilder.WithAllowedMentions(owners.Select(x => new UserMention(x)).Cast()); 49 | } 50 | else 51 | { 52 | embed.WithColor(DiscordColor.Green); 53 | } 54 | 55 | messageBuilder.AddEmbed(embed); 56 | 57 | messageBuilder.AddComponents(new DiscordComponent[] 58 | { 59 | new DiscordButtonComponent(ButtonStyle.Danger, "lguild", "Leave Guild", emoji: new DiscordComponentEmoji("❌")) 60 | }); 61 | 62 | await channel.SendMessageAsync(messageBuilder); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ModCore/Listeners/Reactions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DSharpPlus; 5 | using DSharpPlus.Entities; 6 | using DSharpPlus.EventArgs; 7 | using Humanizer; 8 | using ModCore.Database; 9 | using ModCore.Database.DatabaseEntities; 10 | using ModCore.Database.JsonEntities; 11 | using ModCore.Entities; 12 | using ModCore.Extensions.Attributes; 13 | using ModCore.Extensions.Enums; 14 | using ModCore.Utils; 15 | using ModCore.Utils.Extensions; 16 | 17 | namespace ModCore.Listeners 18 | { 19 | public class Reactions 20 | { 21 | [AsyncListener(EventType.MessageReactionAdded)] 22 | public static async Task ReactionAdd(MessageReactionAddEventArgs eventargs, DatabaseContextBuilder database, DiscordClient client) 23 | { 24 | GuildSettings config = null; 25 | using (var db = database.CreateContext()) 26 | { 27 | config = eventargs.Channel.Guild.GetGuildSettings(db); 28 | if (config == null) 29 | return; 30 | 31 | // Reaction roles 32 | if (config.ReactionRoles.Any(x => (ulong)x.ChannelId == eventargs.Channel.Id && (ulong)x.MessageId == eventargs.Message.Id 33 | && (ulong)x.Reaction.EmojiId == eventargs.Emoji.Id && x.Reaction.EmojiName == eventargs.Emoji.Name)) 34 | { 35 | var reactionroleid = (ulong)config.ReactionRoles.First( 36 | x => (ulong)x.ChannelId == eventargs.Channel.Id && (ulong)x.MessageId == eventargs.Message.Id 37 | && (ulong)x.Reaction.EmojiId == eventargs.Emoji.Id && x.Reaction.EmojiName == eventargs.Emoji.Name).RoleId; 38 | var reactionrole = eventargs.Channel.Guild.GetRole(reactionroleid); 39 | var member = await eventargs.Channel.Guild.GetMemberAsync(eventargs.User.Id); 40 | if(!member.Roles.Any(x => x.Id == reactionroleid)) 41 | await member.GrantRoleAsync(reactionrole); 42 | } 43 | 44 | return; 45 | } 46 | } 47 | 48 | [AsyncListener(EventType.MessageReactionRemoved)] 49 | public static async Task ReactionRemove(MessageReactionRemoveEventArgs eventargs, DatabaseContextBuilder database, DiscordClient client) 50 | { 51 | GuildSettings config = null; 52 | using (var db = database.CreateContext()) 53 | { 54 | config = eventargs.Channel.Guild.GetGuildSettings(db); 55 | if (config == null) 56 | return; 57 | 58 | if (config.ReactionRoles.Any(x => (ulong)x.ChannelId == eventargs.Channel.Id && (ulong)x.MessageId == eventargs.Message.Id && (ulong)x.Reaction.EmojiId == eventargs.Emoji.Id && x.Reaction.EmojiName == eventargs.Emoji.Name)) 59 | { 60 | var reactionroleid = (ulong)config.ReactionRoles.First( 61 | x => (ulong)x.ChannelId == eventargs.Channel.Id && (ulong)x.MessageId == eventargs.Message.Id && (ulong)x.Reaction.EmojiId == eventargs.Emoji.Id && x.Reaction.EmojiName == eventargs.Emoji.Name).RoleId; 62 | var reactionrole = eventargs.Channel.Guild.GetRole(reactionroleid); 63 | var member = await eventargs.Channel.Guild.GetMemberAsync(eventargs.User.Id); 64 | if (member.Roles.Any(x => x.Id == reactionroleid)) 65 | await member.RevokeRoleAsync(reactionrole); 66 | } 67 | 68 | return; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ModCore/Listeners/UnbanTimerRemove.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DSharpPlus; 3 | using DSharpPlus.EventArgs; 4 | using ModCore.Database; 5 | using ModCore.Database.JsonEntities; 6 | using ModCore.Entities; 7 | using ModCore.Extensions.Attributes; 8 | using ModCore.Extensions.Enums; 9 | 10 | namespace ModCore.Listeners 11 | { 12 | public class UnbanTimerRemove 13 | { 14 | [AsyncListener(EventType.GuildBanRemoved)] 15 | public static async Task GuildBanRemoved(GuildBanRemoveEventArgs eventargs, DatabaseContextBuilder database, DiscordClient client, SharedData sharedData) 16 | { 17 | var timer = Timers.FindNearestTimer(TimerActionType.Unban, eventargs.Member.Id, 0, eventargs.Guild.Id, database); 18 | if (timer != null) 19 | await Timers.UnscheduleTimersAsync(timer); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ModCore/Migrations/20220509151704_RemoveBansWarninge.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 4 | 5 | #nullable disable 6 | 7 | namespace ModCore.Migrations 8 | { 9 | public partial class RemoveBansWarninge : Migration 10 | { 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.DropTable( 14 | name: "mcore_bans"); 15 | 16 | migrationBuilder.DropTable( 17 | name: "mcore_warnings"); 18 | } 19 | 20 | protected override void Down(MigrationBuilder migrationBuilder) 21 | { 22 | migrationBuilder.CreateTable( 23 | name: "mcore_bans", 24 | columns: table => new 25 | { 26 | id = table.Column(type: "integer", nullable: false) 27 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 28 | ban_reason = table.Column(type: "text", nullable: true), 29 | guild_id = table.Column(type: "bigint", nullable: false), 30 | user_id = table.Column(type: "bigint", nullable: false) 31 | }, 32 | constraints: table => 33 | { 34 | table.PrimaryKey("PK_mcore_bans", x => x.id); 35 | }); 36 | 37 | migrationBuilder.CreateTable( 38 | name: "mcore_warnings", 39 | columns: table => new 40 | { 41 | id = table.Column(type: "integer", nullable: false) 42 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 43 | guild_id = table.Column(type: "bigint", nullable: false), 44 | issued_at = table.Column(type: "timestamptz", nullable: false), 45 | issuer_id = table.Column(type: "bigint", nullable: false), 46 | member_id = table.Column(type: "bigint", nullable: false), 47 | warning_text = table.Column(type: "text", nullable: false) 48 | }, 49 | constraints: table => 50 | { 51 | table.PrimaryKey("PK_mcore_warnings", x => x.id); 52 | }); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ModCore/Migrations/20220514235804_UpdatedLeveler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 4 | 5 | #nullable disable 6 | 7 | namespace ModCore.Migrations 8 | { 9 | public partial class UpdatedLeveler : Migration 10 | { 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "mcore_levels", 15 | columns: table => new 16 | { 17 | id = table.Column(type: "integer", nullable: false) 18 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 19 | channel_id = table.Column(type: "bigint", nullable: false), 20 | user_id = table.Column(type: "bigint", nullable: false), 21 | experience = table.Column(type: "integer", nullable: false), 22 | last_xp_grant = table.Column(type: "timestamptz", nullable: false) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("PK_mcore_levels", x => x.id); 27 | }); 28 | 29 | migrationBuilder.CreateIndex( 30 | name: "mcore_levels_channel_id_user_id_key", 31 | table: "mcore_levels", 32 | columns: new[] { "channel_id", "user_id" }, 33 | unique: true); 34 | } 35 | 36 | protected override void Down(MigrationBuilder migrationBuilder) 37 | { 38 | migrationBuilder.DropTable( 39 | name: "mcore_levels"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ModCore/Migrations/20220516213245_UpdateTags.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace ModCore.Migrations 6 | { 7 | public partial class UpdateTags : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.DropIndex( 12 | name: "mcore_tags_channel_id_tag_name_key", 13 | table: "mcore_tags"); 14 | 15 | migrationBuilder.AddColumn( 16 | name: "guild_id", 17 | table: "mcore_tags", 18 | type: "bigint", 19 | nullable: false, 20 | defaultValue: 0L); 21 | 22 | migrationBuilder.CreateIndex( 23 | name: "mcore_tags_guild_id_channel_id_name_key", 24 | table: "mcore_tags", 25 | columns: new[] { "guild_id", "channel_id", "tagname" }, 26 | unique: true); 27 | } 28 | 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropIndex( 32 | name: "mcore_tags_guild_id_channel_id_name_key", 33 | table: "mcore_tags"); 34 | 35 | migrationBuilder.DropColumn( 36 | name: "guild_id", 37 | table: "mcore_tags"); 38 | 39 | migrationBuilder.CreateIndex( 40 | name: "mcore_tags_channel_id_tag_name_key", 41 | table: "mcore_tags", 42 | columns: new[] { "channel_id", "tagname" }, 43 | unique: true); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ModCore/Migrations/20220718222104_RemoveModNotes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 3 | 4 | #nullable disable 5 | 6 | namespace ModCore.Migrations 7 | { 8 | public partial class RemoveModNotes : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.DropTable( 13 | name: "mcore_modnotes"); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.CreateTable( 19 | name: "mcore_modnotes", 20 | columns: table => new 21 | { 22 | id = table.Column(type: "integer", nullable: false) 23 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 24 | contents = table.Column(type: "text", nullable: true), 25 | guild_id = table.Column(type: "bigint", nullable: false), 26 | member_id = table.Column(type: "bigint", nullable: false) 27 | }, 28 | constraints: table => 29 | { 30 | table.PrimaryKey("PK_mcore_modnotes", x => x.id); 31 | }); 32 | 33 | migrationBuilder.CreateIndex( 34 | name: "mcore_modnotes_member_id_guild_id_key", 35 | table: "mcore_modnotes", 36 | columns: new[] { "member_id", "guild_id" }, 37 | unique: true); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ModCore/Migrations/20220719211854_RemoveUserData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 3 | 4 | #nullable disable 5 | 6 | namespace ModCore.Migrations 7 | { 8 | public partial class RemoveUserData : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.DropTable( 13 | name: "mcore_userdata"); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | migrationBuilder.CreateTable( 19 | name: "mcore_userdata", 20 | columns: table => new 21 | { 22 | id = table.Column(type: "integer", nullable: false) 23 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 24 | usr_data = table.Column(type: "jsonb", nullable: false), 25 | user_id = table.Column(type: "bigint", nullable: false) 26 | }, 27 | constraints: table => 28 | { 29 | table.PrimaryKey("PK_mcore_userdata", x => x.id); 30 | }); 31 | 32 | migrationBuilder.CreateIndex( 33 | name: "mcore_userdata_user_id_key", 34 | table: "mcore_userdata", 35 | column: "user_id", 36 | unique: true); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ModCore/ModCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net8.0 5 | false 6 | latest 7 | true 8 | true 9 | 10 | 11 | RELEASE 12 | full 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | runtime; build; native; contentfiles; analyzers; buildtransitive 40 | all 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ModCore/Modals/FeedbackModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using ModCore.Entities; 4 | using ModCore.Extensions; 5 | using ModCore.Extensions.Attributes; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading.Tasks; 9 | using ModCore.Extensions.Abstractions; 10 | using ModCore.Utils.Extensions; 11 | 12 | namespace ModCore.Modals 13 | { 14 | [Modal("feedback")] 15 | public class FeedbackModal : IModal 16 | { 17 | [ModalField("What feedback would you like to give?", "feedback", "Hello, ...", null, true, TextInputStyle.Paragraph, 10, 255)] 18 | public string Feedback { get; set; } 19 | 20 | [ModalHiddenField("c")] 21 | public string Category { get; set; } 22 | 23 | public FeedbackType Type => Enum.TryParse(typeof(FeedbackType), Category, out object result) ? (FeedbackType)result : FeedbackType.Other; 24 | 25 | private DiscordClient client; 26 | private Settings settings; 27 | 28 | public FeedbackModal(DiscordClient client, Settings settings) 29 | { 30 | this.client = client; 31 | this.settings = settings; 32 | } 33 | 34 | public async Task HandleAsync(DiscordInteraction interaction) 35 | { 36 | var feedbackChannel = await client.GetChannelAsync(settings.ContactChannelId); 37 | 38 | var embed = new DiscordEmbedBuilder() 39 | .WithAuthor($"{interaction.User.GetDisplayUsername()} in {interaction.Guild.Name}", 40 | iconUrl: interaction.User.GetAvatarUrl(ImageFormat.Png)) 41 | .WithTitle(Category) 42 | .WithDescription(Feedback) 43 | .WithColor(GetColor()); 44 | 45 | var button = ExtensionStatics.GenerateIdString("fb", new Dictionary() 46 | { 47 | {"u", interaction.User.Id.ToString() }, 48 | {"g", interaction.Guild.Id.ToString() } 49 | }); 50 | 51 | var message = new DiscordMessageBuilder() 52 | .WithEmbed(embed) 53 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Success, button, 54 | "Respond to feedback with DM", emoji: new DiscordComponentEmoji(DiscordEmoji.FromUnicode("💬")))); 55 | 56 | await feedbackChannel.SendMessageAsync(message); 57 | 58 | await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 59 | .WithContent($"✨ Your feedback has been submitted to the ModCore team! Thank you for your feedback! 💖").AsEphemeral()); 60 | } 61 | 62 | private DiscordColor GetColor() => Type switch 63 | { 64 | FeedbackType.FeatureRequest => DiscordColor.Turquoise, 65 | FeedbackType.Bug => DiscordColor.Red, 66 | FeedbackType.GeneralFeedback => DiscordColor.Yellow, 67 | FeedbackType.Complaint => DiscordColor.DarkRed, 68 | _ => DiscordColor.Gray, 69 | }; 70 | } 71 | 72 | public enum FeedbackType 73 | { 74 | GeneralFeedback, 75 | Bug, 76 | FeatureRequest, 77 | Complaint, 78 | Other 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ModCore/Modals/FeedbackResponseModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using ModCore.Extensions.Attributes; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using ModCore.Extensions.Abstractions; 8 | using ModCore.Utils.Extensions; 9 | 10 | namespace ModCore.Modals 11 | { 12 | [Modal("fbr")] 13 | public class FeedbackResponseModal : IModal 14 | { 15 | [ModalHiddenField("u")] 16 | public string UserId { get; set; } 17 | [ModalHiddenField("g")] 18 | public string GuildId { get; set; } 19 | 20 | [ModalField("Response to feedback?", "response", style: TextInputStyle.Paragraph, required: true)] 21 | public string Response { get; set; } 22 | 23 | private DiscordClient client { get; set; } 24 | 25 | public FeedbackResponseModal(DiscordClient client) 26 | { 27 | this.client = client; 28 | } 29 | 30 | public async Task HandleAsync(DiscordInteraction interaction) 31 | { 32 | if (client.CurrentApplication.Owners.Any(x => x.Id == interaction.User.Id) 33 | && ulong.TryParse(UserId, out var id) && ulong.TryParse(GuildId, out var gid)) 34 | { 35 | var resp = new DiscordInteractionResponseBuilder().AsEphemeral(); 36 | try 37 | { 38 | var guild = await client.GetGuildAsync(gid); 39 | var member = await guild.GetMemberAsync(id); 40 | await member.SendMessageAsync(new DiscordEmbedBuilder() 41 | .WithAuthor($"{interaction.User.GetDisplayUsername()}", iconUrl: interaction.User.GetAvatarUrl(ImageFormat.Png)) 42 | .WithTitle("Response to your feedback!") 43 | .WithDescription(Response) 44 | .WithFooter("Thank you for using ModCore!") 45 | .WithColor(new DiscordColor("#089FDF"))); 46 | resp.WithContent("✅ Successfully sent response to feedback!"); 47 | } 48 | catch (Exception) 49 | { 50 | resp.WithContent("⚠️ Failed to DM member. Do they use ModCore still? Are their DMs disabled? We may never know."); 51 | } 52 | 53 | await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, resp); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ModCore/Modals/MassBanModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using ModCore.Extensions.Attributes; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using ModCore.Extensions.Abstractions; 8 | 9 | namespace ModCore.Modals 10 | { 11 | [Modal("massban")] 12 | public class MassBanModal : IModal 13 | { 14 | [ModalField("Member IDs, separated by comma", "members", placeholder: "Enter IDs here...", style: TextInputStyle.Paragraph)] 15 | public string Ids { get; set; } 16 | 17 | [ModalField("Reason to ban?", "reason", placeholder: "Reason...", style: TextInputStyle.Paragraph)] 18 | public string Reason { get; set; } 19 | 20 | public async Task HandleAsync(DiscordInteraction interaction) 21 | { 22 | var member = await interaction.Guild.GetMemberAsync(interaction.User.Id); 23 | 24 | if(!member.Permissions.HasFlag(Permissions.BanMembers)) 25 | { 26 | await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 27 | .WithContent("⚠️ You shouldn't be able to use this modal, you do not have the right permissions!")); 28 | return; 29 | } 30 | 31 | if(string.IsNullOrWhiteSpace(Ids)) 32 | { 33 | await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 34 | .WithContent("⚠️ No IDs given!")); 35 | return; 36 | } 37 | 38 | var splitIds = Ids.Replace(" ", "").Split(','); 39 | 40 | var banned = new List(); 41 | var failed = new List(); 42 | foreach(var id in splitIds) 43 | { 44 | if(ulong.TryParse(id, out ulong parsed)) 45 | { 46 | try 47 | { 48 | await interaction.Guild.BanMemberAsync(parsed, 7, string.IsNullOrWhiteSpace(Reason) ? null : Reason); 49 | banned.Add(parsed); 50 | continue; 51 | } 52 | catch (Exception) { } 53 | } 54 | 55 | failed.Add(id); 56 | } 57 | 58 | var response = $"🚓 Banned {banned.Count} users."; 59 | if (failed.Count > 0) 60 | response += $"\nFailed to ban the following IDs: {string.Join(", ", failed)}"; 61 | 62 | await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 63 | .WithContent(response).AsEphemeral()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ModCore/Modals/OverrideTagModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using ModCore.Commands; 4 | using ModCore.Database; 5 | using ModCore.Database.DatabaseEntities; 6 | using ModCore.Extensions.Abstractions; 7 | using ModCore.Extensions.Attributes; 8 | using System; 9 | using System.Threading.Tasks; 10 | 11 | namespace ModCore.Modals 12 | { 13 | [Modal("override_tag")] 14 | public class OverrideTagModal : IModal 15 | { 16 | [ModalField("Tag Content (Markdown Permitted)", "content", "https://www.youtube.com/watch?v=dQw4w9WgXcQ", null, true, TextInputStyle.Paragraph, 1, 4000)] 17 | public string Content { get; set; } 18 | 19 | [ModalHiddenField("n")] 20 | public string TagName { get; set; } 21 | 22 | private DatabaseContextBuilder Database; 23 | 24 | public OverrideTagModal(DatabaseContextBuilder database) 25 | { 26 | this.Database = database; 27 | } 28 | 29 | public async Task HandleAsync(DiscordInteraction interaction) 30 | { 31 | await interaction.DeferAsync(true); 32 | var member = await interaction.Guild.GetMemberAsync(interaction.User.Id); 33 | 34 | bool isNew = false; 35 | if (!Tags.tryGetTag(TagName, interaction.Channel, this.Database, out var tag) 36 | || tag.ChannelId < 1) 37 | { 38 | tag = new DatabaseTag() 39 | { 40 | ChannelId = (long)interaction.Channel.Id, 41 | GuildId = (long)interaction.Guild.Id, 42 | Contents = Content, 43 | CreatedAt = DateTime.Now, 44 | Name = TagName, 45 | OwnerId = (long)interaction.User.Id 46 | }; 47 | isNew = true; 48 | } 49 | else if (!Tags.canManageTag(tag, interaction.Channel, member)) 50 | { 51 | await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder() 52 | .WithContent($"⚠️ The tag {TagName} already exists and you can not manage it!")); 53 | return; 54 | } 55 | 56 | await using var db = this.Database.CreateContext(); 57 | tag.Contents = Content; 58 | if (isNew) 59 | { 60 | db.Tags.Add(tag); 61 | await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder() 62 | .WithContent($"✅ Channel override tag `{TagName}` succesfully created")); 63 | } 64 | else 65 | { 66 | db.Tags.Update(tag); 67 | await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder() 68 | .WithContent($"✅ Channel override tag `{TagName}` succesfully modified!")); 69 | } 70 | await db.SaveChangesAsync(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ModCore/Modals/PostRoleMenuModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.CommandsNext; 3 | using DSharpPlus.Entities; 4 | using ModCore.Database; 5 | using ModCore.Entities; 6 | using ModCore.Extensions; 7 | using ModCore.Extensions.Abstractions; 8 | using ModCore.Extensions.Attributes; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Modals 14 | { 15 | [Modal("postrm")] 16 | public class PostRoleMenuModal : IModal 17 | { 18 | [ModalField("What message should we attach?", "txt", "Open Role Menu!", null, true, TextInputStyle.Short, 0, 255)] 19 | public string Text { get; set; } 20 | 21 | [ModalHiddenField("n")] 22 | public string Name { get; set; } 23 | 24 | private DiscordClient client; 25 | 26 | public PostRoleMenuModal(DiscordClient client, Settings settings) 27 | { 28 | this.client = client; 29 | } 30 | 31 | public async Task HandleAsync(DiscordInteraction interaction) 32 | { 33 | var member = await interaction.Guild.GetMemberAsync(interaction.User.Id); 34 | if (!member.Permissions.HasPermission(Permissions.ManageGuild)) 35 | { 36 | return; 37 | } 38 | 39 | await using var db = ((DatabaseContextBuilder)client.GetCommandsNext().Services.GetService(typeof(DatabaseContextBuilder))).CreateContext(); 40 | var guildConfig = db.GuildConfig.FirstOrDefault(x => x.GuildId == (long)interaction.GuildId); 41 | var settings = guildConfig?.GetSettings(); 42 | if (settings == null) 43 | { 44 | await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"⛔ No guild config??? contact devs!!1") 45 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "rm", "Back to Role Menu config", emoji: new DiscordComponentEmoji("🏃")))); 46 | return; 47 | } 48 | 49 | if (settings.RoleMenus.All(x => x.Name != Name)) 50 | { 51 | await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"⛔ A role menu with that name doesn't exist!") 52 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "rm", "Back to Role Menu config", emoji: new DiscordComponentEmoji("🏃")))); 53 | return; 54 | } 55 | 56 | var menu = settings.RoleMenus.First(x => x.Name == Name); 57 | 58 | var customId = ExtensionStatics.GenerateIdString("rolemenu", new Dictionary() 59 | { 60 | { "n", Name } 61 | }); 62 | 63 | var options = new List(); 64 | var roles = interaction.Guild.Roles.Values.Where(x => menu.RoleIds.Contains(x.Id)); 65 | foreach(var role in roles) 66 | { 67 | options.Add(new DiscordSelectComponentOption(role.Name, role.Id.ToString())); 68 | } 69 | 70 | await interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent($"✅")); 71 | 72 | await interaction.Channel.SendMessageAsync(new DiscordMessageBuilder().WithContent(string.IsNullOrEmpty(Text)? "Open Role Menu" : Text) 73 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Primary, customId, "Select roles..."))); 74 | 75 | await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"✅ Role menu was posted in this channel!") 76 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "rm", "Back to Role Menu config", emoji: new DiscordComponentEmoji("🏃")))); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ModCore/Modals/RoleMenuNameModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.CommandsNext; 3 | using DSharpPlus.Entities; 4 | using ModCore.Database; 5 | using ModCore.Entities; 6 | using ModCore.Extensions; 7 | using ModCore.Extensions.Abstractions; 8 | using ModCore.Extensions.Attributes; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | 13 | namespace ModCore.Modals 14 | { 15 | [Modal("rolemenuname")] 16 | public class RoleMenuNameModal : IModal 17 | { 18 | [ModalField("What name should this new role menu get?", "name", "mymenu", null, true, TextInputStyle.Short, 3, 25)] 19 | public string Name { get; set; } 20 | 21 | private DiscordClient client; 22 | 23 | public RoleMenuNameModal(DiscordClient client, Settings settings) 24 | { 25 | this.client = client; 26 | } 27 | 28 | public async Task HandleAsync(DiscordInteraction interaction) 29 | { 30 | var name = Name.Replace("`", "").Replace("\\", ""); 31 | var member = await interaction.Guild.GetMemberAsync(interaction.User.Id); 32 | if (!member.Permissions.HasPermission(Permissions.ManageGuild)) 33 | { 34 | return; 35 | } 36 | 37 | await using var db = ((DatabaseContextBuilder)client.GetCommandsNext().Services.GetService(typeof(DatabaseContextBuilder))).CreateContext(); 38 | var guildConfig = db.GuildConfig.FirstOrDefault(x => x.GuildId == (long)interaction.GuildId); 39 | var settings = guildConfig?.GetSettings(); 40 | if (settings == null) 41 | { 42 | await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"⛔ No guild config??? contact devs!!1") 43 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "rm", "Back to Role Menu config", emoji: new DiscordComponentEmoji("🏃")))); 44 | return; 45 | } 46 | 47 | if(settings.RoleMenus.Any(x => x.Name == name)) 48 | { 49 | await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"⛔ A role menu with that name already exists!") 50 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "rm", "Back to Role Menu config", emoji: new DiscordComponentEmoji("🏃")))); 51 | return; 52 | } 53 | 54 | var customId = ExtensionStatics.GenerateIdString("rm.setroles", new Dictionary() 55 | { 56 | { "n", name } 57 | }); 58 | 59 | await interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder() 60 | .WithContent($"📝 Pick the roles for this new menu...") 61 | .AddComponents(new DiscordRoleSelectComponent(customId, "Select roles...", maxOptions: 25))); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ModCore/Modals/SetTagModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using ModCore.Commands; 4 | using ModCore.Database; 5 | using ModCore.Database.DatabaseEntities; 6 | using ModCore.Extensions.Abstractions; 7 | using ModCore.Extensions.Attributes; 8 | using System; 9 | using System.Threading.Tasks; 10 | 11 | namespace ModCore.Modals 12 | { 13 | [Modal("set_tag")] 14 | public class SetTagModal : IModal 15 | { 16 | [ModalField("Tag Content (Markdown Permitted)", "content", "https://www.youtube.com/watch?v=dQw4w9WgXcQ", null, true, TextInputStyle.Paragraph, 1, 4000)] 17 | public string Content { get; set; } 18 | 19 | [ModalHiddenField("n")] 20 | public string TagName { get; set; } 21 | 22 | private DatabaseContextBuilder Database; 23 | 24 | public SetTagModal(DatabaseContextBuilder database) 25 | { 26 | this.Database = database; 27 | } 28 | 29 | public async Task HandleAsync(DiscordInteraction interaction) 30 | { 31 | await interaction.DeferAsync(true); 32 | 33 | bool isNew = false; 34 | var member = await interaction.Guild.GetMemberAsync(interaction.User.Id); 35 | 36 | if (Tags.tryGetTag(TagName, interaction.Channel, Database, out var tag)) 37 | { 38 | if (!Tags.canManageTag(tag, interaction.Channel, member)) 39 | { 40 | await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder() 41 | .WithContent($"⚠️ The tag {TagName} already exists and you can not manage it!")); 42 | return; 43 | } 44 | } 45 | else 46 | { 47 | tag = new DatabaseTag 48 | { 49 | ChannelId = -1, // guild tag 50 | Name = TagName, 51 | GuildId = (long)interaction.Guild.Id, 52 | OwnerId = (long)interaction.User.Id, 53 | CreatedAt = DateTime.Now 54 | }; 55 | isNew = true; 56 | } 57 | 58 | await using var db = this.Database.CreateContext(); 59 | tag.Contents = Content; 60 | if (isNew) 61 | { 62 | db.Tags.Add(tag); 63 | await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder() 64 | .WithContent($"✅ Server-global tag `{TagName}` succesfully created!")); 65 | } 66 | else 67 | { 68 | db.Tags.Update(tag); 69 | await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder() 70 | .WithContent($"✅ Server-global tag `{TagName}` succesfully modified!")); 71 | } 72 | await db.SaveChangesAsync(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ModCore/Modals/SnoozeModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.Entities; 3 | using ModCore.Database; 4 | using ModCore.Database.DatabaseEntities; 5 | using ModCore.Database.JsonEntities; 6 | using ModCore.Entities; 7 | using ModCore.Extensions.Abstractions; 8 | using ModCore.Extensions.Attributes; 9 | using ModCore.Listeners; 10 | using ModCore.Utils; 11 | using System; 12 | using System.Linq; 13 | using System.Text.RegularExpressions; 14 | using System.Threading.Tasks; 15 | 16 | namespace ModCore.Modals 17 | { 18 | [Modal("snz")] 19 | public class SnoozeModal : IModal 20 | { 21 | [ModalField("Snooze for how long?", "timespan", "Default: 15 minutes")] 22 | public string Timespan { get; set; } 23 | 24 | [ModalHiddenField("msg")] 25 | public string MessageId { get; set; } 26 | 27 | private static Regex snoozeRegex = new Regex(@"", RegexOptions.Compiled); 28 | 29 | private DatabaseContextBuilder _db; 30 | private DiscordClient client; 31 | private SharedData shared; 32 | 33 | public SnoozeModal(DatabaseContextBuilder db, DiscordClient client, SharedData shared) 34 | { 35 | _db = db; 36 | this.client = client; 37 | this.shared = shared; 38 | } 39 | 40 | public async Task HandleAsync(DiscordInteraction interaction) 41 | { 42 | if (ulong.TryParse(MessageId, out var messageId)) 43 | { 44 | var message = await interaction.Channel.GetMessageAsync(messageId); 45 | var text = message.Embeds[0].Fields[0].Value; 46 | var duration = TimeSpan.FromMinutes(15); 47 | 48 | if (!string.IsNullOrEmpty(Timespan)) 49 | { 50 | var (realDuration, _) = Dates.ParseTime(Timespan); 51 | duration = realDuration; 52 | } 53 | 54 | if (duration > TimeSpan.FromDays(365)) // 1 year is the maximum 55 | { 56 | await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 57 | { 58 | Content = "⚠️ Maximum allowed snooze time span is 1 year.", 59 | IsEphemeral = true 60 | }); 61 | return; 62 | } 63 | 64 | var now = DateTimeOffset.UtcNow; 65 | var dispatchAt = now + duration; 66 | 67 | // create a new timer 68 | var reminder = new DatabaseTimer 69 | { 70 | GuildId = (long)interaction.Channel.Guild.Id, 71 | ChannelId = (long)interaction.Channel.Id, 72 | UserId = (long)interaction.User.Id, 73 | DispatchAt = dispatchAt.LocalDateTime, 74 | ActionType = TimerActionType.Reminder 75 | }; 76 | 77 | var snoozeContext = ""; 78 | if(message.Components.Count > 0 && message.Components.First().Components.Count == 2) 79 | { 80 | var links = message.Components.First().Components.Where(x => x.GetType().IsAssignableTo(typeof(DiscordLinkButtonComponent))).Cast(); 81 | if(links.Any()) 82 | { 83 | snoozeContext = links.First().Url; 84 | } 85 | } 86 | 87 | reminder.SetData(new TimerReminderData 88 | { 89 | ReminderText = text, 90 | MessageId = messageId, 91 | Snoozed = true, 92 | SnoozedContext = snoozeContext 93 | }); 94 | 95 | await using (var db = this._db.CreateContext()) 96 | { 97 | db.Timers.Add(reminder); 98 | await db.SaveChangesAsync(); 99 | } 100 | 101 | // reschedule timers 102 | await Timers.ScheduleNextAsync(); 103 | 104 | await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() 105 | { 106 | Content = $"⏰ Ok, snoozed to remind you about the following:\n\n{text}", 107 | IsEphemeral = true 108 | }); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ModCore/Modals/WelcomeMessageModal.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus; 2 | using DSharpPlus.CommandsNext; 3 | using DSharpPlus.Entities; 4 | using ModCore.Database; 5 | using ModCore.Entities; 6 | using ModCore.Extensions.Attributes; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using ModCore.Extensions.Abstractions; 10 | 11 | namespace ModCore.Modals 12 | { 13 | [Modal("welcome")] 14 | public class WelcomeMessageModal : IModal 15 | { 16 | [ModalField("New welcome message?", "welcome", "Welcomer supports replacement tags. These can be found behind a link button in the welcomer menu.", 17 | "", true, TextInputStyle.Paragraph, 10, 255)] 18 | public string Welcome { get; set; } 19 | 20 | private DiscordClient client; 21 | 22 | public WelcomeMessageModal(DiscordClient client, Settings settings) 23 | { 24 | this.client = client; 25 | } 26 | 27 | public async Task HandleAsync(DiscordInteraction interaction) 28 | { 29 | var member = await interaction.Guild.GetMemberAsync(interaction.User.Id); 30 | if(!member.Permissions.HasPermission(Permissions.ManageGuild)) 31 | { 32 | return; 33 | } 34 | 35 | await using var db = ((DatabaseContextBuilder)client.GetCommandsNext().Services.GetService(typeof(DatabaseContextBuilder))).CreateContext(); 36 | var guildConfig = db.GuildConfig.FirstOrDefault(x => x.GuildId == (long)interaction.GuildId); 37 | var settings = guildConfig?.GetSettings(); 38 | if (settings == null) 39 | { 40 | await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"⛔ No guild config??? contact devs!!1") 41 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "wc", "Back to Welcomer config", emoji: new DiscordComponentEmoji("🏃")))); 42 | return; 43 | } 44 | 45 | settings.Welcome.Message = Welcome; 46 | 47 | guildConfig.SetSettings(settings); 48 | db.GuildConfig.Update(guildConfig); 49 | await db.SaveChangesAsync(); 50 | 51 | await interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent($"✅ Welcome message was configured!")); 52 | 53 | await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent($"✅ Welcome message was configured!") 54 | .AddComponents(new DiscordButtonComponent(ButtonStyle.Secondary, "wc", "Back to Welcomer config", emoji: new DiscordComponentEmoji("🏃")))); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /ModCore/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ModCore 4 | { 5 | internal static class Program 6 | { 7 | private static Task Main(string[] args) => (ModCore = new ModCore()).InitializeAsync(args); 8 | 9 | public static ModCore ModCore { get; private set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ModCore/Utils/AugmentedBoolConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using DSharpPlus.CommandsNext; 3 | using DSharpPlus.CommandsNext.Converters; 4 | using DSharpPlus.Entities; 5 | 6 | namespace ModCore.Utils 7 | { 8 | public class AugmentedBoolConverter : IArgumentConverter 9 | { 10 | public Task> ConvertAsync(string value, CommandContext ctx) 11 | { 12 | return Task.FromResult(TryConvert(value, ctx, out var b) ? new Optional(b) : new Optional()); 13 | } 14 | 15 | public static bool TryConvert(string value, CommandContext ctx, out bool result) 16 | { 17 | switch (value.ToLower()) 18 | { 19 | case "y": 20 | case "ye": 21 | case "ya": 22 | case "yup": 23 | case "yee": 24 | case "davaj": 25 | case "yes": 26 | case "1": 27 | case "on": 28 | case "enable": 29 | case "да": 30 | result = true; 31 | return true; 32 | 33 | case "n": 34 | case "nah": 35 | case "nope": 36 | case "nyet": 37 | case "nada": 38 | case "no": 39 | case "0": 40 | case "off": 41 | case "disable": 42 | case "нет": 43 | result = false; 44 | return true; 45 | 46 | default: 47 | return bool.TryParse(value, out result); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ModCore/Utils/ConfigValueSerialization.cs: -------------------------------------------------------------------------------- 1 | using ModCore.Database.JsonEntities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | 7 | namespace ModCore.Utils 8 | { 9 | public static class ConfigValueSerialization 10 | { 11 | public static string GetConfigPropertyPath(Expression> expr) 12 | { 13 | var expression = expr.Body.ToString(); 14 | var splitIndex = expression.IndexOf('.'); 15 | return expression.Substring(splitIndex + 1); 16 | } 17 | 18 | public static void SetConfigValue(GuildSettings settings, string path, string value) 19 | { 20 | setValueRecursive(settings, path.Split('.').ToList(), value); 21 | } 22 | 23 | private static void setValueRecursive(object obj, IEnumerable path, string value) 24 | { 25 | var type = obj.GetType(); 26 | var prop = type.GetProperty(path.First()); 27 | 28 | // last property, try to assign 29 | if(path.Count() == 1) 30 | { 31 | var converted = convertToMatchingType(prop.PropertyType, value); 32 | prop.SetValue(obj, converted); 33 | return; 34 | } 35 | 36 | // not last property, we go down another 37 | var next = prop.GetValue(obj); 38 | setValueRecursive(next, path.Skip(1), value); 39 | } 40 | 41 | private static object convertToMatchingType(Type type, string value) 42 | { 43 | if (type == typeof(int)) 44 | return int.Parse(value); 45 | else if (type == typeof(bool)) 46 | return bool.Parse(value); 47 | else if (type == typeof(long)) 48 | return long.Parse(value); 49 | else if (type == typeof(ulong)) 50 | return ulong.Parse(value); 51 | else if (type == typeof(DateTimeOffset)) 52 | return DateTimeOffset.Parse(value); 53 | else if (type == typeof(string)) 54 | return value; 55 | 56 | throw new InvalidCastException($"No conversion defined for type {type}"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ModCore/Utils/CustomDiscordMessageConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using DSharpPlus.CommandsNext; 4 | using DSharpPlus.CommandsNext.Converters; 5 | using DSharpPlus.Entities; 6 | 7 | namespace ModCore.Utils 8 | { 9 | public class CustomDiscordMessageConverter : IArgumentConverter 10 | { 11 | public async Task> ConvertAsync(string value, CommandContext ctx) 12 | { 13 | if (value.StartsWith('^')) 14 | { 15 | var counttxt = value.Substring(1); 16 | 17 | if (!int.TryParse(counttxt, out int count)) 18 | count = 1; 19 | 20 | if (count > 100) 21 | return default; 22 | 23 | var msgs = await ctx.Channel.GetMessagesBeforeAsync(ctx.Message.Id, count); 24 | return msgs[^1]; 25 | } 26 | else 27 | { 28 | var conv = new DiscordMessageConverter() as IArgumentConverter; 29 | return await conv.ConvertAsync(value, ctx); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ModCore/Utils/DateLexer.cs: -------------------------------------------------------------------------------- 1 | namespace ModCore.Utils 2 | { 3 | public class DateLexer 4 | { 5 | public static bool IsFillerWord(string s) => s == "me" || s == "in" || s == "at"; 6 | 7 | public static bool IsFinishingWord(string s2) => s2 == "to"; 8 | 9 | public static bool TryIsNumber(string s, out uint i) 10 | { 11 | switch (s) 12 | { 13 | case "a": 14 | case "an": 15 | case "one": 16 | i = 1; 17 | return true; 18 | case "two": 19 | i = 2; 20 | return true; 21 | case "three": 22 | i = 3; 23 | return true; 24 | case "four": 25 | i = 4; 26 | return true; 27 | case "five": 28 | i = 5; 29 | return true; 30 | case "six": 31 | i = 6; 32 | return true; 33 | case "seven": 34 | i = 7; 35 | return true; 36 | case "eight": 37 | i = 8; 38 | return true; 39 | case "nine": 40 | i = 9; 41 | return true; 42 | case "ten": 43 | i = 10; 44 | return true; 45 | case "eleven": 46 | i = 11; 47 | return true; 48 | case "twelve": 49 | i = 12; 50 | return true; 51 | default: 52 | return uint.TryParse(s, out i); 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/AttributeImpl/AlternateKeyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace ModCore.Utils.EntityFramework.AttributeImpl 5 | { 6 | /// 7 | /// 8 | /// Represents an attribute that is placed on a property to indicate that the property is an alternate key in the 9 | /// model. This will make it unique and read-only. 10 | /// 11 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] 12 | public class AlternateKeyAttribute : EfPropertyProcessorBaseAttribute 13 | { 14 | private readonly string _name; 15 | 16 | /// 17 | /// Initializes a new AlternateKeyAttribute instance, with an optional name for the key. 18 | /// 19 | public AlternateKeyAttribute(string name = null) 20 | { 21 | _name = name; 22 | } 23 | 24 | public override void Process(EfProcessorContext ctx, EfPropertyDefinition definition) 25 | { 26 | var k = ctx.Entity.HasAlternateKey(definition.Property.Name); 27 | if (_name != null) k.HasName(_name); // TODO i dont think this matters 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/AttributeImpl/AnnotationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace ModCore.Utils.EntityFramework.AttributeImpl 5 | { 6 | /// 7 | /// 8 | /// Represents an attribute that is placed on a property to append an annotation to it, as with 9 | /// . 10 | /// 11 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] 12 | public class AnnotationAttribute : EfPropertyProcessorBaseAttribute 13 | { 14 | private readonly string _annotation; 15 | private readonly object _value; 16 | 17 | /// 18 | /// Initializes a new AnnotationAttribute instance, with an annotation and a value. 19 | /// 20 | public AnnotationAttribute(string annotation, object value) 21 | { 22 | _annotation = annotation; 23 | _value = value; 24 | } 25 | 26 | public override void Process(EfProcessorContext ctx, EfPropertyDefinition definition) 27 | { 28 | ctx.Entity.Property(definition.Property.Name) 29 | .HasAnnotation(_annotation, _value); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/AttributeImpl/IgnoreIfProviderNotAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using ModCore.Entities; 4 | 5 | namespace ModCore.Utils.EntityFramework.AttributeImpl 6 | { 7 | /// 8 | /// 9 | /// Represents an attribute that is placed on a property to indicate that the database column is to be omitted if 10 | /// the database provider does not match the given one. 11 | /// 12 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] 13 | public class IgnoreIfProviderNotAttribute : EfPropertyProcessorBaseAttribute 14 | { 15 | private readonly DatabaseProvider[] _providers; 16 | 17 | /// 18 | /// Initializes a new IgnoreIfProviderNotAttribute instance for one or more providers to match with. 19 | /// 20 | public IgnoreIfProviderNotAttribute(params DatabaseProvider[] providers) 21 | { 22 | _providers = providers; 23 | } 24 | 25 | public override void Process(EfProcessorContext ctx, EfPropertyDefinition definition) 26 | { 27 | if (_providers.All(e => e != ctx.DatabaseContext.Provider)) 28 | ctx.Entity.Ignore(definition.Property.Name); // TODO is this correct or do we use the column name here 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/EfAttributeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ModCore.Utils.EntityFramework 4 | { 5 | public sealed class EfAttributeException : Exception 6 | { 7 | public EfAttributeException(string message) : base(message) 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/EfIndirectProcessorBaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ModCore.Utils.EntityFramework 5 | { 6 | /// 7 | /// Represents a type processor that is applied as an attribute to one or more properties. Properties where 8 | /// returns true will be aggregated, and then one instance of the attribute will have 9 | /// called on it with the aggregated properties. If there are no matches, the method will not 10 | /// be called. 11 | /// 12 | /// 13 | ///

There is no guarantee as to which instance of the attribute will be used for processing. Implementations of 14 | /// this class can not be compared by their state, and should be sealed.

15 | ///

The choice of having the same attribute type handling both properties and containing types is to avoid 16 | /// needing to use reflection.

17 | ///
18 | [AttributeUsage(AttributeTargets.Property)] 19 | public abstract class EfIndirectProcessorBaseAttribute : EfPropertyBaseAttribute 20 | { 21 | // all implementations of this class must support multiple annotations per type, otherwise using it is pointless 22 | public sealed override bool CanHaveMultiple => true; 23 | 24 | public abstract bool PropertyMatches(EfProcessorContext ctx, EfPropertyDefinition definition); 25 | public abstract void Process(EfProcessorContext ctx, IEnumerable properties); 26 | 27 | // only compare equality and hash code based on the own type and prevent overriding it 28 | // this makes it possible to index attributes as keys in a dictionary 29 | public sealed override bool Equals(object obj) => obj?.GetType() == GetType(); 30 | public sealed override int GetHashCode() => GetType().GetHashCode(); 31 | } 32 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/EfProcessorContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace ModCore.Utils.EntityFramework 7 | { 8 | public sealed class EfProcessorContext 9 | { 10 | public ModelBuilder Model { get; internal set; } 11 | public EntityTypeBuilder Entity { get; internal set; } 12 | public IMutableEntityType EntityType { get; internal set; } 13 | public IEfCustomContext DatabaseContext { get; internal set; } 14 | 15 | public Type ClrType => EntityType.ClrType; 16 | } 17 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/EfPropertyBaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ModCore.Utils.EntityFramework 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | public abstract class EfPropertyBaseAttribute : Attribute 7 | { 8 | /// 9 | /// If set to false in an implementation of this attribute, will throw an exception when encountering more than 10 | /// one of the attribute on a type. 11 | /// 12 | public virtual bool CanHaveMultiple => true; 13 | } 14 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/EfPropertyDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace ModCore.Utils.EntityFramework 5 | { 6 | public sealed class EfPropertyDefinition 7 | { 8 | public PropertyInfo Property { get; internal set; } 9 | public Attribute Source { get; internal set; } 10 | } 11 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/EfPropertyProcessorBaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ModCore.Utils.EntityFramework 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | public abstract class EfPropertyProcessorBaseAttribute : EfPropertyBaseAttribute 7 | { 8 | public abstract void Process(EfProcessorContext ctx, EfPropertyDefinition definition); 9 | } 10 | } -------------------------------------------------------------------------------- /ModCore/Utils/EntityFramework/IEfCustomContext.cs: -------------------------------------------------------------------------------- 1 | using ModCore.Entities; 2 | 3 | namespace ModCore.Utils.EntityFramework 4 | { 5 | public interface IEfCustomContext 6 | { 7 | DatabaseProvider Provider { get; } 8 | } 9 | } -------------------------------------------------------------------------------- /ModCore/Utils/Extensions/Sanitization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using DSharpPlus; 7 | using DSharpPlus.CommandsNext; 8 | using DSharpPlus.Entities; 9 | 10 | namespace ModCore.Utils.Extensions 11 | { 12 | public static class Sanitization 13 | { 14 | // make sure to replace all backslashes preceding @mention so they can't add an extra backslash to escape our escape 15 | private static readonly Regex EscapeEveryoneMention = new Regex(@"\\*@(everyone|here)"); 16 | 17 | public static Task SafeRespondUnformattedAsync(this CommandContext ctx, string s) 18 | => ctx.RespondAsync(Sanitize(s, IsPrivileged(ctx))); 19 | 20 | public static Task SafeModifyUnformattedAsync(this CommandContext ctx, DiscordMessage m, string s) 21 | => m.ModifyAsync(Sanitize(s, IsPrivileged(ctx))); 22 | 23 | public static Task SafeMessageUnformattedAsync(this DiscordChannel channel, string s, bool privileged) 24 | => channel.SendMessageAsync(Sanitize(s, privileged)); 25 | 26 | public static Task SafeMessageUnformattedAsync(this DiscordChannel channel, string s, CommandContext ctx) 27 | => channel.SendMessageAsync(Sanitize(s, IsPrivileged(ctx))); 28 | 29 | public static Task SafeRespondAsync(this CommandContext ctx, FormattableString s) 30 | => ctx.RespondAsync(SanitizeFormat(s, IsPrivileged(ctx))); 31 | 32 | public static Task SafeModifyAsync(this CommandContext ctx, DiscordMessage m, FormattableString s) 33 | => m.ModifyAsync(SanitizeFormat(s, IsPrivileged(ctx))); 34 | 35 | public static Task SafeMessageAsync(this DiscordChannel channel, FormattableString s, bool privileged) 36 | => channel.SendMessageAsync(SanitizeFormat(s, privileged)); 37 | 38 | public static Task SafeMessageAsync(this DiscordChannel channel, FormattableString s, CommandContext ctx) 39 | => channel.SendMessageAsync(SanitizeFormat(s, IsPrivileged(ctx))); 40 | 41 | public static Task ElevatedRespondAsync(this CommandContext ctx, string s) 42 | => ctx.RespondAsync(s); 43 | 44 | public static Task ElevatedRespondAsync(this CommandContext ctx, DiscordEmbed embed) 45 | => ctx.RespondAsync(embed: embed); 46 | 47 | public static Task ElevatedMessageAsync(this DiscordChannel channel, string s) 48 | => channel.SendMessageAsync(s); 49 | 50 | public static Task ElevatedMessageAsync(this DiscordChannel channel, DiscordEmbed embed) 51 | => channel.SendMessageAsync(embed: embed); 52 | 53 | public static Task ElevatedMessageAsync(this DiscordChannel channel, string s, DiscordEmbed embed) 54 | => channel.SendMessageAsync(s, embed: embed); 55 | 56 | public static Task ElevatedMessageAsync(this DiscordMember m, string s) 57 | => m.SendMessageAsync(s); 58 | 59 | public static Task ElevatedMessageAsync(this DiscordMember m, DiscordEmbed embed) 60 | => m.SendMessageAsync(embed: embed); 61 | 62 | private static string SanitizeFormat(FormattableString s, bool privileged) 63 | { 64 | if (privileged) 65 | return s.ToString(CultureInfo.InvariantCulture); 66 | var escapedParameters = s.GetArguments() 67 | .Select(e => Sanitize(ToStringInvariant(e), privileged: false)); 68 | return string.Format(s.Format, escapedParameters.ToArray()); 69 | } 70 | 71 | private static string Sanitize(string s, bool privileged) 72 | { 73 | return privileged ? s : EscapeEveryoneMention.Replace(s, @"\@$1"); 74 | } 75 | 76 | public static bool IsPrivileged(CommandContext ctx) 77 | => ctx.Member.PermissionsIn(ctx.Channel).HasPermission(Permissions.MentionEveryone); 78 | 79 | public static bool IsPrivileged(DiscordMember m, DiscordChannel chan) 80 | => m.PermissionsIn(chan).HasPermission(Permissions.MentionEveryone); 81 | 82 | private static string ToStringInvariant(object e) => string.Format(CultureInfo.InvariantCulture, "{0}", e); 83 | } 84 | } -------------------------------------------------------------------------------- /ModCore/Utils/InteractivityUtil.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using ModCore.Utils.Extensions; 3 | 4 | namespace ModCore.Utils 5 | { 6 | public static class InteractivityUtil 7 | { 8 | public static bool Confirm(DiscordMessage message) 9 | { 10 | return message.Content.EqualsIgnoreCase("yes") 11 | || message.Content.EqualsIgnoreCase("y") 12 | || message.Content.EqualsIgnoreCase("1") 13 | || message.Content.EqualsIgnoreCase("ya") 14 | || message.Content.EqualsIgnoreCase("ja") 15 | || message.Content.EqualsIgnoreCase("si") 16 | || message.Content.EqualsIgnoreCase("da"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /ModCore/Utils/MentionUlongConverter.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.CommandsNext; 2 | using DSharpPlus.CommandsNext.Converters; 3 | using DSharpPlus.Entities; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace ModCore.Utils 8 | { 9 | public class MentionUlongConverter : IArgumentConverter 10 | { 11 | public static readonly Regex MentionRegex = new(@"^<(#|@)!?([0-9]+)>$", RegexOptions.Compiled); 12 | public Task> ConvertAsync(string value, CommandContext ctx) 13 | { 14 | if (ulong.TryParse(value, out var result)) 15 | return Task.FromResult(new Optional(result)); 16 | 17 | Match match = MentionRegex.Match(value); 18 | return Task.FromResult(match.Success && ulong.TryParse(match.Groups[2].Value, out var mention) ? new Optional(mention) : default); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ModCore/Utils/ModCoreSnowflake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DSharpPlus; 3 | 4 | namespace ModCore.Utils 5 | { 6 | public abstract class ModCoreSnowflake 7 | { 8 | /// Gets the ID of this object. 9 | public ulong Id { get; protected set; } 10 | 11 | /// Gets the date and time this object was created. 12 | public DateTimeOffset CreationTimestamp => new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(this.Id >> 22); 13 | 14 | /// Gets the client instance this object is tied to. 15 | internal BaseDiscordClient Discord { get; set; } 16 | 17 | internal ModCoreSnowflake() 18 | { 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ModCore/Utils/Overwrite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using DSharpPlus; 4 | using DSharpPlus.Entities; 5 | 6 | namespace ModCore.Utils 7 | { 8 | public class Overwrite : ModCoreSnowflake 9 | { 10 | /// 11 | /// Gets the type of the overwrite. 12 | /// 13 | public OverwriteType Type { get; internal set; } 14 | 15 | /// Gets the allowed permission set. 16 | public Permissions Allow { get; internal set; } 17 | 18 | /// Gets the denied permission set. 19 | public Permissions Deny { get; internal set; } 20 | 21 | public Task MemberGetterAsync => 22 | Type == OverwriteType.Member ? _guild.GetMemberAsync(Id) : throw new ArgumentException(); 23 | 24 | public DiscordRole Role => Type == OverwriteType.Role ? _guild.GetRole(Id) : throw new ArgumentException(); 25 | 26 | private readonly DiscordGuild _guild; 27 | 28 | public Overwrite(DiscordOverwrite overwrite, DiscordGuild guild) 29 | { 30 | this.Type = overwrite.Type; 31 | this.Allow = overwrite.Allowed; 32 | this.Deny = overwrite.Denied; 33 | this.Id = overwrite.Id; 34 | _guild = guild; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /ModCore/Utils/StringTokenizer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace ModCore.Utils 7 | { 8 | /// 9 | /// String tokenizer by uwx 10 | /// 11 | public class StringTokenizer : IEnumerable, IEnumerator 12 | { 13 | // ReSharper disable once ConvertToAutoPropertyWithPrivateSetter 14 | public string Current => _current; 15 | 16 | // ReSharper disable ConvertToAutoPropertyWhenPossible 17 | internal string String => _string; 18 | internal int Index => _index; 19 | // ReSharper enable ConvertToAutoPropertyWhenPossible 20 | 21 | private string _current; 22 | private readonly string _string; 23 | private int _index; 24 | 25 | private static readonly char[] WhiteSpaceChars = Enumerable.Range(char.MinValue, char.MaxValue) 26 | .Select(e => (char)e) 27 | .Where(c => c == 32 || c >= 9 && c <= 13 || (c == 160 || c == 133)) 28 | .ToArray(); 29 | 30 | public StringTokenizer(string from) 31 | { 32 | _string = from; 33 | _current = null; 34 | _index = 0; 35 | } 36 | 37 | public IEnumerator GetEnumerator() => this; 38 | 39 | IEnumerator IEnumerable.GetEnumerator() => this; 40 | 41 | public bool MoveNext() 42 | { 43 | if (_index == -1) return false; 44 | 45 | while (char.IsWhiteSpace(_string[_index])) 46 | { 47 | _index++; 48 | if (_index >= _string.Length) return false; 49 | } 50 | int newIndex; 51 | // support quoted text 52 | if (_string[_index] == '"') 53 | { 54 | _index++;//remove initial quote 55 | newIndex = _string.IndexOf('"', _index+1); 56 | } 57 | else 58 | { 59 | newIndex = _string.IndexOfAny(WhiteSpaceChars, _index); 60 | } 61 | _current = _string.Substring(_index, (newIndex == -1 ? _string.Length : newIndex) - _index);//substring(startIndex,length) 62 | // remove closing quote 63 | if (newIndex != -1 && newIndex < _string.Length && _string[newIndex] == '"') 64 | { 65 | _index = newIndex + 1; 66 | } 67 | else 68 | { 69 | _index = newIndex; 70 | } 71 | return true; 72 | } 73 | 74 | /// 75 | /// Assigns the next element in the tokenizer to a string 76 | /// 77 | /// the string to assign 78 | /// true if successful 79 | public bool Next(out string str) 80 | { 81 | var b = MoveNext(); 82 | str = _current; 83 | return b; 84 | } 85 | 86 | public string Remaining() 87 | { 88 | var tokens = new StringBuilder(); 89 | foreach (var token in this) 90 | { 91 | tokens.Append(' ').Append(token); 92 | } 93 | 94 | return tokens.Length == 0 ? "" : tokens.ToString().Substring(1); // remove first space 95 | } 96 | 97 | public StringTokenizer Clone() 98 | { 99 | return _index == -1 100 | ? new StringTokenizer(null) 101 | { 102 | _index = -1 103 | } 104 | : new StringTokenizer(_string.Substring(_index)); 105 | } 106 | 107 | public void Reset() 108 | { 109 | _index = 0; 110 | } 111 | 112 | object IEnumerator.Current => Current; 113 | 114 | public void Dispose() 115 | { 116 | // empty 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /PostgreSQL.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naamloos/ModCore/88b3ac8788b28b81df895bdf1b94a0fe5b902a1d/PostgreSQL.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

ModCore

3 |

4 | Github Stars 5 | Github Issues 6 | .NET Build 7 |

8 | 9 | ModCore is A powerful moderating bot written on top of the DSharpPlus library. ModCore is your assistant for server moderation and management through a wide range of hand-crafted features to make your life as a moderator or administrator a breeze! 10 | 11 | [Invite ModCore now!](https://discord.com/api/oauth2/authorize?client_id=359828546719449109&permissions=8&scope=bot%20applications.commands) 12 | 13 | Support 14 | --------- 15 | Need help with ModCore? Either use the `/contact` command or join the ModCore Discord! 16 | 17 | [![ModCore Chat](https://discord.com/api/guilds/709152601978961990/embed.png?style=banner2)](https://discord.gg/MRUP5dd) 18 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | # BUILD 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build 3 | ARG BUILD_VER 4 | WORKDIR /src 5 | COPY ./ModCore ./ 6 | RUN dotnet restore 7 | RUN dotnet publish -c Release -o out /p:VersionPrefix=${BUILD_VER} 8 | 9 | # RUNNER IMAGE 10 | FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine 11 | WORKDIR /app 12 | COPY --from=build /src/out . 13 | WORKDIR /config 14 | 15 | # ADD FFMPEG FOR VOICE SUPPORT 16 | RUN apk add ffmpeg 17 | # FFMPEG is not actually used but fukit 18 | 19 | # ADD TESSERACT FOR OCR 20 | RUN apk add tesseract-ocr 21 | RUN apk add leptonica-dev 22 | 23 | RUN ln -s /usr/lib/libleptonica.so /app/x64/libleptonica-1.82.0.so 24 | RUN ln -s /usr/lib/libtesseract.so.5 /app/x64/libtesseract50.so 25 | 26 | ENTRYPOINT ["dotnet", "/app/ModCore.dll"] 27 | --------------------------------------------------------------------------------