Browse Source

Merge remote-tracking branch 'origin/Develop' into Develop

DarkGolly 4 years ago
parent
commit
96c4a97cce

+ 2 - 1
CardCollector.sln.DotSettings.user

@@ -1,5 +1,6 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
-	<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=CardCollector_002FResources_002FCallbackQueryCommands/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=CardCollector_002FResources_002FCallbackQueryCommands/@EntryIndexedValue">False</s:Boolean>
+	<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=CardCollector_002FResources_002FInlineQueryCommands/@EntryIndexedValue">True</s:Boolean>
 	
 	<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=CardCollector_002FResources_002FMessageCommands/@EntryIndexedValue">False</s:Boolean>
 	<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=CardCollector_002FResources_002FMessages/@EntryIndexedValue">False</s:Boolean>

+ 9 - 0
CardCollector/CardCollector.csproj

@@ -34,6 +34,10 @@
         <Generator>ResXFileCodeGenerator</Generator>
         <LastGenOutput>SortingTypes.Designer.cs</LastGenOutput>
       </EmbeddedResource>
+      <EmbeddedResource Update="Resources\InlineQueryCommands.resx">
+        <Generator>ResXFileCodeGenerator</Generator>
+        <LastGenOutput>InlineQueryCommands.Designer.cs</LastGenOutput>
+      </EmbeddedResource>
     </ItemGroup>
 
     <ItemGroup>
@@ -62,6 +66,11 @@
         <AutoGen>True</AutoGen>
         <DependentUpon>SortingTypes.resx</DependentUpon>
       </Compile>
+      <Compile Update="Resources\InlineQueryCommands.Designer.cs">
+        <DesignTime>True</DesignTime>
+        <AutoGen>True</AutoGen>
+        <DependentUpon>InlineQueryCommands.resx</DependentUpon>
+      </Compile>
     </ItemGroup>
 
 </Project>

+ 5 - 1
CardCollector/Commands/ChosenInlineResult/ChosenInlineResult.cs

@@ -25,8 +25,12 @@ namespace CardCollector.Commands.ChosenInlineResult
         protected readonly string InlineResult = "";
         
         /* Список команд */
-        private static readonly List<ChosenInlineResult> List = new()
+        protected static readonly List<ChosenInlineResult> List = new()
         {
+            /* Этот объект должен быть всегда в начале списка, так как он должен быть вызван
+             вперед других, если в коде включен режим бесконечных стикеров */
+            new GetUnlimitedStickerAndExecuteCommand(),
+        
             // Обработка результата при отправке стикера
             new SendStickerResult(),
         };

+ 52 - 0
CardCollector/Commands/ChosenInlineResult/GetUnlimitedStickerAndExecuteCommand.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using CardCollector.DataBase.Entity;
+using CardCollector.DataBase.EntityDao;
+using CardCollector.Resources;
+using Telegram.Bot.Types;
+
+namespace CardCollector.Commands.ChosenInlineResult
+{
+    public class GetUnlimitedStickerAndExecuteCommand : ChosenInlineResult
+    {
+        protected override string Command => InlineQueryCommands.unlimited_stickers;
+        
+        public override async Task Execute()
+        {
+            /* Получаем хеш стикера */
+            var hash = InlineResult.Split('=')[1];
+            /* Получаем объект стикера */
+            var sticker = await StickerDao.GetStickerByHash(hash);
+            /* Выдаем пользователю 1 стикер */
+            await UserStickerRelationDao.AddNew(User, sticker, 1);
+            /* Выполняем стандартный сценарий команды */
+            await PrivateFactory(Update, User).Execute();
+        }
+        
+        public GetUnlimitedStickerAndExecuteCommand() { }
+        public GetUnlimitedStickerAndExecuteCommand(UserEntity user, Update update, string inlineResult)
+            : base(user, update, inlineResult) { }
+        
+        
+        /* Список команд, аналогичный родительскому, только не включает эту команду (unlimited) */
+        private static readonly List<ChosenInlineResult> PrivateList = List.GetRange(1, List.Count - 1);
+        
+        /* Метод, создающий объекты команд исходя из полученного обновления */
+        private static UpdateModel PrivateFactory(Update update, UserEntity user)
+        {
+            // Текст команды
+            var command = update.ChosenInlineResult!.ResultId;
+            
+            // Возвращаем объект, если команда совпала
+            foreach (var item in PrivateList.Where(item => item.IsMatches(command)))
+                if(Activator.CreateInstance(item.GetType(), 
+                    user, update, update.ChosenInlineResult.ResultId) is ChosenInlineResult executor)
+                    if (executor.IsMatches(command)) return executor;
+        
+            // Возвращаем команда не найдена, если код дошел до сюда
+            return new CommandNotFound(user, update, command);
+        }
+    }
+}

