Current state of the websocket server for the Hermes client

This commit is contained in:
Tom
2024-06-24 22:21:59 +00:00
commit 95bc073a73
32 changed files with 1676 additions and 0 deletions

View File

@ -0,0 +1,57 @@
using HermesSocketLibrary.db;
using HermesSocketLibrary.Socket.Data;
using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Socket.Handlers
{
public class ChatterHandler : ISocketHandler
{
public int OpCode { get; } = 6;
private readonly Database _database;
private readonly HashSet<long> _chatters;
private readonly ChatterMessage[] _array;
private readonly ILogger _logger;
private readonly object _lock;
private int _index;
public ChatterHandler(Database database, ILogger logger)
{
_database = database;
_logger = logger;
_chatters = new HashSet<long>(1001);
_array = new ChatterMessage[1000];
_index = -1;
_lock = new object();
}
public async Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets)
{
if (message is not ChatterMessage data)
return;
lock (_lock)
{
if (_chatters.Contains(data.Id))
return;
_chatters.Add(data.Id);
if (_index == _array.Length - 1)
_index = -1;
_array[++_index] = data;
}
try
{
string sql = "INSERT INTO \"Chatter\" (id, name) VALUES (@idd, @name)";
await _database.Execute(sql, new Dictionary<string, object>() { { "idd", data.Id }, { "name", data.Name } });
}
catch (Exception e)
{
_logger.Error(e, "Failed to add chatter.");
}
}
}
}

View File

@ -0,0 +1,77 @@
using HermesSocketLibrary.db;
using HermesSocketLibrary.Socket.Data;
using Npgsql;
using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Socket.Handlers
{
public class EmoteDetailsHandler : ISocketHandler
{
public int OpCode { get; } = 7;
private readonly Database _database;
private readonly HashSet<string> _emotes;
private readonly ILogger _logger;
private readonly object _lock;
public EmoteDetailsHandler(Database database, ILogger logger)
{
_database = database;
_logger = logger;
_emotes = new HashSet<string>(501);
_lock = new object();
}
public async Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets)
{
if (message is not EmoteDetailsMessage data)
return;
if (data.Emotes == null)
return;
if (!data.Emotes.Any())
return;
lock (_lock)
{
foreach (var entry in data.Emotes)
{
if (_emotes.Contains(entry.Key))
{
_emotes.Remove(entry.Key);
continue;
}
_emotes.Add(entry.Key);
}
}
int rows = 0;
string sql = "INSERT INTO \"Emote\" (id, name) VALUES (@idd, @name)";
using (var connection = await _database.DataSource.OpenConnectionAsync())
{
using (var command = new NpgsqlCommand(sql, connection))
{
foreach (var entry in data.Emotes)
{
command.Parameters.Clear();
command.Parameters.AddWithValue("idd", entry.Key);
command.Parameters.AddWithValue("name", entry.Value);
await command.PrepareAsync();
try
{
rows += await command.ExecuteNonQueryAsync();
}
catch (Exception e)
{
_logger.Error(e, "Failed to add emote detail: " + entry.Key + " -> " + entry.Value);
}
}
}
}
}
}
}

View File

@ -0,0 +1,75 @@
using HermesSocketLibrary.db;
using HermesSocketLibrary.Socket.Data;
using Npgsql;
using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Socket.Handlers
{
public class EmoteUsageHandler : ISocketHandler
{
public int OpCode { get; } = 8;
private readonly Database _database;
private readonly HashSet<string> _history;
private readonly EmoteUsageMessage[] _array;
private readonly ILogger _logger;
private int _index;
public EmoteUsageHandler(Database database, ILogger logger)
{
_database = database;
_logger = logger;
_history = new HashSet<string>(101);
_array = new EmoteUsageMessage[100];
_index = -1;
}
public async Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets)
{
if (message is not EmoteUsageMessage data)
return;
lock (_logger)
{
if (_history.Contains(data.MessageId))
{
return;
}
_history.Add(data.MessageId);
if (_index >= _array.Length - 1)
_index = -1;
_index = (_index + 1) % _array.Length;
if (_array[_index] != null)
_history.Remove(data.MessageId);
_array[_index] = data;
}
int rows = 0;
string sql = "INSERT INTO \"EmoteUsageHistory\" (timestamp, \"broadcasterId\", \"emoteId\", \"chatterId\") VALUES (@time, @broadcaster, @emote, @chatter)";
using (var connection = await _database.DataSource.OpenConnectionAsync())
{
using (var command = new NpgsqlCommand(sql, connection))
{
foreach (var entry in data.Emotes)
{
command.Parameters.Clear();
command.Parameters.AddWithValue("time", data.DateTime);
command.Parameters.AddWithValue("broadcaster", data.BroadcasterId);
command.Parameters.AddWithValue("emote", entry);
command.Parameters.AddWithValue("chatter", data.ChatterId);
await command.PrepareAsync();
rows += await command.ExecuteNonQueryAsync();
}
}
}
_logger.Information($"Tracked {rows} emote(s) to history.");
}
}
}

