diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..677ed3f --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "8.0.4", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index cde0129..452f49d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,13 +13,13 @@ tab_width = 4 # New line preferences end_of_line = crlf -insert_final_newline = false +insert_final_newline = true #### .NET Coding Conventions #### # Organize usings dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = false +dotnet_sort_system_directives_first = true file_header_template = unset # this. and Me. preferences @@ -63,7 +63,7 @@ dotnet_style_prefer_simplified_interpolation = true dotnet_style_readonly_field = true # Parameter preferences -dotnet_code_quality_unused_parameters = all +dotnet_code_quality_unused_parameters = true # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = none @@ -75,59 +75,58 @@ dotnet_style_allow_statement_immediately_after_block_experimental = true #### C# Coding Conventions #### # var preferences -csharp_style_var_elsewhere = false -csharp_style_var_for_built_in_types = false -csharp_style_var_when_type_is_apparent = false +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion # Expression-bodied members -csharp_style_expression_bodied_accessors = true -csharp_style_expression_bodied_constructors = true -csharp_style_expression_bodied_indexers = true -csharp_style_expression_bodied_lambdas = true -csharp_style_expression_bodied_local_functions = true -csharp_style_expression_bodied_methods = true -csharp_style_expression_bodied_operators = true -csharp_style_expression_bodied_properties = true +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent # Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_not_pattern = true +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion csharp_style_prefer_pattern_matching = true:suggestion -csharp_style_prefer_switch_expression = true +csharp_style_prefer_switch_expression = true:suggestion # Null-checking preferences -csharp_style_conditional_delegate_call = true +csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences -csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async +csharp_prefer_static_local_function = true:suggestion # Code-block preferences -csharp_prefer_braces = when_multiline -csharp_prefer_simple_using_statement = true -csharp_style_namespace_declarations = file_scoped +csharp_prefer_braces = when_multiline:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion # Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion csharp_style_pattern_local_over_anonymous_function = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_null_check_over_type_check = true -csharp_style_prefer_range_operator = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent # 'using' directive preferences -csharp_using_directive_placement = outside_namespace +csharp_using_directive_placement = outside_namespace:silent # New line preferences -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false -csharp_style_allow_embedded_statements_on_same_line_experimental = true +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent #### C# Formatting Rules #### @@ -217,3 +216,8 @@ dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion + +csharp_style_prefer_primary_constructors = true:suggestion \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 8669443..9241ea7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,8 +10,8 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net6.0/WorldTime.dll", - "args": [ "-c", "${workspaceFolder}/bin/Debug/net6.0/settings.json" ], + "program": "${workspaceFolder}/bin/Debug/net8.0/WorldTime.dll", + "args": [ "-c", "${workspaceFolder}/bin/Debug/settings.json" ], "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", diff --git a/Commands/CommandsBase.cs b/Commands/CommandsBase.cs index a80b3e9..7511bf8 100644 --- a/Commands/CommandsBase.cs +++ b/Commands/CommandsBase.cs @@ -7,10 +7,9 @@ using WorldTime.Data; namespace WorldTime.Commands; public class CommandsBase : InteractionModuleBase { - protected const string ErrInvalidZone = ":x: Not a valid zone name." - + " To find your time zone, refer to: ."; - protected const string ErrNoUserCache = ":warning: Please try the command again."; - protected const string ErrNotAllowed = ":x: Only server moderators may use this command."; + protected const string ErrInvalidZone = + ":x: Not a valid zone name. To find your zone, you may refer to a site such as ."; + protected const string ErrNoUserCache = ":warning: Oops, bot wasn't ready. Please try again in a moment."; private static readonly ReadOnlyDictionary _tzNameMap; diff --git a/Commands/ConfigCommands.cs b/Commands/ConfigCommands.cs index 8b4dcb9..ccc1725 100644 --- a/Commands/ConfigCommands.cs +++ b/Commands/ConfigCommands.cs @@ -3,7 +3,7 @@ using Discord.Interactions; namespace WorldTime.Commands; [Group("config", "Configuration commands for World Time.")] [DefaultMemberPermissions(GuildPermission.ManageGuild)] -[EnabledInDm(false)] +[CommandContextType(InteractionContextType.Guild)] public class ConfigCommands : CommandsBase { internal const string HelpUse12 = "Sets whether to use the 12-hour (AM/PM) format in time zone listings."; internal const string HelpSetFor = "Sets/updates time zone for a given user."; diff --git a/Commands/UserCommands.cs b/Commands/UserCommands.cs index d967000..cdac27c 100644 --- a/Commands/UserCommands.cs +++ b/Commands/UserCommands.cs @@ -3,17 +3,6 @@ using System.Text; namespace WorldTime.Commands; public class UserCommands : CommandsBase { - const string EmbedHelpField1 = - $"`/help` - {HelpHelp}\n" - + $"`/list` - {HelpList}\n" - + $"`/set` - {HelpSet}\n" - + $"`/remove` - {HelpRemove}"; - const string EmbedHelpField2 = - $"`/config use-12hour` - {ConfigCommands.HelpUse12}\n" - + $"`/config private-confirms` - {ConfigCommands.HelpPrivateConfirms}\n" - + $"`/set-for` - {ConfigCommands.HelpSetFor}\n" - + $"`/remove-for` - {ConfigCommands.HelpRemoveFor}"; - #region Help strings const string HelpHelp = "Displays a list of available bot commands."; const string HelpList = "Shows the current time for all recently active known users."; @@ -23,6 +12,7 @@ public class UserCommands : CommandsBase { #endregion [SlashCommand("help", HelpHelp)] + [CommandContextType(InteractionContextType.Guild, InteractionContextType.BotDm)] public async Task CmdHelp() { var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); var guildct = ShardedClient.Guilds.Count; @@ -30,7 +20,8 @@ public class UserCommands : CommandsBase { var uniquetz = db.GetDistinctZoneCount(); await RespondAsync(embed: new EmbedBuilder() { Title = "Help & About", - Description = $"World Time v{version} - Serving {guildct} communities across {uniquetz} time zones.\n\n" + Description = + $"World Time v{version} - Serving {guildct} communities across {uniquetz} time zones.\n\n" + "This bot is provided for free, without any paywalled 'premium' features. " + "If you've found this bot useful, please consider contributing via the " + "bot author's page on Ko-fi: https://ko-fi.com/noithecat.", @@ -38,16 +29,28 @@ public class UserCommands : CommandsBase { IconUrl = Context.Client.CurrentUser.GetAvatarUrl(), Text = "World Time" } - }.AddField(inline: false, name: "Commands", value: EmbedHelpField1 - ).AddField(inline: false, name: "Admin commands", value: EmbedHelpField2 + }.AddField(inline: false, name: "Commands", value: + $""" + `/help` - {HelpHelp} + `/list` - {HelpList} + `/set` - {HelpSet} + `/remove` - {HelpRemove} + """ + ).AddField(inline: false, name: "Admin commands", value: + $""" + `/config use-12hour` - {ConfigCommands.HelpUse12} + `/config private-confirms` - {ConfigCommands.HelpPrivateConfirms} + `/set-for` - {ConfigCommands.HelpSetFor} + `/remove-for` - {ConfigCommands.HelpRemoveFor} + """ ).AddField(inline: false, name: "Zones", value: "This bot accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database). " + - "A useful tool to determine yours can be found at: https://kevinnovak.github.io/Time-Zone-Picker/" + "A useful tool to determine yours can be found at: https://zones.arilyn.cc/" ).Build()); } [SlashCommand("list", HelpList)] - [EnabledInDm(false)] + [CommandContextType(InteractionContextType.Guild)] public async Task CmdList([Summary(description: "A specific user whose time to look up.")]SocketGuildUser? user = null) { if (!await AreUsersDownloadedAsync(Context.Guild)) { await RespondAsync(ErrNoUserCache, ephemeral: true); @@ -76,9 +79,9 @@ public class UserCommands : CommandsBase { // Generate time and zone names to be displayed, group with associated user IDs var sortedlist = new SortedDictionary>(); var ampm = db.GuildSettings.Where(s => s.GuildId == Context.Guild.Id).SingleOrDefault()?.Use12HourTime ?? false; - foreach ((string area, List users) in userlist.OrderByDescending(o => o.Value.Count)) { + foreach ((var area, List users) in userlist.OrderByDescending(o => o.Value.Count)) { var areaprint = TzPrint(area, ampm); - if (!sortedlist.ContainsKey(areaprint)) sortedlist.Add(areaprint, new List()); + if (!sortedlist.ContainsKey(areaprint)) sortedlist[areaprint] = []; sortedlist[areaprint].AddRange(users); } @@ -87,10 +90,10 @@ public class UserCommands : CommandsBase { // Build zone listings with users var outputlines = new List(); - foreach ((string area, List users) in sortedlist) { + foreach ((var area, List users) in sortedlist) { var buffer = new StringBuilder(); buffer.Append(area[6..] + ": "); - bool empty = true; + var empty = true; foreach (var userid in users) { var userinstance = Context.Guild.GetUser(userid); if (userinstance == null) continue; @@ -107,7 +110,7 @@ public class UserCommands : CommandsBase { // Prepare for output - send buffers out if they become too large outputlines.Sort(); - bool hasOutputOneLine = false; + var hasOutputOneLine = false; // First output is shown as an interaction response, followed then as regular channel messages async Task doOutput(Embed msg) { if (!hasOutputOneLine) { @@ -137,7 +140,7 @@ public class UserCommands : CommandsBase { using var db = DbContext; var result = db.GetUserZone(parameter); if (result == null) { - bool isself = Context.User.Id == parameter.Id; + var isself = Context.User.Id == parameter.Id; if (isself) await RespondAsync(":x: You do not have a time zone. Set it with `tz.set`.", ephemeral: true); else await RespondAsync(":x: The given user does not have a time zone set.", ephemeral: true); return; @@ -149,7 +152,7 @@ public class UserCommands : CommandsBase { } [SlashCommand("set", HelpSet)] - [EnabledInDm(false)] + [CommandContextType(InteractionContextType.Guild)] public async Task CmdSet([Summary(description: "The new time zone to set.")]string zone) { var parsedzone = ParseTimeZone(zone); if (parsedzone == null) { @@ -164,7 +167,7 @@ public class UserCommands : CommandsBase { } [SlashCommand("remove", HelpRemove)] - [EnabledInDm(false)] + [CommandContextType(InteractionContextType.Guild)] public async Task CmdRemove() { using var db = DbContext; var success = db.DeleteUser((SocketGuildUser)Context.User); diff --git a/Data/BotDatabaseContext.cs b/Data/BotDatabaseContext.cs index a3fab44..51af9e8 100644 --- a/Data/BotDatabaseContext.cs +++ b/Data/BotDatabaseContext.cs @@ -102,7 +102,7 @@ public class BotDatabaseContext : DbContext { select Tuple.Create(entry.TimeZone, (ulong)entry.UserId); var resultSet = new Dictionary>(); foreach (var (tz, user) in query) { - if (!resultSet.ContainsKey(tz)) resultSet.Add(tz, new List()); + if (!resultSet.ContainsKey(tz)) resultSet[tz] = []; resultSet[tz].Add(user); } return resultSet; diff --git a/Data/Migrations/.editorconfig b/Data/Migrations/.editorconfig new file mode 100644 index 0000000..5def5b3 --- /dev/null +++ b/Data/Migrations/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] +generated_code = true +dotnet_analyzer_diagnostic.category-CodeQuality.severity = none +dotnet_diagnostic.CS1591.severity = none \ No newline at end of file diff --git a/Readme.md b/Readme.md index de416fe..0d28bf1 100644 --- a/Readme.md +++ b/Readme.md @@ -9,9 +9,8 @@ A social time zone reference tool! #### Running your own instance You need: -* .NET 6 (https://dotnet.microsoft.com/en-us/) +* .NET 8 (https://dotnet.microsoft.com/en-us/) * PostgreSQL (https://www.postgresql.org/) -* EF Core tools (https://learn.microsoft.com/en-us/ef/core/get-started/overview/install#get-the-entity-framework-core-tools) * A Discord bot token (https://discord.com/developers/applications) Get your bot token and set up your database user and schema, then create a JSON file containing the following: @@ -28,10 +27,11 @@ Get your bot token and set up your database user and schema, then create a JSON Then run the following commands: ```sh $ dotnet restore +$ dotnet tool restore $ dotnet ef database update -- -c path/to/config.json ``` And finally, to run the bot: -``` +```sh $ dotnet run -c Release -- -c path/to/config.json ``` \ No newline at end of file diff --git a/WorldTime.cs b/WorldTime.cs index 2b8906f..bf9e681 100644 --- a/WorldTime.cs +++ b/WorldTime.cs @@ -104,7 +104,7 @@ internal class WorldTime : IDisposable { // Discord Bots if (!string.IsNullOrEmpty(Config.DBotsToken)) { try { - string dBotsApiUrl = $"https://discord.bots.gg/api/v1/bots/{ botId }/stats"; + var dBotsApiUrl = $"https://discord.bots.gg/api/v1/bots/{ botId }/stats"; var body = $"{{ \"guildCount\": {guildCount} }}"; var uri = new Uri(string.Format(dBotsApiUrl)); diff --git a/WorldTime.csproj b/WorldTime.csproj index c618b94..551690e 100644 --- a/WorldTime.csproj +++ b/WorldTime.csproj @@ -1,28 +1,41 @@ + 2.3.3 + NoiTheCat + Exe - net6.0 + net8.0 enable enable - 2.3.2 - NoiTheCat + en + + + + none + false + 0 + AnyCPU + + + + AnyCPU - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - - - + + +