scripts/access_level_use_limit/AlsResetScript.cs

360 lines
9.3 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MDP.SID.Scripting.Grpc;
using MDP.SID.Shared.Public.Events;
using Newtonsoft.Json;
using Event = MDP.SID.Scripting.Grpc.Event;
using Timer = System.Timers.Timer;
public class Interval
{
public TimeSpan Start { get; set; }
public TimeSpan End { get; set; }
public string Name { get; set; }
}
private readonly Interval[] _accessIntervals =
{
// Set schedule range and access level name
// TimeSpan(hours, minutes, seconds)
// Name = "Access_level"
new() {Start = new TimeSpan(16, 23, 0), End = new TimeSpan(16, 24, 0), Name = "Breakfast"},
new() {Start = new TimeSpan(10, 24, 0), End = new TimeSpan(16, 25, 0), Name = "Everywhere"},
new() {Start = new TimeSpan(16, 25, 0), End = new TimeSpan(16, 26, 0), Name = "Dinner"}
};
// Set a list of door names that will be monitored
private readonly string[] _canteenDoors =
{
"Canteen_Door_1", "Canteen_Door_2"
};
/// <summary>
/// True -> remove last access granted user al with door open event
/// False -> remove user al with access granted event
/// </summary>
private const bool RemoveUserAlThenDoorOpened = true;
// Set time when access levels will reset
public readonly TimeSpan AccessLevelsResetTime = new(16, 27, 0);
private string _path;
private readonly SemaphoreSlim _semaphoreSlim = new(1);
private Timer _timer;
private Timer _lastAccessGrantTimer;
private static readonly ConcurrentDictionary<int, IList<string>> _failedAddOrUpdate = new();
private int? _lastAccessGrantedUserId;
private int? _lastAccessGrantedId;
private async Task ResetUsersAccessLevelsAsync()
{
try
{
await _semaphoreSlim.WaitAsync();
if (File.Exists(_path) is false)
{
Context.LogError("Users file not found");
return;
}
var alNames = _accessIntervals.Select(a => a.Name);
var accessLevels = await Context.GetAccessLevelsAsync();
var updateEntities = accessLevels
.Where(a => alNames.Contains(a.Name))
.Select(a => new EntityUpdate { Id = a.Id, Update = true })
.ToList();
updateEntities.AddRange(accessLevels
.Where(a => alNames.Contains(a.Name) is false)
.Select(a => new EntityUpdate { Id = a.Id}));
var text = await File.ReadAllTextAsync(_path);
var users = JsonConvert.DeserializeObject<Dictionary<int, IList<string>>>(text);
if (users is null || users.Any() is false)
{
Context.LogWarning("Users file empty");
return;
}
var userIds = users.Keys.ToArray();
var configuration = new BulkUpdateUser();
configuration.AccessLevels.AddRange(updateEntities);
await Context.BulkUpdateUsersAsync(0, userIds.Length, userIds, configuration);
File.Delete(_path);
Context.LogInformation("Users file deleted after bulk update operation");
}
catch (Exception ex)
{
Context.LogError($"Failed reset users access levels. Exception: {ex.Message}");
}
finally
{
_semaphoreSlim.Release();
}
}
private void AddOrUpdateUser(int userId, string accessLevel)
{
try
{
_semaphoreSlim.Wait();
Dictionary<int, IList<string>> users;
if (File.Exists(_path) is false)
{
users = new Dictionary<int, IList<string>>();
}
else
{
var text = File.ReadAllText(_path);
users = JsonConvert.DeserializeObject<Dictionary<int, IList<string>>>(text);
// no content
if (users is null)
{
users = new Dictionary<int, IList<string>>();
}
}
AddFromFailed(users);
if (users.TryGetValue(userId, out var accessLevels))
{
if (accessLevels.Contains(accessLevel) is false)
{
accessLevels.Add(accessLevel);
}
}
else
{
users.Add(userId, new List<string> { accessLevel });
}
var output = JsonConvert.SerializeObject(users);
File.WriteAllText(_path, output);
_failedAddOrUpdate.Clear();
}
catch (Exception ex)
{
Context.LogError($"Failed add or update user with Id: {userId} al: {accessLevel}. Exception: {ex.Message}");
AddToFailed(userId, accessLevel);
}
finally
{
_semaphoreSlim.Release();
}
}
private void AddToFailed(int userId, string accessLevel)
{
if (_failedAddOrUpdate.TryGetValue(userId, out var als))
{
if (als.Contains(accessLevel)) return;
var newAls = new List<string>(als) { accessLevel };
_failedAddOrUpdate.TryUpdate(userId, newAls, als);
}
else
{
_failedAddOrUpdate.TryAdd(userId, new List<string> { accessLevel });
}
}
private void AddFromFailed(Dictionary<int, IList<string>> users)
{
foreach (var user in _failedAddOrUpdate)
{
if (users.TryGetValue(user.Key, out var accessLevels))
{
foreach (var al in user.Value)
{
if (accessLevels.Contains(al)) continue;
accessLevels.Add(al);
}
}
else
{
users.Add(user.Key, user.Value);
}
}
}
private async void EventReceived(Event received)
{
if (_canteenDoors.Contains(received.DoorName) is false) return;
if (Guid.TryParse(received.TypeUid, out var typeUid) is false) return;
if (typeUid == EventTypes.AccessGranted)
{
if (received.UserId.HasValue is false) return;
var eventTime = received.Time.ToDateTime().ToLocalTime().TimeOfDay;
var interval = _accessIntervals.FirstOrDefault(a => a.End >= eventTime && a.Start <= eventTime);
if (interval is null) return;
_lastAccessGrantedUserId = received.UserId.Value;
if (RemoveUserAlThenDoorOpened)
{
if (_lastAccessGrantedId.HasValue)
{
await Context.SetEventsDescriptionAsync(new List<int> { (int)_lastAccessGrantedId }, "Ignore");
}
_lastAccessGrantedId = received.Id;
if (_lastAccessGrantTimer is not null && _lastAccessGrantTimer.Enabled) return;
var timeToIntervalEnd = interval.End - eventTime;
StartLastAccessGrantTimer(timeToIntervalEnd);
}
else
{
await RemoveUserAccessLevelAsync(received);
}
}
else if (RemoveUserAlThenDoorOpened && typeUid == EventTypes.DoorOpened)
{
await RemoveUserAccessLevelAsync(received);
}
}
private async Task RemoveUserAccessLevelAsync(Event received)
{
if (_lastAccessGrantedUserId.HasValue is false) return;
try
{
var eventTime = received.Time.ToDateTime().ToLocalTime().TimeOfDay;
var interval = _accessIntervals.FirstOrDefault(a => a.End >= eventTime && a.Start <= eventTime);
if (interval is null) return;
var userAccessLevels = await Context.GetUserAccessLevelsAsync(_lastAccessGrantedUserId.Value);
var userAccessLevel = userAccessLevels.FirstOrDefault(a => a.Name == interval.Name);
if (userAccessLevel is null)
{
Context.LogWarning($"Al for user with Id: {_lastAccessGrantedUserId.Value} not found");
return;
}
await Context.DeleteUserAccessLevelAsync(userAccessLevel.Id);
Context.LogDebug($"Al: {userAccessLevel.Name} removed for user with Id: {_lastAccessGrantedUserId.Value}");
AddOrUpdateUser(_lastAccessGrantedUserId.Value, userAccessLevel.Name);
}
finally
{
_lastAccessGrantedUserId = null;
_lastAccessGrantedId = null;
}
}
private void StartLastAccessGrantTimer(TimeSpan interval)
{
Context.LogDebug($"Time until last access grant event hit: {interval}");
_lastAccessGrantTimer = new Timer(interval.TotalMilliseconds);
_lastAccessGrantTimer.Elapsed += async (_, _) => await OnLastAccessGrantEvent();
_lastAccessGrantTimer.AutoReset = false;
_lastAccessGrantTimer.Start();
}
private async Task OnLastAccessGrantEvent()
{
if (_lastAccessGrantedId.HasValue)
{
await Context.SetEventsDescriptionAsync(new List<int> { (int)_lastAccessGrantedId }, "Ignore");
_lastAccessGrantedId = null;
}
}
private async Task OnResetEvent()
{
_timer.Interval = new TimeSpan(24, 0, 0).TotalMilliseconds;
_timer.Start();
Context.LogInformation("Reset users al's event raised");
await ResetUsersAccessLevelsAsync();
}
Context.OnEventReceived += EventReceived;
var timeToReset = AccessLevelsResetTime - DateTime.Now.TimeOfDay;
if (timeToReset.CompareTo(TimeSpan.Zero) < 0)
{
timeToReset = new TimeSpan(1, AccessLevelsResetTime.Hours, AccessLevelsResetTime.Minutes, AccessLevelsResetTime.Seconds) - DateTime.Now.TimeOfDay;
}
Context.LogDebug($"Time until al reset first time hit : {timeToReset}");
_timer = new Timer(timeToReset.TotalMilliseconds);
_timer.Elapsed += async (_, _) => await OnResetEvent();
_timer.AutoReset = false;
_timer.Start();
var scriptFilesPath = await Context.GetScriptFilesPathAsync();
_path = Path.Combine(scriptFilesPath, "Canteen.txt");
CancellationToken.WaitHandle.WaitOne();
_timer.Stop();
_timer.Dispose();
if (_lastAccessGrantTimer is not null)
{
_lastAccessGrantTimer.Stop();
_lastAccessGrantTimer.Dispose();
}
Context.LogInformation("Script stopped");