View File

@ -0,0 +1,29 @@
using HermesSocketLibrary.Socket.Data;
using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Socket.Handlers
{
public class ErrorHandler : ISocketHandler
{
public int OpCode { get; } = 0;
private ILogger _logger;
public ErrorHandler(ILogger logger)
{
_logger = logger;
}
public async Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets)
{
if (message is not ErrorMessage data)
return;
if (data.Exception == null)
_logger.Error(data.Message);
else
_logger.Error(data.Exception, data.Message);
}
}
}

View File

@ -0,0 +1,33 @@
using HermesSocketLibrary.Socket.Data;
using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Socket.Handlers
{
public class HeartbeatHandler : ISocketHandler
{
public int OpCode { get; } = 0;
private ILogger _logger;
public HeartbeatHandler(ILogger logger)
{
_logger = logger;
}
public async Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets)
{
if (message is not HeartbeatMessage data)
return;
sender.LastHeartbeatReceived = DateTime.UtcNow;
_logger.Verbose($"Received heartbeat from socket [ip: {sender.IPAddress}].");
if (data.Respond)
await sender.Send(0, new HeartbeatMessage()
{
DateTime = DateTime.UtcNow,
Respond = false
});
}
}
}

View File

@ -0,0 +1,83 @@
using HermesSocketLibrary.db;
using HermesSocketLibrary.Socket.Data;
using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Socket.Handlers
{
public class HermesLoginHandler : ISocketHandler
{
public int OpCode { get; } = 1;
private readonly Database _database;
private readonly HermesSocketManager _sockets;
private readonly ILogger _logger;
private readonly object _lock;
public HermesLoginHandler(Database database, HermesSocketManager sockets, ILogger logger)
{
_database = database;
_sockets = sockets;
_logger = logger;
_lock = new object();
}
public async Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets)
{
if (message is not HermesLoginMessage data || data == null || data.ApiKey == null)
return;
if (sender.Id != null)
return;
string sql = "select \"userId\" from \"ApiKey\" where id = @key";
var result = await _database.ExecuteScalar(sql, new Dictionary<string, object>() { { "key", data.ApiKey } });
string? userId = result?.ToString();
if (userId == null)
return;
var recipients = _sockets.GetSockets(userId).ToList();
lock (_lock)
{
if (sender.Id != null)
return;
sender.Id = userId;
}
string sql2 = "select \"name\" from \"User\" where id = @user";
var result2 = await _database.ExecuteScalar(sql2, new Dictionary<string, object>() { { "user", userId } });
string? name = result2?.ToString();
if (string.IsNullOrEmpty(name))
return;
sender.Name = name;
await sender.Send(2, new LoginAckMessage()
{
UserId = userId
});
var ack = new LoginAckMessage()
{
AnotherClient = true,
UserId = userId
};
foreach (var socket in recipients)
{
try
{
await socket.Send(2, ack);
}
catch (Exception)
{
}
}
_logger.Information($"Hermes client logged in [name: {name}][id: {userId}][ip: {sender.IPAddress}]");
}
}
}

View File

@ -0,0 +1,10 @@
using System.Net.WebSockets;
namespace HermesSocketServer.Socket.Handlers
{
public interface ISocketHandler
{
int OpCode { get; }
Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets);
}
}

View File

@ -0,0 +1,71 @@
using HermesSocketLibrary.Requests;
using HermesSocketLibrary.Socket.Data;
using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Socket.Handlers
{
public class RequestHandler : ISocketHandler
{
public int OpCode { get; } = 3;
private readonly RequestManager _requests;
private readonly HermesSocketManager _sockets;
private readonly ILogger _logger;
public RequestHandler(RequestManager requests, HermesSocketManager sockets, ILogger logger)
{
_requests = requests;
_sockets = sockets;
_logger = logger;
}
public async Task Execute<T>(WebSocketUser sender, T message, HermesSocketManager sockets)
{
if (sender.Id == null)
return;
if (message is not RequestMessage data)
return;
RequestResult? result = null;
_logger.Debug("Executing request handler: " + data.Type);
try
{
result = await _requests.Grant(sender.Id, data);
}
catch (Exception e)
{
_logger.Error(e, $"Failed to grant a request of type '{data.Type}'.");
}
if (result == null || !result.Success)
return;
var ack = new RequestAckMessage()
{
Request = data,
Data = result.Result,
Nounce = data.Nounce
};
if (!result.NotifyClientsOnAccount)
{
await sender.Send(4, ack);
return;
}
var recipients = _sockets.GetSockets(sender.Id);
foreach (var socket in recipients)
{
try
{
_logger.Verbose($"Sending {data.Type} to socket [ip: {socket.IPAddress}].");
await socket.Send(4, ack);
}
catch (Exception)
{
_logger.Warning($"Failed to send {data.Type} to socket [ip: {socket.IPAddress}].");
}
}
}
}
}