Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Database/BaseDBMigration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions Database/DBInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ public static WebApplicationBuilder CreateBuilderByDBType(string[] args)
new SqliteGroupGetter(Config.sqlConnectionString!));
builder.Services.AddSingleton<IGroupSetter>(_ =>
new SqliteGroupSetter(Config.sqlConnectionString!));

builder.Services.AddSingleton<IUserStateRepository>(_ =>
new SqliteUserStateRepository(Config.sqlConnectionString!));

return builder;
}
}
20 changes: 20 additions & 0 deletions Database/Interfaces/IUserStateRepository.cs
Original file line number Diff line number Diff line change
@@ -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<int> CleanupExpiredAsync();
}
3 changes: 3 additions & 0 deletions Database/Repositories/SqLite/SQLiteDBMigration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
81 changes: 81 additions & 0 deletions Database/Repositories/SqLite/UserStateRepository.cs
Original file line number Diff line number Diff line change
@@ -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<StateRow>(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<int> 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; } = "{}";
}
}
Loading