+ 3 - 2
CardCollector/Commands/ChosenInlineResult/SendStickerResult.cs

@@ -1,5 +1,6 @@
 using System.Threading.Tasks;
 using CardCollector.DataBase.Entity;
+using CardCollector.Resources;
 using Telegram.Bot.Types;
 
 namespace CardCollector.Commands.ChosenInlineResult
@@ -8,7 +9,7 @@ namespace CardCollector.Commands.ChosenInlineResult
     public class SendStickerResult : ChosenInlineResult
     {
         /* Ключевое слово для данной команды send_sticker */
-        protected override string Command => "send_sticker";
+        protected override string Command => InlineQueryCommands.send_sticker;
         public override Task Execute()
         {
             // Получаем MD5 хеш из полученного запроса разделением по символу '='
@@ -19,8 +20,8 @@ namespace CardCollector.Commands.ChosenInlineResult
             return Task.CompletedTask;
         }
         
+        public SendStickerResult() { }
         public SendStickerResult(UserEntity user, Update update, string inlineResult)
             : base(user, update, inlineResult) { }
-        public SendStickerResult() { }
     }
 }

+ 1 - 1
CardCollector/Commands/IgnoreUpdate.cs

@@ -6,6 +6,6 @@ namespace CardCollector.Commands
     public class IgnoreUpdate : UpdateModel
     {
         protected override string Command => "";
-        public override async Task Execute() { }
+        public override Task Execute() { return  Task.CompletedTask; }
     }
 }

+ 2 - 0
CardCollector/Commands/InlineQuery/InlineQuery.cs

@@ -28,6 +28,8 @@ namespace CardCollector.Commands.InlineQuery
         {
             // Показать стикеры в чатах для отправки (кроме личного чата с ботом)
             new ShowStickersInGroup(),
+            // Показать стикеры в личных сообщениях с ботом для выбора или просмотра информации
+            new ShowStickersInBotChat(),
         };
         
         

+ 36 - 0
CardCollector/Commands/InlineQuery/ShowStickersInBotChat.cs

@@ -0,0 +1,36 @@
+using System.Threading.Tasks;
+using CardCollector.Controllers;
+using CardCollector.DataBase.Entity;
+using CardCollector.Resources;
+using Telegram.Bot.Types;
+
+namespace CardCollector.Commands.InlineQuery
+{
+    /* Отображение стикеров в личной беседt с ботом */
+    public class ShowStickersInBotChat : InlineQuery
+    {
+        /* Команда - пустая строка, поскольку пользователь может вводить любые слова
+         после @имя_бота, введенная фраза будет использоваться для фильтрации стикеров */
+        protected override string Command => "";
+        
+        public override async Task Execute()
+        {
+            // Фильтр - введенная пользователем фраза
+            var filter = Update.InlineQuery!.Query;
+            // Получаем список стикеров
+            var stickersList = await User.GetStickersList(InlineQueryCommands.select_sticker, filter, true);
+            // Посылаем пользователю ответ на его запрос
+            await MessageController.AnswerInlineQuery(InlineQueryId, stickersList);
+        }
+
+        /* Команда пользователя удовлетворяет условию, если она вызвана
+         в личных сообщениях с ботом */
+        protected internal override bool IsMatches(string command)
+        {
+            return command.Contains("Sender");
+        }
+
+        public ShowStickersInBotChat() { }
+        public ShowStickersInBotChat(UserEntity user, Update update, string inlineQueryId) : base(user, update, inlineQueryId) { }
+    }
+}

+ 3 - 2
CardCollector/Commands/InlineQuery/ShowStickersInGroup.cs

@@ -1,6 +1,7 @@
 using System.Threading.Tasks;
 using CardCollector.Controllers;
 using CardCollector.DataBase.Entity;
+using CardCollector.Resources;
 using Telegram.Bot.Types;
 
 namespace CardCollector.Commands.InlineQuery
@@ -17,14 +18,14 @@ namespace CardCollector.Commands.InlineQuery
             // Фильтр - введенная пользователем фраза
             var filter = Update.InlineQuery!.Query;
             // Получаем список стикеров
