diff --git a/src/Nocr.Users.Api.Contracts/Users/Dto/UserData.cs b/src/Nocr.Users.Api.Contracts/Users/Dto/UserData.cs
index 70b768b..e473193 100644
--- a/src/Nocr.Users.Api.Contracts/Users/Dto/UserData.cs
+++ b/src/Nocr.Users.Api.Contracts/Users/Dto/UserData.cs
@@ -9,6 +9,11 @@ public sealed class UserData
///
public required string Username { get; set; }
+ ///
+ /// Заблокирован ли бот пользователем
+ ///
+ public bool BotBlocked { get; set; }
+
///
/// Список идентити пользователя
///
diff --git a/src/Nocr.Users.Api.Contracts/Users/IUsersController.cs b/src/Nocr.Users.Api.Contracts/Users/IUsersController.cs
index 86decf0..699d79d 100644
--- a/src/Nocr.Users.Api.Contracts/Users/IUsersController.cs
+++ b/src/Nocr.Users.Api.Contracts/Users/IUsersController.cs
@@ -15,4 +15,10 @@ public interface IUsersController
[Get(WebRoutes.Users.ByIdentity)]
Task GetByIdentity([Query] UserIdentityType identityType, [Query] string identity, CancellationToken cancellationToken = default);
+
+ [Post(WebRoutes.Users.BlockBot)]
+ Task BlockBot([Path] long id, CancellationToken cancellationToken = default);
+
+ [Post(WebRoutes.Users.UnblockBot)]
+ Task UnblockBot([Path] long id, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/Nocr.Users.Api.Contracts/WebRoutes.cs b/src/Nocr.Users.Api.Contracts/WebRoutes.cs
index 4b3b969..e4c50f3 100644
--- a/src/Nocr.Users.Api.Contracts/WebRoutes.cs
+++ b/src/Nocr.Users.Api.Contracts/WebRoutes.cs
@@ -11,5 +11,9 @@ public static class WebRoutes
public const string ById = "{id}";
public const string ByIdentity = "identity";
+
+ public const string BlockBot = "{id}/block-bot";
+
+ public const string UnblockBot = "{id}/unblock-bot";
}
}
\ No newline at end of file
diff --git a/src/Nocr.Users.AppServices/Users/Repositories/IUsersRepository.cs b/src/Nocr.Users.AppServices/Users/Repositories/IUsersRepository.cs
index e04c971..c4fc817 100644
--- a/src/Nocr.Users.AppServices/Users/Repositories/IUsersRepository.cs
+++ b/src/Nocr.Users.AppServices/Users/Repositories/IUsersRepository.cs
@@ -9,4 +9,6 @@ public interface IUsersRepository
Task GetUserById(long id, CancellationToken cancellationToken = default);
Task GetByIdentity(UserIdentityType identityType, string identity, CancellationToken cancellationToken = default);
+
+ Task UpdateBotBlockedStatus(long userId, bool botBlocked, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/Nocr.Users.AppServices/Users/Services/IUsersService.cs b/src/Nocr.Users.AppServices/Users/Services/IUsersService.cs
index 1ce5968..63eb529 100644
--- a/src/Nocr.Users.AppServices/Users/Services/IUsersService.cs
+++ b/src/Nocr.Users.AppServices/Users/Services/IUsersService.cs
@@ -11,4 +11,8 @@ public interface IUsersService
Task GetById(long id, CancellationToken cancellationToken = default);
Task GetByIdentity(UserIdentityType identityType, string identity, CancellationToken cancellationToken = default);
+
+ Task BlockBot(long userId, CancellationToken cancellationToken = default);
+
+ Task UnblockBot(long userId, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/Nocr.Users.AppServices/Users/Services/UsersService.cs b/src/Nocr.Users.AppServices/Users/Services/UsersService.cs
index 5852eb1..d85c7d3 100644
--- a/src/Nocr.Users.AppServices/Users/Services/UsersService.cs
+++ b/src/Nocr.Users.AppServices/Users/Services/UsersService.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.Logging;
using Nocr.Users.Api.Contracts.Users;
using Nocr.Users.Api.Contracts.Users.Dto;
using Nocr.Users.AppServices.Users.Repositories;
@@ -7,10 +8,12 @@ namespace Nocr.Users.AppServices.Users.Services;
public sealed class UsersService : IUsersService
{
private readonly IUsersRepository _repository;
+ private readonly ILogger _logger;
- public UsersService(IUsersRepository repository)
+ public UsersService(IUsersRepository repository, ILogger logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task Create(string username, string? telegramUsername, string? email, long? telegramId,
@@ -48,6 +51,32 @@ public sealed class UsersService : IUsersService
return MapToUserData(user);
}
+ public async Task BlockBot(long userId, CancellationToken cancellationToken = default)
+ {
+ var user = await _repository.GetUserById(userId, cancellationToken);
+ if (user == null)
+ {
+ _logger.LogWarning("User with ID {UserId} not found for blocking bot", userId);
+ return;
+ }
+
+ await _repository.UpdateBotBlockedStatus(userId, true, cancellationToken);
+ _logger.LogInformation("Bot blocked for user {UserId}", userId);
+ }
+
+ public async Task UnblockBot(long userId, CancellationToken cancellationToken = default)
+ {
+ var user = await _repository.GetUserById(userId, cancellationToken);
+ if (user == null)
+ {
+ _logger.LogWarning("User with ID {UserId} not found for unblocking bot", userId);
+ return;
+ }
+
+ await _repository.UpdateBotBlockedStatus(userId, false, cancellationToken);
+ _logger.LogInformation("Bot unblocked for user {UserId}", userId);
+ }
+
private static UserData? MapToUserData(User? user)
{
if (user == null)
@@ -57,6 +86,7 @@ public sealed class UsersService : IUsersService
{
Id = user.Id,
Username = user.Username,
+ BotBlocked = user.BotBlocked,
Identities = user.Identities.Select(MapToUserIdentityData).ToArray()
};
}
diff --git a/src/Nocr.Users.AppServices/Users/User.cs b/src/Nocr.Users.AppServices/Users/User.cs
index 278bc5b..ea12f11 100644
--- a/src/Nocr.Users.AppServices/Users/User.cs
+++ b/src/Nocr.Users.AppServices/Users/User.cs
@@ -5,6 +5,8 @@ public sealed class User
public long Id { get; set; }
public string Username { get; private set; } = default!;
+
+ public bool BotBlocked { get; private set; }
private readonly List _identities = new();
@@ -44,4 +46,14 @@ public sealed class User
return new User(username, identities);
}
+
+ public void MarkAsBlocked()
+ {
+ BotBlocked = true;
+ }
+
+ public void MarkAsUnblocked()
+ {
+ BotBlocked = false;
+ }
}
\ No newline at end of file
diff --git a/src/Nocr.Users.Host/Controllers/UsersController.cs b/src/Nocr.Users.Host/Controllers/UsersController.cs
index 9e6b486..71bd780 100644
--- a/src/Nocr.Users.Host/Controllers/UsersController.cs
+++ b/src/Nocr.Users.Host/Controllers/UsersController.cs
@@ -37,4 +37,16 @@ public class UsersController : ControllerBase
{
return _usersService.GetByIdentity(identityType, identity, cancellationToken);
}
+
+ [HttpPost(WebRoutes.Users.BlockBot)]
+ public Task BlockBot([FromRoute] long id, CancellationToken cancellationToken = default)
+ {
+ return _usersService.BlockBot(id, cancellationToken);
+ }
+
+ [HttpPost(WebRoutes.Users.UnblockBot)]
+ public Task UnblockBot([FromRoute] long id, CancellationToken cancellationToken = default)
+ {
+ return _usersService.UnblockBot(id, cancellationToken);
+ }
}
\ No newline at end of file
diff --git a/src/Nocr.Users.Migrator/Migrations/20250721124257_AddBotBlockedField.Designer.cs b/src/Nocr.Users.Migrator/Migrations/20250721124257_AddBotBlockedField.Designer.cs
new file mode 100644
index 0000000..1ff567a
--- /dev/null
+++ b/src/Nocr.Users.Migrator/Migrations/20250721124257_AddBotBlockedField.Designer.cs
@@ -0,0 +1,98 @@
+//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Nocr.Users.Persistence;
+
+#nullable disable
+
+namespace Nocr.Users.Migrator.Migrations
+{
+ [DbContext(typeof(UsersContext))]
+ [Migration("20250721124257_AddBotBlockedField")]
+ partial class AddBotBlockedField
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("BotBlocked")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(false);
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("varchar(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique()
+ .HasDatabaseName("UX_users_username");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.UserIdentity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Identity")
+ .IsRequired()
+ .HasColumnType("varchar(255)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Identity", "Type")
+ .IsUnique()
+ .HasDatabaseName("UX_users_identity_identity_type");
+
+ b.ToTable("UserIdentity");
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.UserIdentity", b =>
+ {
+ b.HasOne("Nocr.Users.AppServices.Users.User", null)
+ .WithMany("Identities")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Nocr.Users.AppServices.Users.User", b =>
+ {
+ b.Navigation("Identities");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Nocr.Users.Migrator/Migrations/20250721124257_AddBotBlockedField.cs b/src/Nocr.Users.Migrator/Migrations/20250721124257_AddBotBlockedField.cs
new file mode 100644
index 0000000..5185eef
--- /dev/null
+++ b/src/Nocr.Users.Migrator/Migrations/20250721124257_AddBotBlockedField.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Nocr.Users.Migrator.Migrations
+{
+ ///
+ public partial class AddBotBlockedField : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "BotBlocked",
+ table: "Users",
+ type: "tinyint(1)",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "BotBlocked",
+ table: "Users");
+ }
+ }
+}
diff --git a/src/Nocr.Users.Migrator/Migrations/UsersContextModelSnapshot.cs b/src/Nocr.Users.Migrator/Migrations/UsersContextModelSnapshot.cs
index 5bd08d9..0d698b6 100644
--- a/src/Nocr.Users.Migrator/Migrations/UsersContextModelSnapshot.cs
+++ b/src/Nocr.Users.Migrator/Migrations/UsersContextModelSnapshot.cs
@@ -29,6 +29,11 @@ namespace Nocr.Users.Migrator.Migrations
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+ b.Property("BotBlocked")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(false);
+
b.Property("Username")
.IsRequired()
.HasColumnType("varchar(255)");
diff --git a/src/Nocr.Users.Persistence/Users/UserConfiguration.cs b/src/Nocr.Users.Persistence/Users/UserConfiguration.cs
index 9eb9075..e981d12 100644
--- a/src/Nocr.Users.Persistence/Users/UserConfiguration.cs
+++ b/src/Nocr.Users.Persistence/Users/UserConfiguration.cs
@@ -11,6 +11,9 @@ public class UserConfiguration : IEntityTypeConfiguration
builder.HasKey(x => x.Id);
builder.Property(x => x.Username)
.IsRequired();
+
+ builder.Property(x => x.BotBlocked)
+ .HasDefaultValue(false);
builder.HasIndex(x => x.Username)
.IsUnique()
diff --git a/src/Nocr.Users.Persistence/Users/UsersRepository.cs b/src/Nocr.Users.Persistence/Users/UsersRepository.cs
index 0c4f08a..9d1e833 100644
--- a/src/Nocr.Users.Persistence/Users/UsersRepository.cs
+++ b/src/Nocr.Users.Persistence/Users/UsersRepository.cs
@@ -35,4 +35,11 @@ public class UsersRepository : IUsersRepository
.FirstOrDefaultAsync(x => x.Identities.Any(i => i.Type == identityType && i.Identity == identity),
cancellationToken: cancellationToken);
}
+
+ public async Task UpdateBotBlockedStatus(long userId, bool botBlocked, CancellationToken cancellationToken = default)
+ {
+ await _db.Users
+ .Where(u => u.Id == userId)
+ .ExecuteUpdateAsync(u => u.SetProperty(x => x.BotBlocked, botBlocked), cancellationToken);
+ }
}
\ No newline at end of file