diff --git a/Database/BaseDBMigration.cs b/Database/BaseDBMigration.cs index 7f981fc..88cf9cd 100644 --- a/Database/BaseDBMigration.cs +++ b/Database/BaseDBMigration.cs @@ -117,6 +117,22 @@ PRIMARY KEY (UserId, ContactId) .WithColumn("TargetType").AsString(255).NotNullable() .WithColumn("TargetValue").AsString(255).NotNullable(); } + + // UserStates + if (!Schema.Table("UserStates").Exists()) + { + Execute.Sql(@" + CREATE TABLE IF NOT EXISTS UserStates ( + ChatId INTEGER PRIMARY KEY, + StateName TEXT NOT NULL, + StateDataJson TEXT NOT NULL DEFAULT '{}', + CreatedAt TEXT NOT NULL DEFAULT (datetime('now')), + ExpiresAt TEXT NOT NULL + )"); + + Execute.Sql(@" + CREATE INDEX IF NOT EXISTS IX_UserStates_ExpiresAt ON UserStates(ExpiresAt)"); + } } protected abstract void CreateSpecificConstraints(); diff --git a/Database/DBInit.cs b/Database/DBInit.cs index 1a2082f..c2babb9 100644 --- a/Database/DBInit.cs +++ b/Database/DBInit.cs @@ -101,6 +101,10 @@ public static WebApplicationBuilder CreateBuilderByDBType(string[] args) new SqliteGroupGetter(Config.sqlConnectionString!)); builder.Services.AddSingleton(_ => new SqliteGroupSetter(Config.sqlConnectionString!)); + + builder.Services.AddSingleton(_ => + new SqliteUserStateRepository(Config.sqlConnectionString!)); + return builder; } } \ No newline at end of file diff --git a/Database/Interfaces/IUserStateRepository.cs b/Database/Interfaces/IUserStateRepository.cs new file mode 100644 index 0000000..d4c0c1d --- /dev/null +++ b/Database/Interfaces/IUserStateRepository.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2024-2025 ZenonEl +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Эта программа является свободным программным обеспечением: вы можете распространять и/или изменять +// её на условиях Стандартной общественной лицензии GNU Affero, опубликованной +// Фондом свободного программного обеспечения, либо версии 3 лицензии, либо +// (по вашему выбору) любой более поздней версии. + +namespace TelegramMediaRelayBot.Database.Interfaces; + +public interface IUserStateRepository +{ + Task SaveStateAsync(long chatId, string stateName, string stateDataJson, DateTime expiresAt); + Task<(string StateName, string StateDataJson)?> GetStateAsync(long chatId); + Task RemoveStateAsync(long chatId); + Task CleanupExpiredAsync(); +} diff --git a/Database/Repositories/SqLite/SQLiteDBMigration.cs b/Database/Repositories/SqLite/SQLiteDBMigration.cs index 48967bf..345efcb 100644 --- a/Database/Repositories/SqLite/SQLiteDBMigration.cs +++ b/Database/Repositories/SqLite/SQLiteDBMigration.cs @@ -80,5 +80,8 @@ protected override void CreateSpecificConstraints() Create.Index("IX_PrivacySettings_UserId") .OnTable("PrivacySettings") .OnColumn("UserId"); + + // ========== Индексы для UserStates ========== + // IX_UserStates_ExpiresAt is created inline via Execute.Sql in BaseDBMigration } } diff --git a/Database/Repositories/SqLite/UserStateRepository.cs b/Database/Repositories/SqLite/UserStateRepository.cs new file mode 100644 index 0000000..e91a461 --- /dev/null +++ b/Database/Repositories/SqLite/UserStateRepository.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2024-2025 ZenonEl +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Эта программа является свободным программным обеспечением: вы можете распространять и/или изменять +// её на условиях Стандартной общественной лицензии GNU Affero, опубликованной +// Фондом свободного программного обеспечения, либо версии 3 лицензии, либо +// (по вашему выбору) любой более поздней версии. + +using Dapper; +using Microsoft.Data.Sqlite; +using TelegramMediaRelayBot.Database.Interfaces; + +namespace TelegramMediaRelayBot.Database.Repositories.Sqlite; + +public class SqliteUserStateRepository(string connectionString) : IUserStateRepository +{ + private readonly string _connectionString = connectionString; + + public async Task SaveStateAsync(long chatId, string stateName, string stateDataJson, DateTime expiresAt) + { + const string query = @" + INSERT INTO UserStates (ChatId, StateName, StateDataJson, ExpiresAt) + VALUES (@chatId, @stateName, @stateDataJson, @expiresAt) + ON CONFLICT(ChatId) DO UPDATE SET + StateName = @stateName, + StateDataJson = @stateDataJson, + CreatedAt = datetime('now'), + ExpiresAt = @expiresAt"; + + using var connection = new SqliteConnection(_connectionString); + await connection.ExecuteAsync(query, new + { + chatId, + stateName, + stateDataJson, + expiresAt = expiresAt.ToString("yyyy-MM-dd HH:mm:ss") + }); + } + + public async Task<(string StateName, string StateDataJson)?> GetStateAsync(long chatId) + { + const string query = @" + SELECT StateName, StateDataJson + FROM UserStates + WHERE ChatId = @chatId + AND ExpiresAt > datetime('now')"; + + using var connection = new SqliteConnection(_connectionString); + var result = await connection.QueryFirstOrDefaultAsync(query, new { chatId }); + + if (result is null) + return null; + + return (result.StateName, result.StateDataJson); + } + + public async Task RemoveStateAsync(long chatId) + { + const string query = "DELETE FROM UserStates WHERE ChatId = @chatId"; + + using var connection = new SqliteConnection(_connectionString); + await connection.ExecuteAsync(query, new { chatId }); + } + + public async Task CleanupExpiredAsync() + { + const string query = "DELETE FROM UserStates WHERE ExpiresAt <= datetime('now')"; + + using var connection = new SqliteConnection(_connectionString); + return await connection.ExecuteAsync(query); + } + + private class StateRow + { + public string StateName { get; set; } = string.Empty; + public string StateDataJson { get; set; } = "{}"; + } +}