-            var stickersList = await User.GetStickersList("send_sticker",filter);
+            var stickersList = await User.GetStickersList(InlineQueryCommands.send_sticker, filter);
             // Посылаем пользователю ответ на его запрос
             await MessageController.AnswerInlineQuery(InlineQueryId, stickersList);
         }
 
         /* Команда пользователя удовлетворяет условию, если она вызвана
          в беседе/канале/личных сообщениях (кроме личных сообщений с ботом) */
-        protected override bool IsMatches(string command)
+        protected internal override bool IsMatches(string command)
         {
             return command.Contains("Group") || command.Contains("Supergroup") || command.Contains("Private");
         }

+ 1 - 1
CardCollector/Commands/Message/ShowSampleMessage.cs

@@ -54,7 +54,7 @@ namespace CardCollector.Commands.Message
         }
 
         /* Нужно помимо совпадения текста проверить пользователя на уровень привилегий */
-        protected override bool IsMatches(string command)
+        protected internal override bool IsMatches(string command)
         {
             return base.IsMatches(command) && User is not {PrivilegeLevel: < Constants.PROGRAMMER_PRIVILEGE_LEVEL};
         }

+ 1 - 1
CardCollector/Commands/UpdateModel.cs

@@ -19,7 +19,7 @@ namespace CardCollector.Commands
 
         public abstract Task Execute();
 
-        protected virtual bool IsMatches(string command)
+        protected internal virtual bool IsMatches(string command)
         {
             return command.Contains(Command);
         }

+ 3 - 0
CardCollector/DataBase/Entity/StickerEntity.cs

@@ -35,5 +35,8 @@ namespace CardCollector.DataBase.Entity
         
         /* Описание стикера */
         [Column("description"), MaxLength(1024)] public string Description { get; set; } = "";
+        
+        /* Хеш id стикера для определения его в системе */
+        [Column("md5hash"), MaxLength(40)] public string Md5Hash { get; set; }
     }
 }

+ 69 - 20
CardCollector/DataBase/Entity/UserEntity.cs

@@ -63,28 +63,77 @@ namespace CardCollector.DataBase.Entity
         }
         
         /* Возвращает стикеры в виде объектов телеграм */
