using NLog; namespace ChatRoom; /// /// Chat room service logic /// public class ChatRoomLogic { /// /// Background thread of deleting approved or rejected messages /// private Thread thread; /// /// Chat Room state /// private ChatRoomState state = new ChatRoomState(); /// /// Logger /// private Logger log = LogManager.GetCurrentClassLogger(); /// /// Chat room logic constructor /// public ChatRoomLogic() { thread = new Thread(BackgroundTask); thread.Start(); } /// /// Generate the next incrementing ID /// /// Unique ID int NextId() { int id = state.lastUniqueId; state.lastUniqueId++; return id; } /// /// Register a client /// /// Client name /// Client ID public int RegisterClient(string name) { lock (state.accessLock) { int clientId = NextId(); state.clients.Add(new Client { id = clientId, name = name }); log.Info($"Registered with client '{name}' with id {clientId}"); return clientId; } } /// /// Find a client by ID, can return NULL if not found /// /// Client ID /// Optional client object Client? FindClientById(int clientId) { foreach (var client in state.clients) { if (client.id == clientId) { return client; } } return null; } /// /// Find message by ID, can return NULL if not found /// /// Message ID /// Optional message object Message? FindMessageById(int messageId) { foreach (var message in state.messages) { if (message.id == messageId) { return message; } } return null; } /// /// Check if client is still blocked, will clear `blockedUntil` if client became unblocked /// /// Client object /// Is client blocked? bool GetAndUpdateBlockedState(Client client) { if (client.blockedUntil != null && DateTime.UtcNow >= client.blockedUntil) { client.blockedUntil = null; } return client.blockedUntil != null; } /// /// Send a message /// /// Client ID /// Message contents /// Does this message need to be censored? /// Was sending the message successful, can fail if client is blocked public bool SendMessage(int clientId, string contents, bool needsToBeCensored) { lock (state.accessLock) { var client = FindClientById(clientId); if (client == null) { return false; } if (GetAndUpdateBlockedState(client)) { return false; } var message = new Message { id = NextId(), clientId = clientId, contents = contents, needsToBeCensored = needsToBeCensored, status = MessageStatus.WaitingForModerator, sentAt = DateTime.UtcNow }; state.messages.Add(message); log.Info($"Client '{client.name}' ({client.id}) sent message '{contents}' ({message.id}). Needs to censored: {needsToBeCensored}"); } return true; } /// /// Get next message which isin't approved or rejected /// /// Optional message object public ChatRoomContract.Message? GetNewMessage() { lock (state.accessLock) { foreach (var message in state.messages) { if (message.status != MessageStatus.WaitingForModerator) continue; log.Info($"Message '{message.id}' given to moderator"); message.status = MessageStatus.GivenToModerator; return new ChatRoomContract.Message{ id = message.id, contents = message.contents, needsToBeCensored = message.needsToBeCensored }; } } return null; } /// /// Approve a message /// /// Message ID public void ApproveMessage(int messageId) { lock (state.accessLock) { var message = FindMessageById(messageId); if (message == null) { return; } if (message.status != MessageStatus.GivenToModerator) { return; } message.status = MessageStatus.Approved; log.Info($"Message {message.id} was approved"); } } /// /// Reject a message /// /// Message ID public void RejectMessage(int messageId) { lock (state.accessLock) { var message = FindMessageById(messageId); if (message == null) { return; } if (message.status != MessageStatus.GivenToModerator) { return; } message.status = MessageStatus.Rejected; log.Info($"Message {message.id} was rejected"); var client = FindClientById(message.clientId); if (client != null && !GetAndUpdateBlockedState(client)) { client.strikes++; var rnd = new Random(); if (client.strikes > rnd.Next(0, 10)) { log.Info($"Client '{client.name}' ({client.id}) was blocked for {client.strikes}s"); client.blockedUntil = DateTime.UtcNow.AddSeconds(client.strikes); client.strikes = 0; } } } } /// /// Get number of strikes a client has /// /// Client ID /// Number of strikes public int GetStrikes(int clientId) { lock (state.accessLock) { var client = FindClientById(clientId); if (client == null) { return 0; } return client.strikes; } } /// /// Get timestamp until when the client is blocked /// /// Client ID /// Optional datetime object public DateTime? GetBlockedUntil(int clientId) { lock (state.accessLock) { var client = FindClientById(clientId); if (client == null) { return null; } return client.blockedUntil; } } /// /// Main loop for background thread. Used for deleting approved or rejected messages. /// public void BackgroundTask() { while (true) { lock (state.accessLock) { var now = DateTime.UtcNow; int count = state.messages.RemoveAll(msg => (now - msg.sentAt).TotalSeconds > 5 && (msg.status == MessageStatus.Approved || msg.status == MessageStatus.Rejected) ); log.Info($"Running periodic cleanup, removed {count} messages"); } Thread.Sleep(10 * 1000); } } }