diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..595f1e7
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,287 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+[*]
+
+trim_trailing_whitespace = true
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = true
+
+# C# files
+[*.cs]
+
+#### Analyzer / IDE diagnostics (grouped) ####
+
+# Headers
+dotnet_diagnostic.IDE0073.severity = warning # File header mismatch
+
+# Usings
+dotnet_diagnostic.IDE0005.severity = warning # Remove unused usings
+
+# var preferences (only if you want nudges)
+dotnet_diagnostic.IDE0007.severity = suggestion # Use explicit type instead of 'var'
+dotnet_diagnostic.IDE0008.severity = suggestion # Use explicit type instead of 'var'
+
+# Namespace/folder mismatch
+dotnet_diagnostic.IDE0130.severity = suggestion # Namespace doesn't match folder structure
+
+# Unnecessary suppression
+dotnet_diagnostic.IDE0079.severity = warning # Unnecessary suppression
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = true
+
+#### .NET Code Actions ####
+
+# Type members
+dotnet_hide_advanced_members = false
+dotnet_member_insertion_location = with_other_members_of_the_same_kind
+dotnet_property_generation_behavior = prefer_throwing_properties
+
+# Symbol search
+dotnet_search_reference_assemblies = true
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+
+# Generic Lost Minions file header
+#file_header_template = Copyright (c) Lost Minions and contributors. All rights reserved.\nSee the LICENSE file in the project root for license information.
+
+# Prefer object/collection initializers (you already prefer them, make it visible)
+dotnet_diagnostic.IDE0017.severity = suggestion
+dotnet_diagnostic.IDE0028.severity = suggestion
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_prefer_system_hash_code = true
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = true
+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
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true
+csharp_style_expression_bodied_constructors = false
+csharp_style_expression_bodied_indexers = true
+csharp_style_expression_bodied_lambdas = true
+csharp_style_expression_bodied_local_functions = false
+csharp_style_expression_bodied_methods = false
+csharp_style_expression_bodied_operators = false
+csharp_style_expression_bodied_properties = true
+
+# 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_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true
+csharp_style_prefer_switch_expression = true
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_style_prefer_readonly_struct = true
+csharp_style_prefer_readonly_struct_member = true
+
+# Code-block preferences
+csharp_prefer_braces = true
+csharp_prefer_simple_using_statement = true
+csharp_prefer_system_threading_lock = true
+csharp_style_namespace_declarations = block_scoped
+csharp_style_prefer_method_group_conversion = true
+csharp_style_prefer_primary_constructors = true
+csharp_style_prefer_top_level_statements = true
+
+# 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_style_prefer_implicitly_typed_lambda_expression = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = true
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_unbound_generic_type_in_nameof = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
+csharp_style_allow_embedded_statements_on_same_line_experimental = true
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+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
+
+# Override for global usings files
+[*.GlobalUsings.cs]
+dotnet_diagnostic.IDE0005.severity = none
diff --git a/.github/workflows/test-compile.yml b/.github/workflows/test-compile.yml
new file mode 100644
index 0000000..aff5d37
--- /dev/null
+++ b/.github/workflows/test-compile.yml
@@ -0,0 +1,46 @@
+name: 🧪 C# Compilation Test
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [ main, master ]
+ paths:
+ - ".github/workflows/test-compile.yml"
+ - "**/*.cs"
+ - "**/*.csproj"
+ - "*.sln"
+
+jobs:
+ compile-test:
+ runs-on: windows-latest
+
+ env:
+ SOLUTION_NAME: Mee6LevelsAPI.sln
+
+ steps:
+ - name: ⚙️ Configure Git
+ shell: pwsh
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git config --global credential.helper store
+
+ - name: 🧾 Checkout Repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: 🧰 Setup .NET 8 SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: 🧱 Restore dependencies
+ run: dotnet restore $env:SOLUTION_NAME
+
+ - name: 🏗️ Build solution
+ run: dotnet build $env:SOLUTION_NAME --configuration Release --no-restore
+
+ - name: ✅ Verify Build Success
+ run: echo "✅ $env:SOLUTION_NAME compiled successfully on GitHub Actions."
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..eba7ce0
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+ net8.0
+ latest
+ enable
+ true
+
+
diff --git a/Mee6LevelsAPI.Tests/Mee6DtoTests.cs b/Mee6LevelsAPI.Tests/Mee6DtoTests.cs
new file mode 100644
index 0000000..73da45f
--- /dev/null
+++ b/Mee6LevelsAPI.Tests/Mee6DtoTests.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+
+using Newtonsoft.Json;
+
+using Xunit;
+
+namespace Mee6LevelsAPI.Tests
+{
+ public sealed class Mee6DtoTests
+ {
+ [Fact]
+ public void Mee6UserInfo_JsonSerialization_RoundTripsFields()
+ {
+ Mee6UserInfo original = new()
+ {
+ Avatar = "avatar-hash",
+ DetailedXp = new[] { 10, 20, 30 },
+ Discriminator = "1234",
+ GuildId = "987654321",
+ Id = "123456789",
+ Level = 7,
+ MessageCount = 42,
+ Username = "TestUser",
+ Xp = 12345
+ };
+
+ string json = JsonConvert.SerializeObject(original);
+ Mee6UserInfo deserialized = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(original.Avatar, deserialized.Avatar);
+ Assert.Equal(original.DetailedXp, deserialized.DetailedXp);
+ Assert.Equal(original.Discriminator, deserialized.Discriminator);
+ Assert.Equal(original.GuildId, deserialized.GuildId);
+ Assert.Equal(original.Id, deserialized.Id);
+ Assert.Equal(original.Level, deserialized.Level);
+ Assert.Equal(original.MessageCount, deserialized.MessageCount);
+ Assert.Equal(original.Username, deserialized.Username);
+ Assert.Equal(original.Xp, deserialized.Xp);
+ }
+
+ [Fact]
+ public void Mee6GuildInfo_JsonSerialization_RoundTripsFields()
+ {
+ Mee6GuildInfo original = new()
+ {
+ AllowJoin = true,
+ Icon = "icon-hash",
+ Id = "123456789",
+ InviteLeaderboard = true,
+ LeaderboardURL = "https://mee6.xyz/leaderboard/123456789",
+ Name = "Test Guild",
+ Premium = false
+ };
+
+ string json = JsonConvert.SerializeObject(original);
+ Mee6GuildInfo deserialized = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(original.AllowJoin, deserialized.AllowJoin);
+ Assert.Equal(original.Icon, deserialized.Icon);
+ Assert.Equal(original.Id, deserialized.Id);
+ Assert.Equal(original.InviteLeaderboard, deserialized.InviteLeaderboard);
+ Assert.Equal(original.LeaderboardURL, deserialized.LeaderboardURL);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Premium, deserialized.Premium);
+ }
+
+ [Fact]
+ public void Mee6Role_JsonSerialization_RoundTripsFields()
+ {
+ Mee6Role original = new()
+ {
+ Color = 0x00FF00,
+ Hoist = true,
+ Id = "987654321",
+ Managed = false,
+ Mentionable = true,
+ Name = "Tester",
+ Permissions = 123456789L,
+ Position = 5
+ };
+
+ string json = JsonConvert.SerializeObject(original);
+ Mee6Role deserialized = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(original.Color, deserialized.Color);
+ Assert.Equal(original.Hoist, deserialized.Hoist);
+ Assert.Equal(original.Id, deserialized.Id);
+ Assert.Equal(original.Managed, deserialized.Managed);
+ Assert.Equal(original.Mentionable, deserialized.Mentionable);
+ Assert.Equal(original.Name, deserialized.Name);
+ Assert.Equal(original.Permissions, deserialized.Permissions);
+ Assert.Equal(original.Position, deserialized.Position);
+ }
+
+ [Fact]
+ public void Mee6RoleInfo_JsonSerialization_RoundTripsFields()
+ {
+ Mee6RoleInfo original = new()
+ {
+ Rank = 1,
+ Role = new Mee6Role
+ {
+ Color = 0xFF0000,
+ Hoist = false,
+ Id = "role-id",
+ Managed = true,
+ Mentionable = false,
+ Name = "RoleName",
+ Permissions = 777,
+ Position = 10
+ }
+ };
+
+ string json = JsonConvert.SerializeObject(original);
+ Mee6RoleInfo deserialized = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(original.Rank, deserialized.Rank);
+ Assert.Equal(original.Role.Color, deserialized.Role.Color);
+ Assert.Equal(original.Role.Hoist, deserialized.Role.Hoist);
+ Assert.Equal(original.Role.Id, deserialized.Role.Id);
+ Assert.Equal(original.Role.Managed, deserialized.Role.Managed);
+ Assert.Equal(original.Role.Mentionable, deserialized.Role.Mentionable);
+ Assert.Equal(original.Role.Name, deserialized.Role.Name);
+ Assert.Equal(original.Role.Permissions, deserialized.Role.Permissions);
+ Assert.Equal(original.Role.Position, deserialized.Role.Position);
+ }
+
+ [Fact]
+ public void Mee6Server_JsonSerialization_RoundTripsFields()
+ {
+ Mee6UserInfo user = new()
+ {
+ Avatar = "avatar-hash",
+ DetailedXp = new[] { 100, 200 },
+ Discriminator = "0001",
+ GuildId = "guild-1",
+ Id = "user-1",
+ Level = 3,
+ MessageCount = 10,
+ Username = "User1",
+ Xp = 300
+ };
+
+ Mee6RoleInfo roleInfo = new()
+ {
+ Rank = 1,
+ Role = new Mee6Role
+ {
+ Color = 1,
+ Hoist = false,
+ Id = "role-1",
+ Managed = false,
+ Mentionable = true,
+ Name = "Role1",
+ Permissions = 42,
+ Position = 1
+ }
+ };
+
+ Mee6Server original = new()
+ {
+ Admin = true,
+ BannerURL = "https://example.com/banner.png",
+ Guild = new Mee6GuildInfo
+ {
+ AllowJoin = true,
+ Icon = "icon-hash",
+ Id = "guild-1",
+ InviteLeaderboard = false,
+ LeaderboardURL = "https://mee6.xyz/leaderboard/guild-1",
+ Name = "Guild One",
+ Premium = true
+ },
+ IsMember = true,
+ Page = 1,
+ Users = new[] { user },
+ RewardRoles = new[] { roleInfo },
+ XPPerMessage = new[] { 5L, 10L },
+ XPRate = 2
+ };
+
+ string json = JsonConvert.SerializeObject(original);
+ Mee6Server deserialized = JsonConvert.DeserializeObject(json);
+
+ Assert.Equal(original.Admin, deserialized.Admin);
+ Assert.Equal(original.BannerURL, deserialized.BannerURL);
+
+ Assert.Equal(original.Guild.AllowJoin, deserialized.Guild.AllowJoin);
+ Assert.Equal(original.Guild.Icon, deserialized.Guild.Icon);
+ Assert.Equal(original.Guild.Id, deserialized.Guild.Id);
+ Assert.Equal(original.Guild.InviteLeaderboard, deserialized.Guild.InviteLeaderboard);
+ Assert.Equal(original.Guild.LeaderboardURL, deserialized.Guild.LeaderboardURL);
+ Assert.Equal(original.Guild.Name, deserialized.Guild.Name);
+ Assert.Equal(original.Guild.Premium, deserialized.Guild.Premium);
+
+ Assert.Equal(original.IsMember, deserialized.IsMember);
+ Assert.Equal(original.Page, deserialized.Page);
+
+ Assert.NotNull(deserialized.Users);
+ _ = Assert.Single(deserialized.Users);
+ Assert.Equal(original.Users[0].Id, deserialized.Users[0].Id);
+ Assert.Equal(original.Users[0].Username, deserialized.Users[0].Username);
+ Assert.Equal(original.Users[0].Xp, deserialized.Users[0].Xp);
+
+ Assert.NotNull(deserialized.RewardRoles);
+ _ = Assert.Single(deserialized.RewardRoles);
+ Assert.Equal(original.RewardRoles[0].Rank, deserialized.RewardRoles[0].Rank);
+ Assert.Equal(original.RewardRoles[0].Role.Name, deserialized.RewardRoles[0].Role.Name);
+
+ Assert.Equal(original.XPPerMessage, deserialized.XPPerMessage);
+ Assert.Equal(original.XPRate, deserialized.XPRate);
+ }
+
+ [Theory]
+ [InlineData(typeof(Mee6UserInfo), "Avatar", "avatar")]
+ [InlineData(typeof(Mee6UserInfo), "DetailedXp", "detailed_xp")]
+ [InlineData(typeof(Mee6UserInfo), "Discriminator", "discriminator")]
+ [InlineData(typeof(Mee6UserInfo), "GuildId", "guild_id")]
+ [InlineData(typeof(Mee6UserInfo), "Id", "id")]
+ [InlineData(typeof(Mee6UserInfo), "Level", "level")]
+ [InlineData(typeof(Mee6UserInfo), "MessageCount", "message_count")]
+ [InlineData(typeof(Mee6UserInfo), "Username", "username")]
+ [InlineData(typeof(Mee6UserInfo), "Xp", "xp")]
+ public void Mee6UserInfo_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName)
+ {
+ FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
+ Assert.NotNull(field);
+
+ JsonPropertyAttribute? attr = field!.GetCustomAttribute();
+ Assert.NotNull(attr);
+ Assert.Equal(expectedJsonName, attr!.PropertyName);
+ }
+
+ [Theory]
+ [InlineData(typeof(Mee6GuildInfo), "AllowJoin", "allow_join")]
+ [InlineData(typeof(Mee6GuildInfo), "Icon", "icon")]
+ [InlineData(typeof(Mee6GuildInfo), "Id", "id")]
+ [InlineData(typeof(Mee6GuildInfo), "InviteLeaderboard", "invite_leaderboard")]
+ [InlineData(typeof(Mee6GuildInfo), "LeaderboardURL", "leaderboard_url")]
+ [InlineData(typeof(Mee6GuildInfo), "Name", "name")]
+ [InlineData(typeof(Mee6GuildInfo), "Premium", "premium")]
+ public void Mee6GuildInfo_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName)
+ {
+ FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
+ Assert.NotNull(field);
+
+ JsonPropertyAttribute? attr = field!.GetCustomAttribute();
+ Assert.NotNull(attr);
+ Assert.Equal(expectedJsonName, attr!.PropertyName);
+ }
+
+ [Theory]
+ [InlineData(typeof(Mee6Role), "Color", "color")]
+ [InlineData(typeof(Mee6Role), "Hoist", "hoist")]
+ [InlineData(typeof(Mee6Role), "Id", "id")]
+ [InlineData(typeof(Mee6Role), "Managed", "managed")]
+ [InlineData(typeof(Mee6Role), "Mentionable", "mentionable")]
+ [InlineData(typeof(Mee6Role), "Name", "name")]
+ [InlineData(typeof(Mee6Role), "Permissions", "permissions")]
+ [InlineData(typeof(Mee6Role), "Position", "position")]
+ public void Mee6Role_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName)
+ {
+ FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
+ Assert.NotNull(field);
+
+ JsonPropertyAttribute? attr = field!.GetCustomAttribute();
+ Assert.NotNull(attr);
+ Assert.Equal(expectedJsonName, attr!.PropertyName);
+ }
+
+ [Theory]
+ [InlineData(typeof(Mee6Server), "Admin", "admin")]
+ [InlineData(typeof(Mee6Server), "BannerURL", "banner_url")]
+ [InlineData(typeof(Mee6Server), "Guild", "guild")]
+ [InlineData(typeof(Mee6Server), "IsMember", "is_member")]
+ [InlineData(typeof(Mee6Server), "Page", "page")]
+ [InlineData(typeof(Mee6Server), "Users", "players")]
+ [InlineData(typeof(Mee6Server), "RewardRoles", "role_rewards")]
+ [InlineData(typeof(Mee6Server), "XPPerMessage", "xp_per_message")]
+ [InlineData(typeof(Mee6Server), "XPRate", "xp_rate")]
+ public void Mee6Server_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName)
+ {
+ FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
+ Assert.NotNull(field);
+
+ JsonPropertyAttribute? attr = field!.GetCustomAttribute();
+ Assert.NotNull(attr);
+ Assert.Equal(expectedJsonName, attr!.PropertyName);
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // NOTE: The methods below (Mee6.GetServer / GetServerAsync / GetUserInfo /
+ // GetAvatarAsync) create HttpClient internally and hit the real Mee6 /
+ // Discord APIs. To truly unit test them, you’ll want to refactor Mee6 to
+ // accept an HttpClient (or an IMee6Api abstraction) so you can inject a
+ // fake handler in tests.
+ //
+ // For now, you could write integration tests like this, replacing the
+ // IDs with real ones from your server:
+ // ----------------------------------------------------------------------
+ public sealed class Mee6IntegrationExamples
+ {
+ // Replace with a real guild ID that has Mee6 levels enabled.
+ private const long ExampleGuildId = 123456789012345678;
+
+ // Replace with a real user ID in that guild.
+ private const long ExampleUserId = 234567890123456789;
+
+ [Fact(Skip = "Integration example – hits the real Mee6 API; configure IDs before enabling.")]
+ public async Task GetServerAsync_ReturnsUsers_ForRealGuild()
+ {
+ Mee6Server server = await Mee6.GetServerAsync(ExampleGuildId);
+
+ Assert.NotNull(server.Users);
+ Assert.NotEmpty(server.Users);
+ }
+
+ [Fact(Skip = "Integration example – hits the real Mee6 API; configure IDs before enabling.")]
+ public void GetUserInfo_ReturnsUser_ForRealGuildAndUser()
+ {
+ Mee6UserInfo user = Mee6.GetUserInfo(ExampleGuildId, ExampleUserId);
+
+ Assert.False(string.IsNullOrEmpty(user.Id));
+ Assert.False(string.IsNullOrEmpty(user.Username));
+ }
+ }
+}
diff --git a/Mee6LevelsAPI.Tests/Mee6LevelsAPI.Tests.csproj b/Mee6LevelsAPI.Tests/Mee6LevelsAPI.Tests.csproj
new file mode 100644
index 0000000..df61d0a
--- /dev/null
+++ b/Mee6LevelsAPI.Tests/Mee6LevelsAPI.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+
+ Mee6LevelsAPI.Tests
+ Mee6LevelsAPI.Tests
+
+
+ false
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/Mee6LevelsAPI.sln b/Mee6LevelsAPI.sln
index e894acb..1b5599d 100644
--- a/Mee6LevelsAPI.sln
+++ b/Mee6LevelsAPI.sln
@@ -1,10 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30717.126
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36518.9 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mee6LevelsAPI", "Mee6LevelsAPI\Mee6LevelsAPI.csproj", "{AC3BDAED-B576-425E-8893-EDDB796C214F}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mee6LevelsAPI.Tests", "Mee6LevelsAPI.Tests\Mee6LevelsAPI.Tests.csproj", "{FD769813-41E0-A579-9F32-110065E5073D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,10 +19,17 @@ Global
{AC3BDAED-B576-425E-8893-EDDB796C214F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC3BDAED-B576-425E-8893-EDDB796C214F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC3BDAED-B576-425E-8893-EDDB796C214F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD769813-41E0-A579-9F32-110065E5073D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD769813-41E0-A579-9F32-110065E5073D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD769813-41E0-A579-9F32-110065E5073D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD769813-41E0-A579-9F32-110065E5073D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {FD769813-41E0-A579-9F32-110065E5073D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC28C2AB-FAB9-4442-8812-48ADFB08FA3D}
EndGlobalSection
diff --git a/Mee6LevelsAPI/Mee6.cs b/Mee6LevelsAPI/Mee6.cs
index 402061b..7386cde 100644
--- a/Mee6LevelsAPI/Mee6.cs
+++ b/Mee6LevelsAPI/Mee6.cs
@@ -1,25 +1,20 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Drawing;
-using System.Net.Http;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using System.IO;
+using Newtonsoft.Json;
+
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
namespace Mee6LevelsAPI
{
public static class Mee6
{
- const string Url = "https://mee6.xyz/api/plugins/levels/leaderboard/";
+ private const string Url = "https://mee6.xyz/api/plugins/levels/leaderboard/";
public static int Limit = 1000;
+
public static Mee6UserInfo GetUserInfo(long guildID, long userID)
{
Mee6Server server = GetServer(guildID);
- for(int i = 0; i < Limit; i++)
+ for (int i = 0; i < Limit; i++)
{
Mee6UserInfo user = server.Users[i];
if (user.Id.Equals(userID.ToString()))
@@ -32,9 +27,9 @@ public static Mee6UserInfo GetUserInfo(long guildID, long userID)
public static Mee6Server GetServer(long guildID)
{
- HttpClient client = new HttpClient();
+ HttpClient client = new();
HttpResponseMessage response = client.GetAsync(Url + $"{guildID}?limit={Limit}").Result;
- response.EnsureSuccessStatusCode();
+ _ = response.EnsureSuccessStatusCode();
string responseBody = response.Content.ReadAsStringAsync().Result;
Mee6Server server = JsonConvert.DeserializeObject(responseBody);
@@ -43,9 +38,9 @@ public static Mee6Server GetServer(long guildID)
public static async Task GetServerAsync(long guildID)
{
- HttpClient client = new HttpClient();
+ HttpClient client = new();
HttpResponseMessage response = await client.GetAsync(Url + $"{guildID}?limit={Limit}");
- response.EnsureSuccessStatusCode();
+ _ = response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
Mee6Server server = JsonConvert.DeserializeObject(responseBody);
@@ -54,9 +49,9 @@ public static async Task GetServerAsync(long guildID)
public static Mee6Server GetServer(long guildID, string fileName)
{
- HttpClient client = new HttpClient();
+ HttpClient client = new();
HttpResponseMessage response = client.GetAsync(Url + $"{guildID}?limit={Limit}").Result;
- response.EnsureSuccessStatusCode();
+ _ = response.EnsureSuccessStatusCode();
string responseBody = response.Content.ReadAsStringAsync().Result;
Mee6Server server = JsonConvert.DeserializeObject(responseBody);
@@ -65,16 +60,12 @@ public static Mee6Server GetServer(long guildID, string fileName)
return server;
}
- public static Image GetAvatar(Mee6UserInfo user, int size)
+ public static async Task> GetAvatarAsync(Mee6UserInfo user, int size)
{
string imageUrl = $"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}?size={size}";
- using (System.Net.WebClient webClient = new System.Net.WebClient())
- {
- using (Stream stream = webClient.OpenRead(imageUrl))
- {
- return Image.FromStream(stream);
- }
- }
+ using HttpClient httpClient = new();
+ using Stream stream = await httpClient.GetStreamAsync(imageUrl);
+ return await Image.LoadAsync(stream);
}
}
@@ -82,20 +73,28 @@ public struct Mee6UserInfo
{
[JsonProperty("avatar")]
public string Avatar;
+
[JsonProperty("detailed_xp")]
public int[] DetailedXp;
+
[JsonProperty("discriminator")]
public string Discriminator;
+
[JsonProperty("guild_id")]
public string GuildId;
+
[JsonProperty("id")]
public string Id;
+
[JsonProperty("level")]
public long Level;
+
[JsonProperty("message_count")]
public long MessageCount;
+
[JsonProperty("username")]
public string Username;
+
[JsonProperty("xp")]
public long Xp;
}
@@ -104,16 +103,22 @@ public struct Mee6GuildInfo
{
[JsonProperty("allow_join")]
public bool AllowJoin;
+
[JsonProperty("icon")]
public string Icon;
+
[JsonProperty("id")]
public string Id;
+
[JsonProperty("invite_leaderboard")]
public bool InviteLeaderboard;
+
[JsonProperty("leaderboard_url")]
public string LeaderboardURL;
+
[JsonProperty("name")]
public string Name;
+
[JsonProperty("premium")]
public bool Premium;
}
@@ -122,6 +127,7 @@ public struct Mee6RoleInfo
{
[JsonProperty("rank")]
public long Rank;
+
[JsonProperty("role")]
public Mee6Role Role;
}
@@ -130,18 +136,25 @@ public struct Mee6Role
{
[JsonProperty("color")]
public long Color;
+
[JsonProperty("hoist")]
public bool Hoist;
+
[JsonProperty("id")]
public string Id;
+
[JsonProperty("managed")]
public bool Managed;
+
[JsonProperty("mentionable")]
public bool Mentionable;
+
[JsonProperty("name")]
public string Name;
+
[JsonProperty("permissions")]
public long Permissions;
+
[JsonProperty("position")]
public long Position;
}
@@ -150,22 +163,30 @@ public struct Mee6Server
{
[JsonProperty("admin")]
public bool Admin;
+
[JsonProperty("banner_url")]
public string BannerURL;
+
[JsonProperty("guild")]
public Mee6GuildInfo Guild;
+
[JsonProperty("is_member")]
public bool IsMember;
+
[JsonProperty("page")]
public long Page;
+
[JsonProperty("players")]
public Mee6UserInfo[] Users;
+
[JsonProperty("role_rewards")]
public Mee6RoleInfo[] RewardRoles;
+
//user_guild_settings not implemented
[JsonProperty("xp_per_message")]
public long[] XPPerMessage;
+
[JsonProperty("xp_rate")]
public long XPRate;
}
-}
+}
\ No newline at end of file
diff --git a/Mee6LevelsAPI/Mee6LevelsAPI.csproj b/Mee6LevelsAPI/Mee6LevelsAPI.csproj
index 5d791b3..cf78285 100644
--- a/Mee6LevelsAPI/Mee6LevelsAPI.csproj
+++ b/Mee6LevelsAPI/Mee6LevelsAPI.csproj
@@ -1,55 +1,24 @@
-
-
-
+
+
- Debug
- AnyCPU
- {AC3BDAED-B576-425E-8893-EDDB796C214F}
- Library
- Properties
- Mee6LevelsAPI
+
+ enable
+ enable
+
Mee6LevelsAPI
- v4.7.2
- 512
- true
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
+ Mee6LevelsAPI
+ Library
+ latest
+
-
- ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
\ No newline at end of file
+
+
diff --git a/Mee6LevelsAPI/Properties/AssemblyInfo.cs b/Mee6LevelsAPI/Properties/AssemblyInfo.cs
deleted file mode 100644
index 1c5ac2a..0000000
--- a/Mee6LevelsAPI/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Mee6LevelsAPI")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Mee6LevelsAPI")]
-[assembly: AssemblyCopyright("Copyright © 2020")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("ac3bdaed-b576-425e-8893-eddb796c214f")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Mee6LevelsAPI/packages.config b/Mee6LevelsAPI/packages.config
deleted file mode 100644
index a9de8b5..0000000
--- a/Mee6LevelsAPI/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file