diff --git a/README.md b/README.md index a4ec1b1..903a138 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # .NET Telegram Bot Framework - Context based addon -[![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/vpre/TelegramBotBase.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase/) +[![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/v/TelegramBotBase.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase/) [![Telegram chat](https://img.shields.io/badge/Support_Chat-Telegram-blue.svg?style=flat-square)](https://www.t.me/tgbotbase) [![License](https://img.shields.io/github/license/MajMcCloud/telegrambotframework.svg?style=flat-square&maxAge=2592000&label=License)](https://raw.githubusercontent.com/MajMcCloud/TelegramBotFramework/master/LICENCE.md) @@ -723,6 +723,7 @@ The current available languages for controls are: - English - German - Persian +- Russian You can add other languages easily by creating a subclass of the [TelegramBotBase/Localizations/Localization.cs](TelegramBotBase/Localizations/Localization.cs) class. @@ -1039,6 +1040,8 @@ again at 1 (due to `PopAsync` or `PopToRootAsync` calls) it will replace the con you have given to the constructor at the beginning.* +--- + ## Extensions ### TelegramBotBase.Extensions.Images @@ -1048,7 +1051,28 @@ Extends the base package with some additional image methods like SendPhoto (usin [![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/v/TelegramBotBase.Extensions.Images.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Images/) [![Downloads](https://img.shields.io/nuget/dt/TelegramBotBase.Extensions.Images.svg?style=flat-square&label=Package%20Downloads)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Images) -[https://www.nuget.org/packages/TelegramBotBase.Extensions.Images/](https://www.nuget.org/packages/TelegramBotBase.Extensions.Images/) +Source code: [TelegramBotBase.Extensions.Images/](/TelegramBotBase.Extensions.Images/) + +Nuget package: [https://www.nuget.org/packages/TelegramBotBase.Extensions.Images/](https://www.nuget.org/packages/TelegramBotBase.Extensions.Images/) + +--- + +### TelegramBotBase.Extensions.Images.IronSoftware + +Extends the base package with some additional image methods like SendPhoto (using Bitmap) + +Important: This extension uses the IronSoftware drawing library. + +[![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/v/TelegramBotBase.Extensions.Images.IronSoftware.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Images.IronSoftware/) +[![Downloads](https://img.shields.io/nuget/dt/TelegramBotBase.Extensions.Images.IronSoftware.svg?style=flat-square&label=Package%20Downloads)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Images.IronSoftware) + +[https://www.nuget.org/packages/TelegramBotBase.Extensions.Images.IronSoftware/](https://www.nuget.org/packages/TelegramBotBase.Extensions.Images.IronSoftware/) + +Source code: [TelegramBotBase.Extensions.Images.IronSoftware/](TelegramBotBase.Extensions.Images.IronSoftware/) + +Nuget package: [https://www.nuget.org/packages/TelegramBotBase.Extensions.IronSoftware/](https://www.nuget.org/packages/TelegramBotBase.Extensions.IronSoftware/) + +--- Project: [open source](TelegramBotBase.Extensions.Images/) @@ -1072,7 +1096,26 @@ A session serializer for Microsoft SQL Server. [![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/v/TelegramBotBase.Extensions.Serializer.Database.MSSQL.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.MSSQL/) [![Downloads](https://img.shields.io/nuget/dt/TelegramBotBase.Extensions.Serializer.Database.MSSQL.svg?style=flat-square&label=Package%20Downloads)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.MSSQL) -[https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.MSSQL/](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.MSSQL/) +Source code: [TelegramBotBase.Extensions.Serializer.Database.MSSQL/](/TelegramBotBase.Extensions.Serializer.Database.MSSQL/) + +Nuget package: [https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.MSSQL/](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.MSSQL/) + +--- + +### TelegramBotBase.Extensions.Serializer.Database.PostgreSql + +A session serializer for PostgreSql Server. + +[![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/v/TelegramBotBase.Extensions.Serializer.Database.PostgreSql.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/) +[![Downloads](https://img.shields.io/nuget/dt/TelegramBotBase.Extensions.Serializer.Database.PostgreSql.svg?style=flat-square&label=Package%20Downloads)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.PostgreSql) + +Source code: [TelegramBotBase.Extensions.Serializer.Database.PostgreSql/](/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/) + +Nuget package: [https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/) + +Credits: [@Kataane](https://github.com/Kataane) + +--- Project: [open source](TelegramBotBase.Extensions.Serializer.Database.MSSQL/) diff --git a/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/BotBaseBuilderExtensions.cs b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/BotBaseBuilderExtensions.cs new file mode 100644 index 0000000..7b79d06 --- /dev/null +++ b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/BotBaseBuilderExtensions.cs @@ -0,0 +1,92 @@ +using System; +using TelegramBotBase.Builder; +using TelegramBotBase.Builder.Interfaces; + +namespace TelegramBotBase.Extensions.Serializer.Database.PostgreSql +{ + /// + /// Provides extension methods for configuring the use of PostgreSQL Server Database for session serialization. + /// + public static class BotBaseBuilderExtensions + { + /// + /// Uses an PostgreSQL Server Database to save and restore sessions. + /// + /// The session serialization stage builder. + /// The connection string to the PostgreSQL database. + /// The fallback form type. + /// The prefix for database table names (default is "tgb_"). + /// The language selection stage builder. + public static ILanguageSelectionStage UsePostgreSqlDatabase( + this ISessionSerializationStage builder, + string connectionString, Type fallbackForm = null, + string tablePrefix = "tgb_") + { + var serializer = new PostgreSqlSerializer(connectionString, tablePrefix, fallbackForm); + + builder.UseSerialization(serializer); + + return builder as BotBaseBuilder; + } + + + /// + /// Uses an PostgreSQL Server Database to save and restore sessions. + /// + /// The session serialization stage builder. + /// The host or IP address of the PostgreSQL server. + /// The port number for the PostgreSQL server. + /// The name of the PostgreSQL database. + /// The user ID for connecting to the PostgreSQL server. + /// The password for connecting to the PostgreSQL server. + /// The fallback form type. + /// The prefix for database table names (default is "tgb_"). + /// The language selection stage builder. + public static ILanguageSelectionStage UsePostgreSqlDatabase( + this ISessionSerializationStage builder, + string hostOrIp, int port, + string databaseName, string userId, + string password, Type fallbackForm = null, + string tablePrefix = "tgb_") + { + var connectionString = $"Host={hostOrIp};Port={port};Database={databaseName};Username={userId};Password={password}"; + + var serializer = new PostgreSqlSerializer(connectionString, tablePrefix, fallbackForm); + + builder.UseSerialization(serializer); + + return builder as BotBaseBuilder; + } + + /// + /// Uses an PostgreSQL Server Database with Windows Authentication to save and restore sessions. + /// + /// The session serialization stage builder. + /// The host or IP address of the PostgreSQL server. + /// The port number for the PostgreSQL server. + /// The name of the PostgreSQL database. + /// A flag indicating whether to use Windows Authentication (true) or not (false). + /// The fallback form type. + /// The prefix for database table names (default is "tgb_"). + /// The language selection stage builder. + public static ILanguageSelectionStage UsePostgreSqlDatabase( + this ISessionSerializationStage builder, + string hostOrIp, int port, + string databaseName, bool integratedSecurity = true, + Type fallbackForm = null, string tablePrefix = "tgb_") + { + if (!integratedSecurity) + { + throw new ArgumentOutOfRangeException(); + } + + var connectionString = $"Host={hostOrIp};Port={port};Database={databaseName};Integrated Security=true;"; + + var serializer = new PostgreSqlSerializer(connectionString, tablePrefix, fallbackForm); + + builder.UseSerialization(serializer); + + return builder as BotBaseBuilder; + } + } +} diff --git a/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/PostgreSqlSerializer.cs b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/PostgreSqlSerializer.cs new file mode 100644 index 0000000..7875b70 --- /dev/null +++ b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/PostgreSqlSerializer.cs @@ -0,0 +1,252 @@ +using Npgsql; +using System; +using System.Data; +using NpgsqlTypes; +using TelegramBotBase.Args; +using TelegramBotBase.Base; +using TelegramBotBase.Form; +using TelegramBotBase.Interfaces; + +namespace TelegramBotBase.Extensions.Serializer.Database.PostgreSql +{ + /// + /// Represents a PostgreSQL implementation of the for saving and loading form states. + /// + public class PostgreSqlSerializer : IStateMachine + { + private readonly string insertIntoSessionSql; + private readonly string insertIntoSessionsDataSql; + private readonly string selectAllDevicesSessionsSql; + private readonly string selectAllDevicesSessionsDataSql; + + /// + /// Initializes a new instance of the class. + /// + /// The connection string to the PostgreSQL database. + /// The prefix for database table names (default is "tgb_"). + /// The fallback state form type. + /// Thrown when is null. + /// Thrown when is not a subclass of . + public PostgreSqlSerializer(string connectionString, string tablePrefix = "tgb_", Type fallbackStateForm = null) + { + ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + + TablePrefix = tablePrefix; + + FallbackStateForm = fallbackStateForm; + + if (FallbackStateForm != null && !FallbackStateForm.IsSubclassOf(typeof(FormBase))) + { + throw new ArgumentException($"{nameof(FallbackStateForm)} is not a subclass of {nameof(FormBase)}"); + } + + insertIntoSessionSql = "INSERT INTO " + TablePrefix + + "devices_sessions (deviceId, deviceTitle, \"FormUri\", \"QualifiedName\") VALUES (@deviceId, @deviceTitle, @FormUri, @QualifiedName)"; + insertIntoSessionsDataSql = "INSERT INTO " + TablePrefix + "devices_sessions_data (deviceId, key, value, type) VALUES (@deviceId, @key, @value, @type)"; + + selectAllDevicesSessionsSql = "SELECT * FROM " + TablePrefix + "devices_sessions"; + selectAllDevicesSessionsDataSql = "SELECT * FROM " + TablePrefix + "devices_sessions_data WHERE deviceId = @deviceId"; + } + + /// + /// Gets the connection string to the PostgreSQL database. + /// + public string ConnectionString { get; } + + /// + /// Gets or sets the table name prefix for database tables. + /// + public string TablePrefix { get; set; } + + /// + /// Gets or sets the fallback state form type. + /// + public Type FallbackStateForm { get; set; } + + /// + /// + /// Saves form states to the PostgreSQL database. + /// + /// The containing the states to be saved. + public void SaveFormStates(SaveStatesEventArgs e) + { + var container = e.States; + + //Cleanup old Session data + Cleanup(); + + using (var connection = new NpgsqlConnection(ConnectionString)) + { + connection.Open(); + + //Store session data in database + foreach (var state in container.States) + { + using (var sessionCommand = connection.CreateCommand()) + { + sessionCommand.CommandText = insertIntoSessionSql; + + sessionCommand.Parameters.Add(new NpgsqlParameter("@deviceId", NpgsqlDbType.Bigint){Value = state.DeviceId }); + sessionCommand.Parameters.Add(new NpgsqlParameter("@deviceTitle", DbType.StringFixedLength){Value = state.ChatTitle ?? string.Empty}); + sessionCommand.Parameters.Add(new NpgsqlParameter("@FormUri", DbType.StringFixedLength) {Value = state.FormUri}); + sessionCommand.Parameters.Add(new NpgsqlParameter("@QualifiedName", DbType.StringFixedLength){Value = state.QualifiedName }); + + sessionCommand.ExecuteNonQuery(); + } + } + } + + using (var connection = new NpgsqlConnection(ConnectionString)) + { + connection.Open(); + + foreach (var state in container.States) + { + SaveSessionsData(state, connection); + } + } + } + + /// + /// + /// Loads form states from the PostgreSQL database. + /// + /// A containing the loaded form states. + public StateContainer LoadFormStates() + { + var stateContainer = new StateContainer(); + + using (var connection = new NpgsqlConnection(ConnectionString)) + { + connection.Open(); + + using (var sessionCommand = connection.CreateCommand()) + { + sessionCommand.CommandText = selectAllDevicesSessionsSql; + + var sessionTable = new DataTable(); + using (var dataAdapter = new NpgsqlDataAdapter(sessionCommand)) + { + dataAdapter.Fill(sessionTable); + + foreach (DataRow row in sessionTable.Rows) + { + var stateEntry = new StateEntry + { + DeviceId = (long)row["deviceId"], + ChatTitle = row["deviceTitle"].ToString(), + FormUri = row["FormUri"].ToString(), + QualifiedName = row["QualifiedName"].ToString() + }; + + stateContainer.States.Add(stateEntry); + + if (stateEntry.DeviceId > 0) + { + stateContainer.ChatIds.Add(stateEntry.DeviceId); + } + else + { + stateContainer.GroupIds.Add(stateEntry.DeviceId); + } + + LoadDataTable(connection, row, stateEntry); + } + } + } + } + + return stateContainer; + } + + /// + /// Cleans up old session data in the PostgreSQL database. + /// + private void Cleanup() + { + using (var connection = new NpgsqlConnection(ConnectionString)) + { + connection.Open(); + + using (var clearCommand = connection.CreateCommand()) + { + clearCommand.CommandText = $"DELETE FROM {TablePrefix}devices_sessions_data"; + clearCommand.ExecuteNonQuery(); + } + + using (var clearCommand = connection.CreateCommand()) + { + clearCommand.CommandText = $"DELETE FROM {TablePrefix}devices_sessions"; + clearCommand.ExecuteNonQuery(); + } + } + } + + /// + /// Saves session data to the PostgreSQL database. + /// + /// The state entry containing session data to be saved. + /// The NpgsqlConnection used for the database interaction. + private void SaveSessionsData(StateEntry state, NpgsqlConnection connection) + { + foreach (var data in state.Values) + { + using (var dataCommand = connection.CreateCommand()) + { + dataCommand.CommandText = insertIntoSessionsDataSql; + + dataCommand.Parameters.Add(new NpgsqlParameter("@deviceId", NpgsqlDbType.Bigint) { Value = state.DeviceId }); + dataCommand.Parameters.Add(new NpgsqlParameter("@key", DbType.StringFixedLength) { Value = data.Key }); + + var type = data.Value.GetType(); + + if (type.IsPrimitive || type == typeof(string)) + { + dataCommand.Parameters.Add(new NpgsqlParameter("@value", NpgsqlDbType.Text) { Value = data.Value }); + } + else + { + var json = System.Text.Json.JsonSerializer.Serialize(data.Value); + dataCommand.Parameters.Add(new NpgsqlParameter("@value", NpgsqlDbType.Text) { Value = json }); + } + + dataCommand.Parameters.Add(new NpgsqlParameter("@type", DbType.StringFixedLength) { Value = type.AssemblyQualifiedName }); + + dataCommand.ExecuteNonQuery(); + } + } + } + + /// + /// Loads session data from the PostgreSQL database. + /// + /// The NpgsqlConnection used for the database interaction. + /// The DataRow representing a session entry in the main sessions table. + /// The StateEntry object to which session data will be loaded. + private void LoadDataTable(NpgsqlConnection connection, DataRow row, StateEntry stateEntry) + { + using (var sessionCommand = connection.CreateCommand()) + { + sessionCommand.CommandText = selectAllDevicesSessionsDataSql; + sessionCommand.Parameters.Add(new NpgsqlParameter("@deviceId", row["deviceId"])); + + var dataCommandTable = new DataTable(); + using (var npgSqlDataAdapter = new NpgsqlDataAdapter(sessionCommand)) + { + npgSqlDataAdapter.Fill(dataCommandTable); + + foreach (DataRow dataRow in dataCommandTable.Rows) + { + var key = dataRow["key"].ToString(); + var type = Type.GetType(dataRow["type"].ToString()); + + var value = System.Text.Json.JsonSerializer.Deserialize(dataRow["value"].ToString(), type); + + stateEntry.Values.Add(key, value); + } + } + + } + } + } +} \ No newline at end of file diff --git a/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/README.md b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/README.md new file mode 100644 index 0000000..fb985d8 --- /dev/null +++ b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/README.md @@ -0,0 +1,27 @@ +# TelegramBotBase.Extensions.Serializer.Database.PostgreSQL + +[![NuGet version (TelegramBotBase)](https://img.shields.io/nuget/v/TelegramBotBase.Extensions.Serializer.Database.PostgreSQL.svg?style=flat-square)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.PostgreSQL/) +[![Telegram chat](https://img.shields.io/badge/Support_Chat-Telegram-blue.svg?style=flat-square)](https://www.t.me/tgbotbase) + +[![License](https://img.shields.io/github/license/MajMcCloud/telegrambotframework.svg?style=flat-square&maxAge=2592000&label=License)](https://raw.githubusercontent.com/MajMcCloud/TelegramBotFramework/master/LICENCE.md) +[![Package Downloads](https://img.shields.io/nuget/dt/TelegramBotBase.Extensions.Serializer.Database.PostgreSQL.svg?style=flat-square&label=Package%20Downloads)](https://www.nuget.org/packages/TelegramBotBase.Extensions.Serializer.Database.PostgreSQL) + +## How to use + +```csharp +using TelegramBotBase.Extensions.Serializer.Database.PostgreSQL; + + +var bot = BotBaseBuilder + .Create() + .WithAPIKey(APIKey) + .DefaultMessageLoop() + .WithStartForm() + .NoProxy() + .OnlyStart() + .UsePostgreSqlDatabase("localhost", "8181", "telegram_bot") + .UseEnglish() + .Build(); + +bot.Start(); +``` diff --git a/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/TelegramBotBase.Extensions.Serializer.Database.PostgreSql.csproj b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/TelegramBotBase.Extensions.Serializer.Database.PostgreSql.csproj new file mode 100644 index 0000000..ce2b6ef --- /dev/null +++ b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/TelegramBotBase.Extensions.Serializer.Database.PostgreSql.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0;netcoreapp3.1;net6 + True + https://github.com/MajMcCloud/TelegramBotFramework + https://github.com/MajMcCloud/TelegramBotFramework + MIT + true + snupkg + 1.0.1 + 1.0.1 + 1.0.1 + + A session serializer for PostgreSQL Server. + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/create_tables.sql b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/create_tables.sql new file mode 100644 index 0000000..d83bd38 --- /dev/null +++ b/TelegramBotBase.Extensions.Serializer.Database.PostgreSql/create_tables.sql @@ -0,0 +1,25 @@ +-- Enable uuid-ossp extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Create table tgb_devices_sessions +CREATE TABLE tgb_devices_sessions ( + deviceId bigint NOT NULL, + deviceTitle character varying(512) NOT NULL, + "FormUri" character varying(512) NOT NULL, + "QualifiedName" character varying(512) NOT NULL, + CONSTRAINT PK_tgb_devices_sessions_1 PRIMARY KEY (deviceId) +); + +-- Create table tgb_devices_sessions_data +CREATE TABLE tgb_devices_sessions_data ( + Id uuid DEFAULT uuid_generate_v4() NOT NULL, + deviceId bigint NOT NULL, + key character varying(512) NOT NULL, + "value" text NOT NULL, + "type" character varying(512) NOT NULL, + CONSTRAINT PK_tgb_devices_session_data PRIMARY KEY (Id) +); + +-- Add default constraint for Id column in tgb_devices_sessions_data +ALTER TABLE tgb_devices_sessions_data + ALTER COLUMN Id SET DEFAULT uuid_generate_v4(); diff --git a/TelegramBotBase/Builder/BotBaseBuilder.cs b/TelegramBotBase/Builder/BotBaseBuilder.cs index 6dd7462..64596b9 100644 --- a/TelegramBotBase/Builder/BotBaseBuilder.cs +++ b/TelegramBotBase/Builder/BotBaseBuilder.cs @@ -398,30 +398,42 @@ public class BotBaseBuilder : IAPIKeySelectionStage, IMessageLoopSelectionStage, #region "Step 7 (Language)" - public IThreadingStage DefaultLanguage() + /// + public IBuildingStage DefaultLanguage() { return this; } - public IThreadingStage UseEnglish() + /// + public IBuildingStage UseEnglish() { Default.Language = new English(); return this; } - public IThreadingStage UseGerman() + /// + public IBuildingStage UseGerman() { Default.Language = new German(); return this; } - public IThreadingStage UsePersian() + /// + public IBuildingStage UsePersian() { Default.Language = new Persian(); return this; } - public IThreadingStage Custom(Localization language) + /// + public IBuildingStage UseRussian() + { + Default.Language = new Russian(); + return this; + } + + /// + public IBuildingStage Custom(Localization language) { Default.Language = language; return this; diff --git a/TelegramBotBase/Builder/Interfaces/ILanguageSelectionStage.cs b/TelegramBotBase/Builder/Interfaces/ILanguageSelectionStage.cs index 1a43a52..b06addd 100644 --- a/TelegramBotBase/Builder/Interfaces/ILanguageSelectionStage.cs +++ b/TelegramBotBase/Builder/Interfaces/ILanguageSelectionStage.cs @@ -2,35 +2,44 @@ namespace TelegramBotBase.Builder.Interfaces; +/// +/// Represents the language selection stage in the localization process. +/// public interface ILanguageSelectionStage { /// /// Selects the default language for control usage. (English) /// - /// - IThreadingStage DefaultLanguage(); + /// The next stage in the building process. + IBuildingStage DefaultLanguage(); /// /// Selects english as the default language for control labels. /// - /// - IThreadingStage UseEnglish(); + /// The next stage in the building process. + IBuildingStage UseEnglish(); /// /// Selects german as the default language for control labels. /// - /// - IThreadingStage UseGerman(); + /// The next stage in the building process. + IBuildingStage UseGerman(); /// /// Selects persian as the default language for control labels. /// - /// - IThreadingStage UsePersian(); + /// The next stage in the building process. + IBuildingStage UsePersian(); + + /// + /// Selects russian as the default language for control labels. + /// + /// The next stage in the building process. + IBuildingStage UseRussian(); /// /// Selects a custom language as the default language for control labels. /// - /// - IThreadingStage Custom(Localization language); + /// The next stage in the building process. + IBuildingStage Custom(Localization language); } \ No newline at end of file diff --git a/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs b/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs index ca0a895..bc06b06 100644 --- a/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs +++ b/TelegramBotBase/Controls/Hybrid/ButtonGrid.cs @@ -426,8 +426,6 @@ public class ButtonGrid : ControlBase return; } - await result.ConfirmAction(ConfirmationText ?? ""); - ButtonRow match = null; var index = -1; @@ -462,6 +460,8 @@ public class ButtonGrid : ControlBase check: if (match != null) { + await result.ConfirmAction(ConfirmationText ?? ""); + await OnButtonClicked(new ButtonClickedEventArgs(match.GetButtonMatch(result.RawData, false), index, match)); diff --git a/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs b/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs index c2fec7e..94266b7 100644 --- a/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs +++ b/TelegramBotBase/Controls/Hybrid/TaggedButtonGrid.cs @@ -29,8 +29,6 @@ public class TaggedButtonGrid : MultiView public string BackLabel = Default.Language["ButtonGrid_Back"]; - public string CheckAllLabel = Default.Language["ButtonGrid_CheckAll"]; - public string NextPageLabel = Default.Language["ButtonGrid_NextPage"]; public string NoItemsLabel = Default.Language["ButtonGrid_NoItems"]; @@ -39,11 +37,17 @@ public class TaggedButtonGrid : MultiView public string SearchLabel = Default.Language["ButtonGrid_SearchFeature"]; - public string UncheckAllLabel = Default.Language["ButtonGrid_UncheckAll"]; + public string TotalTagsLabel = Default.Language["TaggedButtonGrid_TotalTags"]; + + public string CheckedTagsLabel = Default.Language["TaggedButtonGrid_CheckedTags"]; + + public string CheckAllLabel = Default.Language["TaggedButtonGrid_CheckAll"]; + + public string UncheckAllLabel = Default.Language["TaggedButtonGrid_UncheckAll"]; public string SearchIcon = Default.Language["ButtonGrid_SearchIcon"]; - public string TagIcon = Default.Language["ButtonGrid_TagIcon"]; + public string TagIcon = Default.Language["TaggedButtonGrid_TagIcon"]; public TaggedButtonGrid() { @@ -379,14 +383,6 @@ public class TaggedButtonGrid : MultiView index = br.Item2; } - - //var button = HeadLayoutButtonRow?. .FirstOrDefault(a => a.Text.Trim() == result.MessageText) - // ?? SubHeadLayoutButtonRow?.FirstOrDefault(a => a.Text.Trim() == result.MessageText); - - // bf.ToList().FirstOrDefault(a => a.Text.Trim() == result.MessageText) - - //var index = bf.FindRowByButton(button); - check: @@ -490,10 +486,16 @@ public class TaggedButtonGrid : MultiView if (result.MessageText == CheckAllLabel) { CheckAllTags(); + Updated(); + result.Handled = true; + return; } else if (result.MessageText == UncheckAllLabel) { UncheckAllTags(); + Updated(); + result.Handled = true; + return; } var i = result.MessageText.LastIndexOf(" "); @@ -569,15 +571,6 @@ public class TaggedButtonGrid : MultiView index = br.Item2; } - - //var bf = DataSource.ButtonForm; - - //var button = HeadLayoutButtonRow?.FirstOrDefault(a => a.Value == result.RawData) - // ?? SubHeadLayoutButtonRow?.FirstOrDefault(a => a.Value == result.RawData) - // ?? bf.ToList().FirstOrDefault(a => a.Value == result.RawData); - - //var index = bf.FindRowByButton(button); - check: if (match != null) { @@ -619,22 +612,47 @@ public class TaggedButtonGrid : MultiView break; - case "$back$": - SelectedViewIndex = 0; + default: + + if (SelectedViewIndex != 1) + return; + + + switch (result.RawData) + { + case "$back$": + + SelectedViewIndex = 0; + Updated(); + + return; + + case "$checkall$": + + CheckAllTags(); + + return; + + case "$uncheckall$": + + UncheckAllTags(); + + return; + + } + + if (SelectedTags.Contains(result.RawData)) + { + SelectedTags.Remove(result.RawData); + } + else + { + SelectedTags.Add(result.RawData); + } + Updated(); - break; - - case "$checkall$": - - CheckAllTags(); - - break; - - case "$uncheckall$": - - UncheckAllTags(); break; } @@ -723,8 +741,10 @@ public class TaggedButtonGrid : MultiView if (EnableCheckAllTools) { - TagsSubHeadLayoutButtonRow = new ButtonRow(new ButtonBase(CheckAllLabel, "$checkall$"), - new ButtonBase(UncheckAllLabel, "$uncheckall$")); + TagsSubHeadLayoutButtonRow = new ButtonRow(new ButtonBase(string.Format(TotalTagsLabel, Tags.Count), "$total$"), + new ButtonBase(CheckAllLabel, "$checkall$"), + new ButtonBase(UncheckAllLabel, "$uncheckall$"), + new ButtonBase(string.Format(CheckedTagsLabel, SelectedTags.Count), "checked$")); bf.AddButtonRow(TagsSubHeadLayoutButtonRow); } @@ -756,10 +776,6 @@ public class TaggedButtonGrid : MultiView return; } - //if (bf.Count == 0) - // return; - - var rkm = (ReplyKeyboardMarkup)bf; rkm.ResizeKeyboard = ResizeKeyboard; rkm.OneTimeKeyboard = OneTimeKeyboard; @@ -881,21 +897,20 @@ public class TaggedButtonGrid : MultiView { Message m = null; - var form = DataSource.PickItems(CurrentPageIndex * ItemRowsPerPage, ItemRowsPerPage, - EnableSearch ? SearchQuery : null); - - //if (this.EnableSearch && this.SearchQuery != null && this.SearchQuery != "") - //{ - // form = form.FilterDuplicate(this.SearchQuery, true); - //} - //else - //{ - // form = form.Duplicate(); - //} + ButtonForm form = null; if (Tags != null && SelectedTags != null) { + form = DataSource.PickAllItems(EnableSearch ? SearchQuery : null); //CurrentPageIndex * ItemRowsPerPage, ItemRowsPerPage, + form = form.TagDuplicate(SelectedTags); + + form = new ButtonForm(form.ToRowList().Skip(CurrentPageIndex * ItemRowsPerPage).Take(ItemRowsPerPage)); + } + else + { + form = DataSource.PickItems(CurrentPageIndex * ItemRowsPerPage, ItemRowsPerPage, + EnableSearch ? SearchQuery : null); } if (EnablePaging) diff --git a/TelegramBotBase/Form/ButtonForm.cs b/TelegramBotBase/Form/ButtonForm.cs index 9591ddf..c5f745a 100644 --- a/TelegramBotBase/Form/ButtonForm.cs +++ b/TelegramBotBase/Form/ButtonForm.cs @@ -23,6 +23,11 @@ public class ButtonForm DependencyControl = control; } + public ButtonForm(IEnumerable rows) + { + _buttons = rows.ToList(); + } + public IReplyMarkup Markup { get; set; } @@ -149,6 +154,11 @@ public class ButtonForm .Aggregate((a, b) => a.Union(b).ToList()); } + public List ToRowList() + { + return _buttons; + } + public InlineKeyboardButton[][] ToInlineButtonArray() { var ikb = _buttons.Select(a => a.ToArray().Select(b => b.ToInlineButton(this)).ToArray()).ToArray(); diff --git a/TelegramBotBase/Localizations/English.cs b/TelegramBotBase/Localizations/English.cs index d69bbe2..974143b 100644 --- a/TelegramBotBase/Localizations/English.cs +++ b/TelegramBotBase/Localizations/English.cs @@ -12,8 +12,10 @@ public sealed class English : Localization Values["ButtonGrid_CurrentPage"] = "Page {0} of {1}"; Values["ButtonGrid_SearchFeature"] = "💡 Send a message to filter the list. Click the 🔍 to reset the filter."; Values["ButtonGrid_Back"] = "Back"; - Values["ButtonGrid_CheckAll"] = "Check all"; - Values["ButtonGrid_UncheckAll"] = "Uncheck all"; + Values["TaggedButtonGrid_TotalTags"] = "Total: {0}"; + Values["TaggedButtonGrid_CheckedTags"] = "Checked: {0}"; + Values["TaggedButtonGrid_CheckAll"] = "Check all"; + Values["TaggedButtonGrid_UncheckAll"] = "Uncheck all"; Values["CalendarPicker_Title"] = "Pick date"; Values["CalendarPicker_PreviousPage"] = "◀️"; Values["CalendarPicker_NextPage"] = "▶️"; @@ -31,6 +33,6 @@ public sealed class English : Localization Values["PromptDialog_Back"] = "Back"; Values["ToggleButton_Changed"] = "Setting changed"; Values["ButtonGrid_SearchIcon"] = "🔍"; - Values["ButtonGrid_TagIcon"] = "📁"; + Values["TaggedButtonGrid_TagIcon"] = "📁"; } } diff --git a/TelegramBotBase/Localizations/German.cs b/TelegramBotBase/Localizations/German.cs index 6380635..ffe99c4 100644 --- a/TelegramBotBase/Localizations/German.cs +++ b/TelegramBotBase/Localizations/German.cs @@ -10,11 +10,12 @@ public sealed class German : Localization Values["ButtonGrid_PreviousPage"] = "◀️"; Values["ButtonGrid_NextPage"] = "▶️"; Values["ButtonGrid_CurrentPage"] = "Seite {0} von {1}"; - Values["ButtonGrid_SearchFeature"] = - "💡 Sende eine Nachricht um die Liste zu filtern. Klicke die 🔍 um den Filter zurückzusetzen."; + Values["ButtonGrid_SearchFeature"] = "💡 Sende eine Nachricht um die Liste zu filtern. Klicke die 🔍 um den Filter zurückzusetzen."; Values["ButtonGrid_Back"] = "Zurück"; - Values["ButtonGrid_CheckAll"] = "Alle auswählen"; - Values["ButtonGrid_UncheckAll"] = "Keine auswählen"; + Values["TaggedButtonGrid_TotalTags"] = "Gesamt: {0}"; + Values["TaggedButtonGrid_CheckedTags"] = "Ausgewählt: {0}"; + Values["TaggedButtonGrid_CheckAll"] = "Alle auswählen"; + Values["TaggedButtonGrid_UncheckAll"] = "Keine auswählen"; Values["CalendarPicker_Title"] = "Datum auswählen"; Values["CalendarPicker_PreviousPage"] = "◀️"; Values["CalendarPicker_NextPage"] = "▶️"; @@ -31,5 +32,7 @@ public sealed class German : Localization Values["MultiToggleButton_Changed"] = "Ausgewählt"; Values["PromptDialog_Back"] = "Zurück"; Values["ToggleButton_Changed"] = "Einstellung geändert"; + Values["ButtonGrid_SearchIcon"] = "🔍"; + Values["TaggedButtonGrid_TagIcon"] = "📁"; } } \ No newline at end of file diff --git a/TelegramBotBase/Localizations/Persian.cs b/TelegramBotBase/Localizations/Persian.cs index 919e355..72c6b40 100644 --- a/TelegramBotBase/Localizations/Persian.cs +++ b/TelegramBotBase/Localizations/Persian.cs @@ -1,37 +1,39 @@ -namespace TelegramBotBase.Localizations +namespace TelegramBotBase.Localizations; + +public sealed class Persian : Localization { - public sealed class Persian : Localization + public Persian() { - public Persian() - { - Values["Language"] = "فارسی"; - Values["ButtonGrid_Title"] = "منو"; - Values["ButtonGrid_NoItems"] = "هیچ آیتمی وجود ندارد."; - Values["ButtonGrid_PreviousPage"] = "◀️"; - Values["ButtonGrid_NextPage"] = "▶️"; - Values["ButtonGrid_CurrentPage"] = "صفحه ی {0} از {1}"; - Values["ButtonGrid_SearchFeature"] = "💡 برای فیلتر کردن لیست پیام ارسال کنید. برای بازنشانی فیلتر روی 🔍 کلیک کنید."; - Values["ButtonGrid_Back"] = "بازگشت"; - Values["ButtonGrid_CheckAll"] = "بررسی کردن همه"; - Values["ButtonGrid_UncheckAll"] = "بررسی نکردن همه"; - Values["CalendarPicker_Title"] = "تاریخ را انتخاب کنید"; - Values["CalendarPicker_PreviousPage"] = "◀️"; - Values["CalendarPicker_NextPage"] = "▶️"; - Values["TreeView_Title"] = "گره را انتخاب کنید"; - Values["TreeView_LevelUp"] = "🔼 سطح بالا"; - Values["ToggleButton_On"] = "روشن"; - Values["ToggleButton_Off"] = "خاموش"; - Values["ToggleButton_OnIcon"] = "⚫"; - Values["ToggleButton_OffIcon"] = "⚪"; - Values["ToggleButton_Title"] = "تغییر وضعیت"; - Values["ToggleButton_Changed"] = "انتخاب شده"; - Values["MultiToggleButton_SelectedIcon"] = "✅"; - Values["MultiToggleButton_Title"] = "چند تعویض"; - Values["MultiToggleButton_Changed"] = "انتخاب شده"; - Values["PromptDialog_Back"] = "بازگشت"; - Values["ToggleButton_Changed"] = "تنظیمات تغییر کرد"; - Values["ButtonGrid_SearchIcon"] = "🔍"; - Values["ButtonGrid_TagIcon"] = "📁"; - } + Values["Language"] = "فارسی"; + Values["ButtonGrid_Title"] = "منو"; + Values["ButtonGrid_NoItems"] = "هیچ آیتمی وجود ندارد."; + Values["ButtonGrid_PreviousPage"] = "◀️"; + Values["ButtonGrid_NextPage"] = "▶️"; + Values["ButtonGrid_CurrentPage"] = "صفحه ی {0} از {1}"; + Values["ButtonGrid_SearchFeature"] = "💡 برای فیلتر کردن لیست پیام ارسال کنید. برای بازنشانی فیلتر روی 🔍 کلیک کنید."; + Values["ButtonGrid_Back"] = "بازگشت"; + Values["TaggedButtonGrid_TotalTags"] = "Total: {0}"; + Values["TaggedButtonGrid_CheckedTags"] = "Checked: {0}"; + Values["TaggedButtonGrid_CheckAll"] = "بررسی کردن همه"; + Values["TaggedButtonGrid_UncheckAll"] = "بررسی نکردن همه"; + Values["CalendarPicker_Title"] = "تاریخ را انتخاب کنید"; + Values["CalendarPicker_PreviousPage"] = "◀️"; + Values["CalendarPicker_NextPage"] = "▶️"; + Values["TreeView_Title"] = "گره را انتخاب کنید"; + Values["TreeView_LevelUp"] = "🔼 سطح بالا"; + Values["ToggleButton_On"] = "روشن"; + Values["ToggleButton_Off"] = "خاموش"; + Values["ToggleButton_OnIcon"] = "⚫"; + Values["ToggleButton_OffIcon"] = "⚪"; + Values["ToggleButton_Title"] = "تغییر وضعیت"; + Values["ToggleButton_Changed"] = "انتخاب شده"; + Values["MultiToggleButton_SelectedIcon"] = "✅"; + Values["MultiToggleButton_Title"] = "چند تعویض"; + Values["MultiToggleButton_Changed"] = "انتخاب شده"; + Values["PromptDialog_Back"] = "بازگشت"; + Values["ToggleButton_Changed"] = "تنظیمات تغییر کرد"; + Values["ButtonGrid_SearchIcon"] = "🔍"; + Values["TaggedButtonGrid_TagIcon"] = "📁"; } } + diff --git a/TelegramBotBase/Localizations/Russian.cs b/TelegramBotBase/Localizations/Russian.cs new file mode 100644 index 0000000..08315e6 --- /dev/null +++ b/TelegramBotBase/Localizations/Russian.cs @@ -0,0 +1,38 @@ +namespace TelegramBotBase.Localizations; + +public sealed class Russian : Localization +{ + public Russian() + { + Values["Language"] = "Русский (Russian)"; + Values["ButtonGrid_Title"] = "Меню"; + Values["ButtonGrid_NoItems"] = "Нет доступных элементов."; + Values["ButtonGrid_PreviousPage"] = "◀️"; + Values["ButtonGrid_NextPage"] = "▶️"; + Values["ButtonGrid_CurrentPage"] = "Страница {0} из {1}"; + Values["ButtonGrid_SearchFeature"] = "💡 Отправьте сообщение, чтобы отфильтровать список. Нажмите на 🔍, чтобы сбросить фильтр."; + Values["ButtonGrid_Back"] = "Назад"; + Values["TaggedButtonGrid_TotalTags"] = "Всего: {0}"; + Values["TaggedButtonGrid_CheckedTags"] = "Отмечено: {0}"; + Values["TaggedButtonGrid_CheckAll"] = "Выделить все"; + Values["TaggedButtonGrid_UncheckAll"] = "Отменить выбор"; + Values["CalendarPicker_Title"] = "Календарь / Выберите дату"; + Values["CalendarPicker_PreviousPage"] = "◀️"; + Values["CalendarPicker_NextPage"] = "▶️"; + Values["TreeView_Title"] = "Выберите пункт"; + Values["TreeView_LevelUp"] = "🔼 Обратно"; + Values["ToggleButton_On"] = "Вкл"; + Values["ToggleButton_Off"] = "Выкл"; + Values["ToggleButton_OnIcon"] = "⚫"; + Values["ToggleButton_OffIcon"] = "⚪"; + Values["ToggleButton_Title"] = "Переключить"; + Values["ToggleButton_Changed"] = "Выбрано"; + Values["MultiToggleButton_SelectedIcon"] = "✅"; + Values["MultiToggleButton_Title"] = "Множественный выбор"; + Values["MultiToggleButton_Changed"] = "Выбрано"; + Values["PromptDialog_Back"] = "Назад"; + Values["ToggleButton_Changed"] = "Настройки изменены"; + Values["ButtonGrid_SearchIcon"] = "🔍"; + Values["TaggedButtonGrid_TagIcon"] = "📁"; + } +} \ No newline at end of file diff --git a/TelegramBotFramework.sln b/TelegramBotFramework.sln index 84dbf82..409d950 100644 --- a/TelegramBotFramework.sln +++ b/TelegramBotFramework.sln @@ -34,6 +34,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InlineAndReplyCombination", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection", "Examples\DependencyInjection\DependencyInjection.csproj", "{689B16BC-200E-4C68-BB2E-8B209070849B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelegramBotBase.Extensions.Serializer.Database.PostgreSql", "TelegramBotBase.Extensions.Serializer.Database.PostgreSql\TelegramBotBase.Extensions.Serializer.Database.PostgreSql.csproj", "{7C55D9FF-7DC1-41D0-809C-469EBFA20992}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelegramBotBase.Extensions.Images.IronSoftware", "TelegramBotBase.Extensions.Images.IronSoftware\TelegramBotBase.Extensions.Images.IronSoftware.csproj", "{DC521A4C-7446-46F7-845B-AAF10EDCF8C6}" EndProject Global @@ -86,6 +88,10 @@ Global {689B16BC-200E-4C68-BB2E-8B209070849B}.Debug|Any CPU.Build.0 = Debug|Any CPU {689B16BC-200E-4C68-BB2E-8B209070849B}.Release|Any CPU.ActiveCfg = Release|Any CPU {689B16BC-200E-4C68-BB2E-8B209070849B}.Release|Any CPU.Build.0 = Release|Any CPU + {7C55D9FF-7DC1-41D0-809C-469EBFA20992}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C55D9FF-7DC1-41D0-809C-469EBFA20992}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C55D9FF-7DC1-41D0-809C-469EBFA20992}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C55D9FF-7DC1-41D0-809C-469EBFA20992}.Release|Any CPU.Build.0 = Release|Any CPU {DC521A4C-7446-46F7-845B-AAF10EDCF8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC521A4C-7446-46F7-845B-AAF10EDCF8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC521A4C-7446-46F7-845B-AAF10EDCF8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -104,6 +110,7 @@ Global {52EA3201-02E8-46F5-87C4-B4752C8A815C} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} {067E8EBE-F90A-4AFF-A0FF-20578216486E} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} {689B16BC-200E-4C68-BB2E-8B209070849B} = {BFA71E3F-31C0-4FC1-A320-4DCF704768C5} + {7C55D9FF-7DC1-41D0-809C-469EBFA20992} = {E3193182-6FDA-4FA3-AD26-A487291E7681} {DC521A4C-7446-46F7-845B-AAF10EDCF8C6} = {E3193182-6FDA-4FA3-AD26-A487291E7681} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution