297 lines
8.1 KiB
C#
297 lines
8.1 KiB
C#
using NLog;
|
|
|
|
namespace ChatRoom;
|
|
|
|
/// <summary>
|
|
/// Chat room service logic
|
|
/// </summary>
|
|
public class ChatRoomLogic
|
|
{
|
|
/// <summary>
|
|
/// Background thread of deleting approved or rejected messages
|
|
/// </summary>
|
|
private Thread thread;
|
|
/// <summary>
|
|
/// Chat Room state
|
|
/// </summary>
|
|
private ChatRoomState state = new ChatRoomState();
|
|
/// <summary>
|
|
/// Logger
|
|
/// </summary>
|
|
private Logger log = LogManager.GetCurrentClassLogger();
|
|
|
|
/// <summary>
|
|
/// Chat room logic constructor
|
|
/// </summary>
|
|
public ChatRoomLogic()
|
|
{
|
|
thread = new Thread(BackgroundTask);
|
|
thread.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate the next incrementing ID
|
|
/// </summary>
|
|
/// <returns>Unique ID</returns>
|
|
int NextId()
|
|
{
|
|
int id = state.lastUniqueId;
|
|
state.lastUniqueId++;
|
|
return id;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a client
|
|
/// </summary>
|
|
/// <param name="name">Client name</param>
|
|
/// <returns>Client ID</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find a client by ID, can return NULL if not found
|
|
/// </summary>
|
|
/// <param name="clientId">Client ID</param>
|
|
/// <returns>Optional client object</returns>
|
|
Client? FindClientById(int clientId)
|
|
{
|
|
foreach (var client in state.clients)
|
|
{
|
|
if (client.id == clientId)
|
|
{
|
|
return client;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find message by ID, can return NULL if not found
|
|
/// </summary>
|
|
/// <param name="messageId">Message ID</param>
|
|
/// <returns>Optional message object</returns>
|
|
Message? FindMessageById(int messageId)
|
|
{
|
|
foreach (var message in state.messages)
|
|
{
|
|
if (message.id == messageId)
|
|
{
|
|
return message;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if client is still blocked, will clear `blockedUntil` if client became unblocked
|
|
/// </summary>
|
|
/// <param name="client">Client object</param>
|
|
/// <returns>Is client blocked?</returns>
|
|
bool GetAndUpdateBlockedState(Client client)
|
|
{
|
|
if (client.blockedUntil != null && DateTime.UtcNow >= client.blockedUntil)
|
|
{
|
|
client.blockedUntil = null;
|
|
}
|
|
|
|
return client.blockedUntil != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send a message
|
|
/// </summary>
|
|
/// <param name="clientId">Client ID</param>
|
|
/// <param name="contents">Message contents</param>
|
|
/// <param name="needsToBeCensored">Does this message need to be censored?</param>
|
|
/// <returns>Was sending the message successful, can fail if client is blocked</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get next message which isin't approved or rejected
|
|
/// </summary>
|
|
/// <returns>Optional message object</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Approve a message
|
|
/// </summary>
|
|
/// <param name="messageId">Message ID</param>
|
|
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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reject a message
|
|
/// </summary>
|
|
/// <param name="messageId">Message ID</param>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get number of strikes a client has
|
|
/// </summary>
|
|
/// <param name="clientId">Client ID</param>
|
|
/// <returns>Number of strikes</returns>
|
|
public int GetStrikes(int clientId)
|
|
{
|
|
lock (state.accessLock)
|
|
{
|
|
var client = FindClientById(clientId);
|
|
if (client == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return client.strikes;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get timestamp until when the client is blocked
|
|
/// </summary>
|
|
/// <param name="clientId">Client ID</param>
|
|
/// <returns>Optional datetime object</returns>
|
|
public DateTime? GetBlockedUntil(int clientId)
|
|
{
|
|
lock (state.accessLock)
|
|
{
|
|
var client = FindClientById(clientId);
|
|
if (client == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return client.blockedUntil;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Main loop for background thread. Used for deleting approved or rejected messages.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|