Compare commits

..

No commits in common. "3f3ba635540801f5c5ede4a2e773b4e36a9b540f" and "39c6442126742d4aeae38c4440246087bfbcd9aa" have entirely different histories.

18 changed files with 55 additions and 710 deletions

View File

@ -1,7 +1,6 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketLibrary.db; using HermesSocketLibrary.db;
using HermesSocketLibrary.Requests; using HermesSocketLibrary.Requests;
using HermesSocketServer.Store;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Requests namespace HermesSocketServer.Requests
@ -10,13 +9,11 @@ namespace HermesSocketServer.Requests
{ {
public string Name => "create_tts_user"; public string Name => "create_tts_user";
private Database _database; private Database _database;
private ChatterStore _chatters;
private ILogger _logger; private ILogger _logger;
public CreateTTSUser(ChatterStore chatters, Database database, ILogger logger) public CreateTTSUser(Database database, ILogger logger)
{ {
_database = database; _database = database;
_chatters = chatters;
_logger = logger; _logger = logger;
} }
@ -28,25 +25,20 @@ namespace HermesSocketServer.Requests
return new RequestResult(false, null); return new RequestResult(false, null);
} }
if (long.TryParse(data["chatter"].ToString(), out long chatterId)) if (long.TryParse(data["chatter"].ToString(), out long chatter))
data["chatter"] = chatterId; data["chatter"] = chatter;
else
return new RequestResult(false, "Invalid Twitch user id");
if (data["voice"] is JsonElement v) if (data["voice"] is JsonElement v)
data["voice"] = v.ToString(); data["voice"] = v.ToString();
else
return new RequestResult(false, "Invalid voice id");
data["user"] = sender; data["user"] = sender;
var check = await _database.ExecuteScalar("SELECT state FROM \"TtsVoiceState\" WHERE \"userId\" = @user AND \"ttsVoiceId\" = @voice", data) ?? false; var check = await _database.ExecuteScalar("SELECT state FROM \"TtsVoiceState\" WHERE \"userId\" = @user AND \"ttsVoiceId\" = @voice", data) ?? false;
if (check is not bool state || !state) if (check is not bool state || !state)
return new RequestResult(false, "Voice is disabled on this channel."); return new RequestResult(false, null);
_chatters.Set(sender, chatterId, data["voice"].ToString()); string sql = "INSERT INTO \"TtsChatVoice\" (\"userId\", \"chatterId\", \"ttsVoiceId\") VALUES (@user, @chatter, @voice)";
var result = await _database.Execute(sql, data);
_logger.Information($"Selected a tts voice [voice: {data["voice"]}] for user [chatter: {data["chatter"]}] in channel [channel: {data["user"]}]"); _logger.Information($"Selected a tts voice [voice: {data["voice"]}] for user [chatter: {data["chatter"]}] in channel [channel: {data["user"]}]");
return new RequestResult(true, null); return new RequestResult(result == 1, null);
} }
} }
} }

View File

@ -1,6 +1,6 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketLibrary.db;
using HermesSocketLibrary.Requests; using HermesSocketLibrary.Requests;
using HermesSocketServer.Store;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Requests namespace HermesSocketServer.Requests
@ -8,13 +8,13 @@ namespace HermesSocketServer.Requests
public class CreateTTSVoice : IRequest public class CreateTTSVoice : IRequest
{ {
public string Name => "create_tts_voice"; public string Name => "create_tts_voice";
private IStore<string, string> _voices; private Database _database;
private ILogger _logger; private ILogger _logger;
private Random _random; private Random _random;
public CreateTTSVoice(VoiceStore voices, ILogger logger) public CreateTTSVoice(Database database, ILogger logger)
{ {
_voices = voices; _database = database;
_logger = logger; _logger = logger;
_random = new Random(); _random = new Random();
} }
@ -28,17 +28,18 @@ namespace HermesSocketServer.Requests
return new RequestResult(false, null); return new RequestResult(false, null);
} }
string id = RandomString(25);
data.Add("idd", id);
if (data["voice"] is JsonElement v) if (data["voice"] is JsonElement v)
data["voice"] = v.ToString(); data["voice"] = v.ToString();
else
return new RequestResult(false, "Invalid voice name.");
string id = RandomString(25); string sql = "INSERT INTO \"TtsVoice\" (id, name) VALUES (@idd, @voice)";
var result = await _database.Execute(sql, data);
_logger.Information($"Added a new voice [voice: {data["voice"]}][voice id: {data["idd"]}]");
_voices.Set(id, data["voice"].ToString()); data.Remove("idd");
_logger.Information($"Added a new voice [voice: {data["voice"]}][voice id: {id}]"); return new RequestResult(result == 1, id);
return new RequestResult(true, id);
} }
private string RandomString(int length) private string RandomString(int length)

