User synchronization revamped.

This commit is contained in:
2025-07-04 23:33:33 +02:00
parent 4393977389
commit c775e3a25e
14 changed files with 348 additions and 139 deletions

View File

@ -0,0 +1,263 @@
using MessengerApi.Configuration.Model;
using MessengerApi.Contracts.Factories;
using MessengerApi.Db.Entities;
using MessengerApi.Models;
using Microsoft.EntityFrameworkCore;
using System.Text;
namespace MessengerApi.Handlers
{
public class UserConfigHandler
{
private readonly MessengerConfiguration configuration;
private readonly ILogger logger;
private readonly IDbContextFactory dbContextFactory;
public UserConfigHandler(
MessengerConfiguration configuration,
ILogger logger,
IDbContextFactory dbContextFactory)
{
this.configuration = configuration;
this.logger = logger;
this.dbContextFactory = dbContextFactory;
}
public async Task<UserIngestionItem[]> GetItemsFromFile(FileInfo file)
{
if (file == null)
{
throw new InvalidOperationException("No file provided.");
}
else if (File.Exists(file.FullName) == false)
{
throw new FileNotFoundException($"Userfile doesn't exist: {this.configuration.UsersConfig.FullName}.");
}
var lines = await File.ReadAllLinesAsync(file.FullName, Encoding.UTF8);
var collection = new List<Tuple<UserIngestionItem, string>>();
foreach (var line in lines)
{
var columns = line.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var item = new UserIngestionItem();
item.UserName = columns[0];
item.IsEnabled = bool.Parse(columns[1]);
item.Id = Guid.Parse(columns[2]);
item.ApiKey = Guid.Parse(columns[3]);
var recipients = columns.Length == 5
? columns[4]
: null;
collection.Add(new Tuple<UserIngestionItem, string>(item, recipients));
this.logger.Trace($"Reading user {item.UserName} with ID {item.Id}");
}
foreach (var item in collection)
{
if(item.Item2 == null)
{
item.Item1.CanSendTo = [];
continue;
}
var names = item.Item2.Split(
',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
item.Item1.CanSendTo = names
.Select(x => collection.Single(c => c.Item1.UserName == x).Item1)
.ToArray();
this.logger.Trace($"User {item.Item1.UserName} has buddies {item.Item2}.");
}
return collection.Select(x => x.Item1).ToArray();
}
public Task<UserIngestionOperation[]> GetOperationsFromItems(UserIngestionItem[] fileItems)
{
if(fileItems == null)
{
throw new ArgumentNullException(nameof(fileItems));
}
using var context = this.dbContextFactory.CreateDbContext();
var existingUsers = context.Users.ToList();
var existingRoutes = context.UserRoutes.Include(x => x.From).Include(x => x.To).ToList();
var collection = new List<UserIngestionOperation>();
foreach (var item in fileItems)
{
var dbItem = context.Users.SingleOrDefault(x => x.Id == item.Id);
if (dbItem == null)
{
collection.Add(new UserIngestionOperation
{
Type = UserIngestionOperationTypes.Create,
Subtype = UserIngestionOperationSubtypes.AddUser,
Username = item.UserName,
Operation = (c) =>
{
c.Users.Local.Add(new User
{
Id = item.Id,
ApiKey = item.ApiKey,
IsEnabled = item.IsEnabled,
Name = item.UserName,
});
}
});
}
else
{
if(dbItem.ApiKey != item.ApiKey)
{
collection.Add(new UserIngestionOperation
{
Type = UserIngestionOperationTypes.Update,
Subtype = UserIngestionOperationSubtypes.ChangeUserApiKey,
Username = item.UserName,
Operation = (c) =>
{
c.Users.Single(x => x.Id == item.ApiKey).ApiKey = item.ApiKey;
}
});
}
if(dbItem.IsEnabled != item.IsEnabled)
{
collection.Add(new UserIngestionOperation
{
Type = UserIngestionOperationTypes.Update,
Subtype = UserIngestionOperationSubtypes.ChangeUserIsEnabled,
Username = item.UserName,
Operation = (c) =>
{
c.Users.Single(x => x.Id == item.ApiKey).IsEnabled = item.IsEnabled;
}
});
}
if (dbItem.Name != item.UserName)
{
collection.Add(new UserIngestionOperation
{
Type = UserIngestionOperationTypes.Update,
Subtype = UserIngestionOperationSubtypes.ChangeUserName,
Username = item.UserName,
Operation = (c) =>
{
c.Users.Single(x => x.Id == item.ApiKey).Name = item.UserName;
}
});
}
}
this.logger.Trace($"Processed line with {item.UserName}.");
}
this.logger.Debug("Finished evaluating add/update users, doing routes.");
foreach(var item in fileItems)
{
foreach(var com in item.CanSendTo)
{
this.logger.Trace($"Validating if {item.UserName} can send to {com.UserName}.");
var dbRoute = context.UserRoutes
.Include(x => x.From)
.Include(x => x.To)
.SingleOrDefault(x => x.From.Id == item.Id && x.To.Id == com.Id);
if(dbRoute == null)
{
collection.Add(new UserIngestionOperation
{
Type = UserIngestionOperationTypes.Create,
Subtype = UserIngestionOperationSubtypes.AddUserRoute,
Username = item.UserName,
Operation = (c) =>
{
var from = c.Users
.SingleOrDefault(x => x.Id == item.Id) ??
c.Users.Local.Single(x => x.Id == item.Id);
var to = c.Users
.SingleOrDefault(x => x.Id == com.Id) ??
c.Users.Local.Single(x => x.Id == com.Id);
c.Add(new UserRoute
{
From = from,
To = to
});
}
});
}
}
}
this.logger.Debug("Finished evaluating add userroutes, doing user deletes.");
foreach(var existingDbUser in existingUsers)
{
if(fileItems.Any(x=>x.Id == existingDbUser.Id) == false)
{
collection.Add(new UserIngestionOperation
{
Username = existingDbUser.Name,
Type = UserIngestionOperationTypes.Delete,
Subtype = UserIngestionOperationSubtypes.RemoveUser,
Operation = (c) =>
{
c.Remove(c.Users.Single(x => x.Id == existingDbUser.Id));
}
});
}
}
foreach (var existingRoute in existingRoutes)
{
if (fileItems.Any(fi => fi.CanSendTo.Any(fic => fi.Id == existingRoute.From.Id && fic.Id == existingRoute.To.Id)) == false)
{
collection.Add(new UserIngestionOperation
{
Username = existingRoute.From.Name,
Type = UserIngestionOperationTypes.Delete,
Subtype = UserIngestionOperationSubtypes.RemoveUserRoute,
Operation = (c) =>
{
c.Remove(c.UserRoutes.Single(x => x.Id == existingRoute.Id));
}
});
}
}
return Task.FromResult(collection.ToArray());
}
public async Task UpdateFromFile(FileInfo file)
{
var fileItems = await this.GetItemsFromFile(file);
var operations = await this.GetOperationsFromItems(fileItems);
using var context = this.dbContextFactory.CreateDbContext();
foreach(var operation in operations)
{
this.logger.Info(operation.ToString());
operation.Operation(context);
}
if(operations.Any())
{
await context.SaveChangesAsync();
}
}
}
}

View File

@ -1,99 +0,0 @@
using MessengerApi.Configuration.Model;
using MessengerApi.Contracts.Factories;
using MessengerApi.Db;
using MessengerApi.Models;
using System.Text;
namespace MessengerApi.Handlers
{
// TODO: This needs to be redone, because at every run, it wipes users and creates new ones. This makes
// all existing DB messages unassignable.
public class UserSetupHandler
{
private readonly MessengerConfiguration configuration;
private readonly ILogger logger;
private readonly IDbContextFactory dbContextFactory;
public UserSetupHandler(
MessengerConfiguration configuration,
ILogger logger,
IDbContextFactory dbContextFactory)
{
this.configuration = configuration;
this.logger = logger;
this.dbContextFactory = dbContextFactory;
}
public async Task UpdateFromFile(FileInfo file)
{
if(file.Exists)
{
var lines = await File.ReadAllLinesAsync(file.FullName, Encoding.UTF8);
var items = await this.ReadLines(lines);
await this.SynchronizeUsers(items);
}
}
private async Task<UserSetupItem[]> ReadLines(string[] lines)
{
var items = new List<UserSetupItem>();
foreach (var line in lines)
{
var values = line.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var item = new UserSetupItem
{
UserName = values[0],
ApiKey = values[1],
};
if(values.Length > 2)
{
item.CanSendToUserNames = values[2].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
items.Add(item);
}
if (items.GroupBy(x => x.UserName).Any(x => x.Count() > 1))
{
throw new InvalidOperationException("Usernames are not unique. One username per line.");
}
else if(items.GroupBy(x=>x.ApiKey).Any(x=>x.Count() > 1))
{
throw new InvalidOperationException("API keys are not unique. One API key per line.");
}
return items.ToArray();
}
private Task SynchronizeUsers(IEnumerable<UserSetupItem> users)
{
using var db = this.dbContextFactory.CreateDbContext();
db.RemoveRange(db.Users);
db.RemoveRange(db.UserRoutes);
var dbUsers = users.Select(x => new Db.Entities.User
{
Id = new Guid(),
Name = x.UserName,
ApiKey = Guid.Parse(x.ApiKey),
IsEnabled = true
});
var dbRoutes = users.SelectMany(x => x.CanSendToUserNames.Select(cs => new Db.Entities.UserRoute
{
Id = new Guid(),
From = dbUsers.Single(dbu => dbu.Name == x.UserName),
To = dbUsers.Single(dbu => dbu.Name == x.UserName)
}));
db.AddRange(dbUsers);
db.AddRange(dbRoutes);
db.SaveChanges();
return Task.CompletedTask;
}
}
}