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" }; /// /// True -> remove last access granted user al with door open event /// False -> remove user al with access granted event /// 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> _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>>(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> users; if (File.Exists(_path) is false) { users = new Dictionary>(); } else { var text = File.ReadAllText(_path); users = JsonConvert.DeserializeObject>>(text); // no content if (users is null) { users = new Dictionary>(); } } AddFromFailed(users); if (users.TryGetValue(userId, out var accessLevels)) { if (accessLevels.Contains(accessLevel) is false) { accessLevels.Add(accessLevel); } } else { users.Add(userId, new List { 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(als) { accessLevel }; _failedAddOrUpdate.TryUpdate(userId, newAls, als); } else { _failedAddOrUpdate.TryAdd(userId, new List { accessLevel }); } } private void AddFromFailed(Dictionary> 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)_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)_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");