View File

@ -1,6 +1,6 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketLibrary.db;
using HermesSocketLibrary.Requests; using HermesSocketLibrary.Requests;
using HermesSocketServer.Store;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Requests namespace HermesSocketServer.Requests
@ -8,12 +8,12 @@ namespace HermesSocketServer.Requests
public class DeleteTTSVoice : IRequest public class DeleteTTSVoice : IRequest
{ {
public string Name => "delete_tts_voice"; public string Name => "delete_tts_voice";
private IStore<string, string> _voices; private Database _database;
private ILogger _logger; private ILogger _logger;
public DeleteTTSVoice(VoiceStore voices, ILogger logger) public DeleteTTSVoice(Database database, ILogger logger)
{ {
_voices = voices; _database = database;
_logger = logger; _logger = logger;
} }
@ -28,9 +28,10 @@ namespace HermesSocketServer.Requests
if (data["voice"] is JsonElement v) if (data["voice"] is JsonElement v)
data["voice"] = v.ToString(); data["voice"] = v.ToString();
_voices.Remove(data["voice"].ToString()); string sql = "DELETE FROM \"TtsVoice\" WHERE id = @voice";
var result = await _database.Execute(sql, data);
_logger.Information($"Deleted a voice by id [voice id: {data["voice"]}]"); _logger.Information($"Deleted a voice by id [voice id: {data["voice"]}]");
return new RequestResult(true, null); return new RequestResult(result == 1, null);
} }
} }
} }

View File

@ -1,5 +1,5 @@
using HermesSocketLibrary.db;
using HermesSocketLibrary.Requests; using HermesSocketLibrary.Requests;
using HermesSocketServer.Store;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Requests namespace HermesSocketServer.Requests
@ -7,20 +7,24 @@ namespace HermesSocketServer.Requests
public class GetTTSUsers : IRequest public class GetTTSUsers : IRequest
{ {
public string Name => "get_tts_users"; public string Name => "get_tts_users";
private ChatterStore _chatters; private readonly Database _database;
private ILogger _logger; private readonly ILogger _logger;
public GetTTSUsers(ChatterStore chatters, ILogger logger) public GetTTSUsers(Database database, ILogger logger)
{ {
_chatters = chatters; _database = database;
_logger = logger; _logger = logger;
} }
public async Task<RequestResult> Grant(string sender, IDictionary<string, object>? data) public async Task<RequestResult> Grant(string sender, IDictionary<string, object>? data)
{ {
var temp = _chatters.Get(sender); var temp = new Dictionary<string, object>() { { "user", sender } };
IDictionary<long, string> users = new Dictionary<long, string>();
string sql = $"SELECT \"ttsVoiceId\", \"chatterId\" FROM \"TtsChatVoice\" WHERE \"userId\" = @user";
await _database.Execute(sql, temp, (r) => users.Add(r.GetInt64(1), r.GetString(0)));
_logger.Information($"Fetched all chatters' selected tts voice for channel [channel: {sender}]"); _logger.Information($"Fetched all chatters' selected tts voice for channel [channel: {sender}]");
return new RequestResult(true, temp, notifyClientsOnAccount: false); return new RequestResult(true, users, notifyClientsOnAccount: false);
} }
} }
} }