-        public async Task<IEnumerable<InlineQueryResult>> GetStickersList(string command, string filter)
+        public async Task<IEnumerable<InlineQueryResult>> GetStickersList(string command, string filter, bool userFilterEnabled = false)
+        {
+            /* Получаем список стикеров исходя из того, нужно ли для отладки получить бесконечные стикеры */
+            var stickersList = Constants.UNLIMITED_ALL_STICKERS
+                ? await GetUnlimitedStickers(filter) : await GetUserStickers(filter);
+            /* Если пользовательская сортировка не применена, то возвращаем реультат */
+            if (!userFilterEnabled) return ToTelegramResults(stickersList, command);
+            /* Фильтруем по автору */
+            if (Filters["author"] is not "")
+                stickersList = stickersList.Where(item => item.Author.Contains((string) Filters["author"]));
+            /* Фильтруем по тиру */
+            if (Filters["tier"] is not -1)
+                stickersList = stickersList.Where(item => item.Tier.Equals((int) Filters["tier"]));
+            /* Фильтруем по эмоции */
+            if (Filters["emoji"] is not "")
+                stickersList = stickersList.Where(item => item.Emoji.Contains((string) Filters["emoji"]));
+            /* Фильтруем по цене если пользователь не в меню коллекции */
+            if (Filters["emoji"] is not "" && State is not UserState.CollectionMenu)
+                stickersList = stickersList.Where(item => item.Emoji.Contains((string) Filters["emoji"]));
+            /* Если не установлена сортировка, возвращаем результат */
+            if ((string) Filters["sorting"] == SortingTypes.None) return ToTelegramResults(stickersList, command);
+            
+            /* Сортируем список, если тип сортировки установлен */
+            /* Сортируем по автору */
+            if ((string) Filters["sorting"] == SortingTypes.ByAuthor)
+                stickersList = stickersList.OrderBy(item => item.Author);
+            /* Сортируем по названию */
+            if ((string) Filters["sorting"] == SortingTypes.ByTitle)
+                stickersList = stickersList.OrderBy(item => item.Title);
+            /* Сортируем по увеличению тира */
+            if ((string) Filters["sorting"] == SortingTypes.ByTierIncrease)
+                stickersList = stickersList.OrderBy(item => item.Tier);
+            /* Сортируем по уменьшению тира */
+            if ((string) Filters["sorting"] == SortingTypes.ByTierDecrease)
+                stickersList = stickersList.OrderByDescending(item => item.Tier);
+            
+            return ToTelegramResults(stickersList, command);
+        }
+
+        /* Возвращает все стикеры системы */
+        private static async Task<IEnumerable<StickerEntity>> GetUnlimitedStickers(string filter)
+        {
+            return (await StickerDao.GetAll())
+                .Where(item => item.Title.Contains(filter));
+        }
+
+        /* Возвращает все стикеры системы */
+        private async Task<IEnumerable<StickerEntity>> GetUserStickers(string filter)
+        {
+            var result = new List<StickerEntity>();
+            foreach (var relation in Stickers.Values.Where(i => i.Count > 0))
+            {
+                var sticker = await StickerDao.GetStickerByHash(relation.StickerId);
+                if (sticker.Title.Contains(filter)) result.Add(sticker);
+            }
+            return result;
+        }
+
+        /* Преобразует список стикеров в список результатов для телеграм */
+        private static IEnumerable<InlineQueryResult> ToTelegramResults(IEnumerable<StickerEntity> list, string command)
         {
             var result = new List<InlineQueryResult>();
-            if (Constants.UNLIMITED_ALL_STICKERS)
-                foreach (var sticker in await StickerDao.GetAll())
-                {
-                    var item = new InlineQueryResultCachedSticker($"unlimited_sticker={sticker.Title}", sticker.Id);
-                    result.Add(item);
-                    if (result.Count > 49) return result;
-                }
-            else
-                foreach (var sticker in Stickers.Values.Where(i => i.Count > 0))
-                {
-                    if (filter != "")
-                    {
-                        var stickerInfo = await StickerDao.GetStickerInfo(sticker.StickerId);
-                        if (!stickerInfo.Title.Contains(filter)) break;
-                    }
-                    var item = new InlineQueryResultCachedSticker($"{command}={sticker.ShortHash}", sticker.StickerId);
-                    result.Add(item);
-                    if (result.Count > 49) return result;
-                }
+            foreach (var item in list)
+            {
+                result.Add(new 
+                    InlineQueryResultCachedSticker(
+                        $"{(Constants.UNLIMITED_ALL_STICKERS ? InlineQueryCommands.unlimited_stickers : "")}{command}={item.Md5Hash}",
+                        item.Id));
+                /* Ограничение Telegram API по количеству результатов в 50 шт. */
+                if (result.Count > 49) return result;
+            }
             return result;
         }
     }

+ 1 - 1
CardCollector/DataBase/Entity/UserStickerRelationEntity.cs

@@ -19,7 +19,7 @@ namespace CardCollector.DataBase.Entity
         /* Количество стикеров данного вида у пользователя */
         [Column("count"), MaxLength(32)] public int Count { get; set; }
         
-        /* MD5 хеш id стикера и пользователя */
+        /* MD5 хеш id стикера */
         [Column("short_hash"), MaxLength(40)] public string ShortHash { get; set; }
     }
 }

+ 5 - 4
CardCollector/DataBase/EntityDao/StickerDao.cs

@@ -12,10 +12,10 @@ namespace CardCollector.DataBase.EntityDao
         /* Таблица stickers в представлении Entity Framework */
         private static readonly DbSet<StickerEntity> Table = CardCollectorDatabase.Instance.Stickers;
         
-        /* Получение информации о стикере по его Id, возвращает Null, если стикера не существует */
-        public static async Task<StickerEntity> GetStickerInfo(string stickerId)
+        /* Получение информации о стикере по его хешу, возвращает Null, если стикера не существует */
+        public static async Task<StickerEntity> GetStickerByHash(string hash)
         {
-            return await Table.FindAsync(stickerId);
+            return await Table.FirstOrDefaultAsync(item => item.Md5Hash == hash);
         }
 
          /* Добавление новго стикера в систему
@@ -35,7 +35,8 @@ namespace CardCollector.DataBase.EntityDao
             {
                 Id = fileId, Title = title, Author = author,
                 IncomeCoins = incomeCoins, IncomeGems = incomeGems,
-                Tier = tier, Emoji = emoji, Description = description
+                Tier = tier, Emoji = emoji, Description = description,
+                Md5Hash = Utilities.CreateMd5(fileId)
             };
             var result = await Table.AddAsync(cash);
             return result.Entity;

+ 12 - 6
CardCollector/DataBase/EntityDao/UserStickerRelationDao.cs

@@ -22,16 +22,22 @@ namespace CardCollector.DataBase.EntityDao
         }
 
         /* Добавляет новое отношение в таблицу */