View File

@ -1,7 +1,6 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketLibrary.db; using HermesSocketLibrary.db;
using HermesSocketLibrary.Requests; using HermesSocketLibrary.Requests;
using HermesSocketServer.Store;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Requests namespace HermesSocketServer.Requests
@ -12,18 +11,15 @@ namespace HermesSocketServer.Requests
private readonly ServerConfiguration _configuration; private readonly ServerConfiguration _configuration;
private readonly Database _database; private readonly Database _database;
private ChatterStore _chatters; private readonly ILogger _logger;
private ILogger _logger;
public UpdateTTSUser(ChatterStore chatters, Database database, ServerConfiguration configuration, ILogger logger) public UpdateTTSUser(ServerConfiguration configuration, Database database, ILogger logger)
{ {
_database = database;
_chatters = chatters;
_configuration = configuration; _configuration = configuration;
_database = database;
_logger = logger; _logger = logger;
} }
public async Task<RequestResult> Grant(string sender, IDictionary<string, object>? data) public async Task<RequestResult> Grant(string sender, IDictionary<string, object>? data)
{ {
if (data == null) if (data == null)
@ -44,9 +40,10 @@ namespace HermesSocketServer.Requests
return new RequestResult(false, null); return new RequestResult(false, null);
} }
_chatters.Set(sender, chatterId, data["voice"].ToString()); string sql = "UPDATE \"TtsChatVoice\" SET \"ttsVoiceId\" = @voice WHERE \"userId\" = @user AND \"chatterId\" = @chatter";
var result = await _database.Execute(sql, data);
_logger.Information($"Updated chatter's [chatter: {data["chatter"]}] selected tts voice [voice: {data["voice"]}] in channel [channel: {sender}]"); _logger.Information($"Updated chatter's [chatter: {data["chatter"]}] selected tts voice [voice: {data["voice"]}] in channel [channel: {sender}]");
return new RequestResult(true, null); return new RequestResult(result == 1, null);
} }
} }
} }

View File

@ -1,6 +1,6 @@
using System.Text.Json; using System.Text.Json;
using HermesSocketLibrary.db;
using HermesSocketLibrary.Requests; using HermesSocketLibrary.Requests;
using HermesSocketServer.Store;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace HermesSocketServer.Requests namespace HermesSocketServer.Requests
@ -8,12 +8,12 @@ namespace HermesSocketServer.Requests
public class UpdateTTSVoice : IRequest public class UpdateTTSVoice : IRequest
{ {
public string Name => "update_tts_voice"; public string Name => "update_tts_voice";
private IStore<string, string> _voices; private Database _database;
private ILogger _logger; private ILogger _logger;
public UpdateTTSVoice(VoiceStore voices, ILogger logger) public UpdateTTSVoice(Database database, ILogger logger)
{ {
_voices = voices; _database = database;
_logger = logger; _logger = logger;
} }
@ -30,9 +30,10 @@ namespace HermesSocketServer.Requests
if (data["voiceid"] is JsonElement id) if (data["voiceid"] is JsonElement id)
data["voiceid"] = id.ToString(); data["voiceid"] = id.ToString();
_voices.Set(data["voiceid"].ToString(), data["voice"].ToString()); string sql = "UPDATE \"TtsVoice\" SET name = @voice WHERE id = @voiceid";
var result = await _database.Execute(sql, data);
_logger.Information($"Updated voice's [voice id: {data["voiceid"]}] name [new name: {data["voice"]}]"); _logger.Information($"Updated voice's [voice id: {data["voiceid"]}] name [new name: {data["voice"]}]");
return new RequestResult(true, null); return new RequestResult(result == 1, null);
} }
} }
} }

View File

@ -52,19 +52,6 @@ namespace HermesSocketLibrary
if (obj.OpCode != 0) if (obj.OpCode != 0)
_logger.Information($"rxm: {message} [ip: {socket.IPAddress}][id: {socket.Id}][name: {socket.Name}][token: {socket.ApiKey}][uid: {socket.UID}]"); _logger.Information($"rxm: {message} [ip: {socket.IPAddress}][id: {socket.Id}][name: {socket.Name}][token: {socket.ApiKey}][uid: {socket.UID}]");
int[] nonProtectedOps = { 0, 1 };
if (string.IsNullOrEmpty(socket.Id) && !nonProtectedOps.Contains(obj.OpCode))
{
_logger.Warning($"An attempt was made to use protected routes while not logged in [ip: {socket.IPAddress}][id: {socket.Id}][name: {socket.Name}][token: {socket.ApiKey}][uid: {socket.UID}]");
return;
}
int[] protectedOps = { 0, 3, 5, 6, 7, 8 };
if (!string.IsNullOrEmpty(socket.Id) && !protectedOps.Contains(obj.OpCode))
{
_logger.Warning($"An attempt was made to use non-protected routes while logged in [ip: {socket.IPAddress}][id: {socket.Id}][name: {socket.Name}][token: {socket.ApiKey}][uid: {socket.UID}]");
return;
}
/** /**
* 0: Heartbeat * 0: Heartbeat
* 1: Login RX * 1: Login RX

View File

@ -18,6 +18,5 @@ namespace HermesSocketServer
public class DatabaseConfiguration public class DatabaseConfiguration
{ {
public string ConnectionString; public string ConnectionString;
public int SaveDelayInSeconds;
} }
} }

View File

@ -1,37 +0,0 @@
using HermesSocketServer.Store;
namespace HermesSocketServer.Services
{
public class DatabaseService : BackgroundService
{
private readonly VoiceStore _voices;
private readonly ChatterStore _chatters;
private readonly ServerConfiguration _configuration;
private readonly Serilog.ILogger _logger;
public DatabaseService(VoiceStore voices, ChatterStore chatters, ServerConfiguration configuration, Serilog.ILogger logger) {
_voices = voices;
_chatters = chatters;
_configuration = configuration;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
_logger.Information("Loading TTS voices...");
await _voices.Load();
_logger.Information("Loading TTS chatters' voice.");
await _chatters.Load();
await Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(_configuration.Database.SaveDelayInSeconds));
await _voices.Save();
await _chatters.Save();
}
});
}
}
}

View File

@ -13,9 +13,6 @@ using Serilog.Events;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using HermesSocketServer.Validators;
using HermesSocketServer.Store;
using HermesSocketServer.Services;
var yamlDeserializer = new DeserializerBuilder() var yamlDeserializer = new DeserializerBuilder()
@ -32,7 +29,7 @@ var configuration = yamlDeserializer.Deserialize<ServerConfiguration>(configCont
if (configuration.Environment.ToUpper() != "QA" && configuration.Environment.ToUpper() != "PROD") if (configuration.Environment.ToUpper() != "QA" && configuration.Environment.ToUpper() != "PROD")
throw new Exception("Invalid environment set."); throw new Exception("Invalid environment set.");
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); builder.Logging.ClearProviders();
builder.Services.Configure<ForwardedHeadersOptions>(options => builder.Services.Configure<ForwardedHeadersOptions>(options =>
@ -60,7 +57,7 @@ builder.Host.UseSerilog(logger);
builder.Logging.AddSerilog(logger); builder.Logging.AddSerilog(logger);
var s = builder.Services; var s = builder.Services;
s.AddSerilog(logger); s.AddSerilog();
s.AddSingleton<ServerConfiguration>(configuration); s.AddSingleton<ServerConfiguration>(configuration);
s.AddSingleton<Database>(); s.AddSingleton<Database>();
@ -75,14 +72,6 @@ s.AddSingleton<ISocketHandler, ChatterHandler>();
s.AddSingleton<ISocketHandler, EmoteDetailsHandler>(); s.AddSingleton<ISocketHandler, EmoteDetailsHandler>();
s.AddSingleton<ISocketHandler, EmoteUsageHandler>(); s.AddSingleton<ISocketHandler, EmoteUsageHandler>();
// Validators
s.AddSingleton<VoiceIdValidator>();
s.AddSingleton<VoiceNameValidator>();
// Stores
s.AddSingleton<VoiceStore>();
s.AddSingleton<ChatterStore>();
// Request handlers // Request handlers
s.AddSingleton<IRequest, GetTTSUsers>(); s.AddSingleton<IRequest, GetTTSUsers>();
s.AddSingleton<IRequest, GetTTSVoices>(); s.AddSingleton<IRequest, GetTTSVoices>();
@ -113,9 +102,6 @@ s.AddSingleton(new JsonSerializerOptions()
}); });
s.AddSingleton<Server>(); s.AddSingleton<Server>();
s.AddHostedService<DatabaseService>();
var app = builder.Build(); var app = builder.Build();
app.UseForwardedHeaders(); app.UseForwardedHeaders();
app.UseSerilogRequestLogging(); app.UseSerilogRequestLogging();
@ -136,7 +122,7 @@ app.Use(async (HttpContext context, RequestDelegate next) =>
{ {
if (context.Request.Path != "/") if (context.Request.Path != "/")
{ {
context.Response.StatusCode = StatusCodes.Status401Unauthorized; context.Response.StatusCode = StatusCodes.Status403Forbidden;
return; return;
} }
@ -149,7 +135,6 @@ app.Use(async (HttpContext context, RequestDelegate next) =>
{ {
context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.StatusCode = StatusCodes.Status400BadRequest;
} }
await next(context);
}); });
await app.RunAsync(); await app.RunAsync();

View File

@ -1,283 +0,0 @@
using System.Collections.Immutable;
using System.Text;
using HermesSocketLibrary.db;
namespace HermesSocketServer.Store
{
public class ChatterStore : IStore<string, long, string>
{
private readonly Database _database;
private readonly Serilog.ILogger _logger;
private readonly IDictionary<string, IDictionary<long, string>> _chatters;
private readonly IDictionary<string, IList<long>> _added;
private readonly IDictionary<string, IList<long>> _modified;
private readonly IDictionary<string, IList<long>> _deleted;
private readonly object _lock;
public ChatterStore(Database database, Serilog.ILogger logger)
{
_database = database;
_logger = logger;
_chatters = new Dictionary<string, IDictionary<long, string>>();
_added = new Dictionary<string, IList<long>>();
_modified = new Dictionary<string, IList<long>>();
_deleted = new Dictionary<string, IList<long>>();
_lock = new object();
}
public string? Get(string user, long key)
{
if (!_chatters.TryGetValue(user, out var broadcaster))
return null;
if (broadcaster.TryGetValue(key, out var chatter))
return chatter;
return null;
}
public IEnumerable<string> Get()
{
return _chatters.Select(c => c.Value).SelectMany(c => c.Values).ToImmutableList();
}
public IDictionary<long, string> Get(string user)
{
if (_chatters.TryGetValue(user, out var chatters))
return chatters.ToImmutableDictionary();
return new Dictionary<long, string>();
}
public async Task Load()
{
string sql = "SELECT \"chatterId\", \"ttsVoiceId\", \"userId\" FROM \"TtsChatVoice\";";
await _database.Execute(sql, new Dictionary<string, object>(), (reader) =>
{
var chatterId = reader.GetInt64(0);
var ttsVoiceId = reader.GetString(1);
var userId = reader.GetString(2);
if (!_chatters.TryGetValue(userId, out var chatters))
{
chatters = new Dictionary<long, string>();
_chatters.Add(userId, chatters);
}
chatters.Add(chatterId, ttsVoiceId);
});
_logger.Information($"Loaded {_chatters.Count} TTS voices from database.");
}
public void Remove(string user, long? key)
{
if (key == null)
return;
lock (_lock)
{
if (_chatters.TryGetValue(user, out var chatters) && chatters.Remove(key.Value))
{
if (!_added.TryGetValue(user, out var added) || !added.Remove(key.Value))
{
if (_modified.TryGetValue(user, out var modified))
modified.Remove(key.Value);
if (!_deleted.TryGetValue(user, out var deleted))
{
deleted = new List<long>();
_deleted.Add(user, deleted);
deleted.Add(key.Value);
}
else if (!deleted.Contains(key.Value))
deleted.Add(key.Value);
}
}
}
}
public void Remove(string? leftKey, long rightKey)
{
throw new NotImplementedException();
}
public async Task<bool> Save()
{
var changes = false;
var sb = new StringBuilder();
var sql = "";
if (_added.Any())
{
int count = _added.Count;
sb.Append("INSERT INTO \"TtsChatVoice\" (\"chatterId\", \"ttsVoiceId\", \"userId\") VALUES ");
lock (_lock)
{
foreach (var broadcaster in _added)
{
var userId = broadcaster.Key;
var user = _chatters[userId];
foreach (var chatterId in broadcaster.Value)
{
var voiceId = user[chatterId];
sb.Append("(")
.Append(chatterId)
.Append(",'")
.Append(voiceId)
.Append("','")
.Append(userId)
.Append("'),");
}
}
sb.Remove(sb.Length - 1, 1)
.Append(';');
sql = sb.ToString();
sb.Clear();
_added.Clear();
}
try
{
_logger.Debug($"About to save {count} voices to database.");
await _database.ExecuteScalar(sql);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to save TTS voices on database: " + sql);
}
changes = true;
}
if (_modified.Any())
{
int count = _modified.Count;
sb.Append("UPDATE \"TtsChatVoice\" as t SET \"ttsVoiceId\" = c.\"ttsVoiceId\" FROM (VALUES ");
lock (_lock)
{
foreach (var broadcaster in _modified)
{
var userId = broadcaster.Key;
var user = _chatters[userId];
foreach (var chatterId in broadcaster.Value)
{
var voiceId = user[chatterId];
sb.Append("(")
.Append(chatterId)
.Append(",'")
.Append(voiceId)
.Append("','")
.Append(userId)
.Append("'),");
}
}
sb.Remove(sb.Length - 1, 1)
.Append(") AS c(\"chatterId\", \"ttsVoiceId\", \"userId\") WHERE \"userId\" = c.\"userId\" AND \"chatterId\" = c.\"chatterId\";");
sql = sb.ToString();
sb.Clear();
_modified.Clear();
}
try
{
_logger.Debug($"About to update {count} voices on the database.");
await _database.ExecuteScalar(sql);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to modify TTS voices on database: " + sql);
}
changes = true;
}
if (_deleted.Any())
{
int count = _deleted.Count;
sb.Append("DELETE FROM \"TtsChatVoice\" WHERE (\"chatterId\", \"userId\") IN (");
lock (_lock)
{
foreach (var broadcaster in _deleted)
{
var userId = broadcaster.Key;
var user = _chatters[userId];
foreach (var chatterId in broadcaster.Value)
{
sb.Append("(")
.Append(chatterId)
.Append(",'")
.Append(userId)
.Append("'),");
}
}
sb.Remove(sb.Length - 1, 1)
.Append(");");
sql = sb.ToString();
sb.Clear();
_deleted.Clear();
}
try
{
_logger.Debug($"About to delete {count} voices from the database.");
await _database.ExecuteScalar(sql);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to modify TTS voices on database: " + sql);
}
changes = true;
}
return changes;
}
public bool Set(string? user, long key, string? value)
{
if (user == null || value == null)
return false;
lock (_lock)
{
if (!_chatters.TryGetValue(user, out var broadcaster))
{
broadcaster = new Dictionary<long, string>();
_chatters.Add(user, broadcaster);
}
if (broadcaster.TryGetValue(key, out var chatter))
{
if (chatter != value)
{
broadcaster[key] = value;
if (!_added.TryGetValue(user, out var added) || !added.Contains(key))
{
if (!_modified.TryGetValue(user, out var modified))
{
modified = new List<long>();
_modified.Add(user, modified);
modified.Add(key);
}
else if (!modified.Contains(key))
modified.Add(key);
}
}
}
else
{
broadcaster.Add(key, value);
_added.TryAdd(user, new List<long>());
if (!_deleted.TryGetValue(user, out var deleted) || !deleted.Remove(key))
{
if (!_added.TryGetValue(user, out var added))
{
added = new List<long>();
_added.Add(user, added);
added.Add(key);
}
else if (!added.Contains(key))
added.Add(key);
}
}
}
return true;
}
}
}

View File

@ -1,22 +0,0 @@
namespace HermesSocketServer.Store
{
public interface IStore<K, V>
{
V? Get(K key);
IDictionary<K, V> Get();
Task Load();
void Remove(K? key);
Task<bool> Save();
bool Set(K? key, V? value);
}
public interface IStore<L, R, V>
{
V? Get(L leftKey, R rightKey);
IDictionary<R, V> Get(L leftKey);
Task Load();
void Remove(L? leftKey, R? rightKey);
Task<bool> Save();
bool Set(L? leftKey, R? rightKey, V? value);
}
}

View File

@ -1,224 +0,0 @@
using System.Collections.Immutable;
using System.Text;
using HermesSocketLibrary.db;
using HermesSocketServer.Validators;
namespace HermesSocketServer.Store
{
public class VoiceStore : IStore<string, string>
{
private readonly Database _database;
private readonly IValidator _voiceIdValidator;
private readonly IValidator _voiceNameValidator;
private readonly Serilog.ILogger _logger;
private readonly IDictionary<string, string> _voices;
private readonly IList<string> _added;
private readonly IList<string> _modified;
private readonly IList<string> _deleted;
private readonly object _lock;
public DateTime PreviousSave;
public VoiceStore(Database database, VoiceIdValidator voiceIdValidator, VoiceNameValidator voiceNameValidator, Serilog.ILogger logger)
{
_database = database;
_voiceIdValidator = voiceIdValidator;
_voiceNameValidator = voiceNameValidator;
_logger = logger;
_voices = new Dictionary<string, string>();
_added = new List<string>();
_modified = new List<string>();
_deleted = new List<string>();
_lock = new object();
PreviousSave = DateTime.UtcNow;
}
public string? Get(string key)
{
if (_voices.TryGetValue(key, out var voice))
return voice;
return null;
}
public IDictionary<string, string> Get()
{
return _voices.ToImmutableDictionary();
}
public async Task Load()
{
string sql = "SELECT id, name FROM \"TtsVoice\";";
await _database.Execute(sql, new Dictionary<string, object>(), (reader) =>
{
var id = reader.GetString(0);
var name = reader.GetString(1);
_voices.Add(id, name);
});
_logger.Information($"Loaded {_voices.Count} TTS voices from database.");
}
public void Remove(string? key)
{
if (key == null)
return;
lock (_lock)
{
if (_voices.ContainsKey(key))
{
_voices.Remove(key);
if (!_added.Remove(key))
{
_modified.Remove(key);
if (!_deleted.Contains(key))
_deleted.Add(key);
}
}
}
}
public async Task<bool> Save()
{
var changes = false;
var sb = new StringBuilder();
var sql = "";
if (_added.Any())
{
int count = _added.Count;
sb.Append("INSERT INTO \"TtsVoice\" (id, name) VALUES ");
lock (_lock)
{
foreach (var voiceId in _added)
{
string voice = _voices[voiceId];
sb.Append("('")
.Append(voiceId)
.Append("','")
.Append(voice)
.Append("'),");
}
sb.Remove(sb.Length - 1, 1)
.Append(';');
sql = sb.ToString();
sb.Clear();
_added.Clear();
}
try
{
_logger.Debug($"About to save {count} voices to database.");
await _database.ExecuteScalar(sql);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to save TTS voices on database: " + sql);
}
changes = true;
}
if (_modified.Any())
{
int count = _modified.Count;
sb.Append("UPDATE \"TtsVoice\" as t SET name = c.name FROM (VALUES ");
lock (_lock)
{
foreach (var voiceId in _modified)
{
string voice = _voices[voiceId];
sb.Append("('")
.Append(voiceId)
.Append("','")
.Append(voice)
.Append("'),");
}
sb.Remove(sb.Length - 1, 1)
.Append(") AS c(id, name) WHERE id = c.id;");
sql = sb.ToString();
sb.Clear();
_modified.Clear();
}
try
{
_logger.Debug($"About to update {count} voices on the database.");
await _database.ExecuteScalar(sql);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to modify TTS voices on database: " + sql);
}
changes = true;
}
if (_deleted.Any())
{
int count = _deleted.Count;
sb.Append("DELETE FROM \"TtsVoice\" WHERE id IN (");
lock (_lock)
{
foreach (var voiceId in _deleted)
{
sb.Append("'")
.Append(voiceId)
.Append("',");
}
sb.Remove(sb.Length - 1, 1)
.Append(");");
sql = sb.ToString();
sb.Clear();
_deleted.Clear();
}
try
{
_logger.Debug($"About to delete {count} voices from the database.");
await _database.ExecuteScalar(sql);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to modify TTS voices on database: " + sql);
}
changes = true;
}
return changes;
}
public bool Set(string? key, string? value)
{
if (key == null || value == null)
return false;
_voiceNameValidator.Check(value);
lock (_lock)
{
if (_voices.TryGetValue(key, out var voice))
{
if (voice != value)
{
_voices[key] = value;
if (!_added.Contains(key) && !_modified.Contains(key))
_modified.Add(key);
}
}
else
{
_voiceIdValidator.Check(key);
_voices.Add(key, value);
if (!_deleted.Remove(key) && !_added.Contains(key))
_added.Add(key);
}
}
return true;
}
}
}

View File

@ -1,7 +0,0 @@
namespace HermesSocketServer.Validators
{
public interface IValidator
{
bool Check(string? input);
}
}

View File

@ -1,29 +0,0 @@
using System.Text.RegularExpressions;
namespace HermesSocketServer.Validators
{
public class RegexValidator : IValidator
{
private readonly Regex _regex;
private readonly int _minimum;
private readonly int _maximum;
public RegexValidator(string regex, RegexOptions options, int minimum, int maximum) {
_regex = new Regex(regex, options | RegexOptions.Compiled);
_minimum = minimum;
_maximum = maximum;
}
public bool Check(string? input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
if (input.Length < _minimum)
throw new ArgumentException("Too short. Must be of length 8 or greater.", nameof(input));
if (input.Length > _maximum)
throw new ArgumentException("Too long. Must be of length 24 or less.", nameof(input));
return _regex.IsMatch(input);
}
}
}

View File

@ -1,10 +0,0 @@
using System.Text.RegularExpressions;
namespace HermesSocketServer.Validators
{
public class VoiceIdValidator : RegexValidator
{
public VoiceIdValidator()
: base("^[a-z0-9]{25}$", RegexOptions.IgnoreCase, 25, 25) { }
}
}

View File

@ -1,10 +0,0 @@
using System.Text.RegularExpressions;
namespace HermesSocketServer.Validators
{
public class VoiceNameValidator : RegexValidator
{
public VoiceNameValidator()
: base("^[a-z0-9_\\-]{2,32}$", RegexOptions.IgnoreCase, 2, 32) { }
}
}

View File

@ -79,13 +79,13 @@ namespace HermesSocketLibrary.db
} }
} }
public async Task<int> Execute(string sql, Action<NpgsqlCommand> prepare) public async Task<int> Execute(string sql, Action<NpgsqlCommand> action)
{ {
using (var connection = await _source.OpenConnectionAsync()) using (var connection = await _source.OpenConnectionAsync())
{ {
using (var command = new NpgsqlCommand(sql, connection)) using (var command = new NpgsqlCommand(sql, connection))
{ {
prepare(command); action(command);
await command.PrepareAsync(); await command.PrepareAsync();
return await command.ExecuteNonQueryAsync(); return await command.ExecuteNonQueryAsync();