-        private static async Task<UserStickerRelationEntity> AddNew(long userId, string stickerId, int count)
+        public static async Task<UserStickerRelationEntity> AddNew(UserEntity user, StickerEntity sticker, int count)
         {
-            var cash = new UserStickerRelationEntity
+            if (await Table.FirstOrDefaultAsync(item => item.ShortHash == sticker.Md5Hash) is { } entity)
             {
-                UserId = userId,
-                StickerId = stickerId,
+                entity.Count += count;
+                return entity;
+            }
+            var relation = new UserStickerRelationEntity
+            {
+                UserId = user.Id,
+                StickerId = sticker.Id,
                 Count = count,
-                ShortHash = Utilities.CreateMD5(stickerId + userId)
+                ShortHash = sticker.Md5Hash
             };
-            var result = await Table.AddAsync(cash);
+            var result = await Table.AddAsync(relation);
+            user.Stickers.Add(sticker.Md5Hash, result.Entity);
             return result.Entity;
         }
     }

+ 1 - 1
CardCollector/Resources/Constants.cs

@@ -10,7 +10,7 @@ namespace CardCollector.Resources
         /* Время кэширования результатов @имя_бота команд */
         public const int INLINE_RESULTS_CACHE_TIME = 1;
         /* Включает бесконечные стикеры без наличия их в коллекции */
-        public const bool UNLIMITED_ALL_STICKERS = DEBUG;
+        public static readonly bool UNLIMITED_ALL_STICKERS = DEBUG;
 
 
         /* Уровни привилегий пользователей системы */

+ 90 - 0
CardCollector/Resources/InlineQueryCommands.Designer.cs

@@ -0,0 +1,90 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace CardCollector.Resources {
+    using System;
+    
+    
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class InlineQueryCommands {
+        
+        private static global::System.Resources.ResourceManager resourceMan;
+        
+        private static global::System.Globalization.CultureInfo resourceCulture;
+        
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal InlineQueryCommands() {
+        }
+        
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager {
+            get {
+                if (object.ReferenceEquals(resourceMan, null)) {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CardCollector.Resources.InlineQueryCommands", typeof(InlineQueryCommands).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+        
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture {
+            get {
+                return resourceCulture;
+            }
+            set {
+                resourceCulture = value;
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to select.
+        /// </summary>
+        internal static string select_sticker {
+            get {
+                return ResourceManager.GetString("select_sticker", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to send.
+        /// </summary>
+        internal static string send_sticker {
+            get {
+                return ResourceManager.GetString("send_sticker", resourceCulture);
+            }
+        }
+        
+        /// <summary>
+        ///   Looks up a localized string similar to unlim.
+        /// </summary>
+        internal static string unlimited_stickers {
+            get {
+                return ResourceManager.GetString("unlimited_stickers", resourceCulture);
+            }
+        }
+    }
+}

+ 30 - 0
CardCollector/Resources/InlineQueryCommands.resx

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<root>
+    <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+        <xsd:element name="root" msdata:IsDataSet="true">
+            
+        </xsd:element>
+    </xsd:schema>
+    <resheader name="resmimetype">
+        <value>text/microsoft-resx</value>
+    </resheader>
+    <resheader name="version">
+        <value>1.3</value>
+    </resheader>
+    <resheader name="reader">
+        <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+    </resheader>
+    <resheader name="writer">
+        <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+    </resheader>
+    <data name="send_sticker" xml:space="preserve">
+        <value>send</value>
+    </data>
+    <data name="select_sticker" xml:space="preserve">
+        <value>select</value>
+    </data>
+    <data name="unlimited_stickers" xml:space="preserve">
+        <value>unlim</value>
+    </data>
+</root>

+ 2 - 2
CardCollector/Utilities.cs

@@ -2,14 +2,14 @@
 
 namespace CardCollector
 {
-    public class Utilities
+    public static class Utilities
     {
         public static string ToJson(object obj)
         {
             return Newtonsoft.Json.JsonConvert.SerializeObject(obj);
         }
         
-        public static string CreateMD5(string input)
+        public static string CreateMd5(string input)
         {
             // Use input string to calculate MD5 hash
             using var md5 = System.Security.Cryptography.MD5.Create();