Current state of the websocket server for the Hermes client
This commit is contained in:
commit
95bc073a73
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
bin/
|
||||
obj/
|
||||
appsettings*.json
|
||||
server.config*.yml
|
||||
logs/
|
13
Properties/launchSettings.json
Normal file
13
Properties/launchSettings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:15891",
|
||||
"sslPort": 44309
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
}
|
||||
}
|
136
Quests/QuestManager.cs
Normal file
136
Quests/QuestManager.cs
Normal file
@ -0,0 +1,136 @@
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Quests;
|
||||
using HermesSocketLibrary.Quests.Tasks;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Quests
|
||||
{
|
||||
public class QuestManager
|
||||
{
|
||||
private IDictionary<short, Quest> _quests;
|
||||
private IDictionary<long, IList<ChatterQuestProgression>> _progression;
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
private Random _random;
|
||||
|
||||
public QuestManager(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
|
||||
_quests = new Dictionary<short, Quest>();
|
||||
_progression = new Dictionary<long, IList<ChatterQuestProgression>>();
|
||||
_random = new Random();
|
||||
}
|
||||
|
||||
public async Task AddNewDailyQuests(DateOnly date, int count)
|
||||
{
|
||||
var midnight = date.ToDateTime(TimeOnly.MinValue);
|
||||
var quests = _quests.Values.Select(q => q.StartTime.Date == midnight);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
IQuestTask task;
|
||||
var questType = _random.Next(2);
|
||||
if (questType == 0)
|
||||
{
|
||||
task = new MessageQuestTask(_random.Next(21) + 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
task = new EmoteMessageQuestTask(_random.Next(11) + 5);
|
||||
}
|
||||
var temp = new DailyQuest(-1, task, date);
|
||||
|
||||
string sql = "INSERT INTO \"Quest\" (type, target, start, end) VALUES (@type, @target, @start, @end)";
|
||||
await _database.Execute(sql, c =>
|
||||
{
|
||||
c.Parameters.AddWithValue("@type", temp.Type);
|
||||
c.Parameters.AddWithValue("@target", temp.Task.Target);
|
||||
c.Parameters.AddWithValue("@start", temp.StartTime);
|
||||
c.Parameters.AddWithValue("@end", temp.EndTime);
|
||||
});
|
||||
|
||||
string sql2 = "SELECT id FROM \"Quest\" WHERE type = @type AND start = @start";
|
||||
int? questId = (int?)await _database.ExecuteScalar(sql, c =>
|
||||
{
|
||||
c.Parameters.AddWithValue("@type", temp.Type);
|
||||
c.Parameters.AddWithValue("@start", temp.StartTime);
|
||||
});
|
||||
|
||||
var quest = new DailyQuest((short)questId.Value, task, date);
|
||||
_quests.Add(quest.Id, quest);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddNewWeeklyQuests(DateOnly date, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
IQuestTask task;
|
||||
var questType = _random.Next(2);
|
||||
if (questType == 0)
|
||||
{
|
||||
task = new MessageQuestTask(_random.Next(21) + 10);
|
||||
}
|
||||
else
|
||||
{
|
||||
task = new EmoteMessageQuestTask(_random.Next(11) + 5);
|
||||
}
|
||||
var temp = new WeeklyQuest(-1, task, date);
|
||||
|
||||
string sql = "INSERT INTO \"Quest\" (type, target, start, end) VALUES (@type, @target, @start, @end)";
|
||||
await _database.Execute(sql, c =>
|
||||
{
|
||||
c.Parameters.AddWithValue("@type", temp.Type);
|
||||
c.Parameters.AddWithValue("@target", temp.Task.Target);
|
||||
c.Parameters.AddWithValue("@start", temp.StartTime);
|
||||
c.Parameters.AddWithValue("@end", temp.EndTime);
|
||||
});
|
||||
|
||||
string sql2 = "SELECT id FROM \"Quest\" WHERE type = @type AND start = @start";
|
||||
int? questId = (int?)await _database.ExecuteScalar(sql, c =>
|
||||
{
|
||||
c.Parameters.AddWithValue("@type", temp.Type);
|
||||
c.Parameters.AddWithValue("@start", temp.StartTime);
|
||||
});
|
||||
|
||||
var quest = new WeeklyQuest((short)questId.Value, task, date);
|
||||
_quests.Add(quest.Id, quest);
|
||||
}
|
||||
}
|
||||
|
||||
public void Process(long chatterId, string message, HashSet<string> emotes)
|
||||
{
|
||||
if (!_progression.TryGetValue(chatterId, out IList<ChatterQuestProgression>? progressions) || progressions == null || !progressions.Any())
|
||||
return;
|
||||
|
||||
foreach (var progression in progressions)
|
||||
{
|
||||
if (!progression.Quest.IsOngoing())
|
||||
continue;
|
||||
|
||||
_logger.Information($"Quest {progression.Quest.Task.Name} [id: {progression.Quest.Id}][progression: {progression.Counter}/{progression.Quest.Task.Target}]");
|
||||
progression.Process(message, emotes);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateQuests(DateOnly date, int dailies, int weeklies)
|
||||
{
|
||||
var dateMidnight = date.ToDateTime(TimeOnly.MinValue);
|
||||
var dquests = _quests.Values.Select(q => q.StartTime.Date == dateMidnight);
|
||||
var monday = date.AddDays((date.DayOfWeek - DayOfWeek.Monday + 7) % 7);
|
||||
var mondayMidnight = monday.ToDateTime(TimeOnly.MinValue);
|
||||
var wquests = _quests.Values.Select(q => q.StartTime >= mondayMidnight);
|
||||
|
||||
if (dquests.Count() < dailies)
|
||||
{
|
||||
await AddNewDailyQuests(date, dailies - dquests.Count());
|
||||
}
|
||||
|
||||
if (wquests.Count() < weeklies)
|
||||
{
|
||||
await AddNewWeeklyQuests(monday, dailies - wquests.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
Requests/BanTTSUser.cs
Normal file
34
Requests/BanTTSUser.cs
Normal file
@ -0,0 +1,34 @@
|
||||
// using System.Text.Json;
|
||||
// using HermesSocketLibrary.db;
|
||||
// using HermesSocketLibrary.Requests;
|
||||
|
||||
// namespace HermesSocketServer.Requests
|
||||
// {
|
||||
// public class BanTTSUser : IRequest
|
||||
// {
|
||||
// public string Name => "ban_tts_user";
|
||||
// private Database _database;
|
||||
// private ILogger _logger;
|
||||
|
||||
// public BanTTSUser(Database database, ILogger logger)
|
||||
// {
|
||||
// _database = database;
|
||||
// _logger = logger;
|
||||
// }
|
||||
|
||||
// public async Task<RequestResult> Grant(IDictionary<string, object> data)
|
||||
// {
|
||||
// // if (long.TryParse(data["user"].ToString(), out long user))
|
||||
// // data["user"] = user;
|
||||
// // if (data["broadcaster"] is JsonElement b)
|
||||
// // data["broadcaster"] = b.ToString();
|
||||
// // if (data["voice"] is JsonElement v)
|
||||
// // data["voice"] = v.ToString();
|
||||
|
||||
// // string sql = "UPDATE \"TtsChatVoice\" (\"broadcasterId\", \"chatterId\", \"ttsVoiceId\") VALUES (@broadcaster, @user, @voice)";
|
||||
// // var result = await _database.Execute(sql, data);
|
||||
// // _logger.Information($"Selected a tts voice for {data["user"]} in channel {data["broadcaster"]}: {data["voice"]}");
|
||||
// return new RequestResult(1 == 1, null);
|
||||
// }
|
||||
// }
|
||||
// }
|
39
Requests/CreateTTSUser.cs
Normal file
39
Requests/CreateTTSUser.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class CreateTTSUser : IRequest
|
||||
{
|
||||
public string Name => "create_tts_user";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
|
||||
public CreateTTSUser(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
if (long.TryParse(data["chatter"].ToString(), out long chatter))
|
||||
data["chatter"] = chatter;
|
||||
if (data["voice"] is JsonElement v)
|
||||
data["voice"] = v.ToString();
|
||||
data["user"] = sender;
|
||||
|
||||
var check = await _database.ExecuteScalar("SELECT state FROM \"TtsVoiceState\" WHERE \"userId\" = @user AND \"ttsVoiceId\" = @voice", data) ?? false;
|
||||
if (check is not bool state || !state){
|
||||
return new RequestResult(false, null);
|
||||
}
|
||||
|
||||
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 for {data["chatter"]} in channel {data["user"]}: {data["voice"]}");
|
||||
return new RequestResult(result == 1, null);
|
||||
}
|
||||
}
|
||||
}
|
46
Requests/CreateTTSVoice.cs
Normal file
46
Requests/CreateTTSVoice.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class CreateTTSVoice : IRequest
|
||||
{
|
||||
public string Name => "create_tts_voice";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
private Random _random;
|
||||
|
||||
public CreateTTSVoice(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
_random = new Random();
|
||||
}
|
||||
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
string id = RandomString(25);
|
||||
data.Add("idd", id);
|
||||
|
||||
if (data["voice"] is JsonElement v)
|
||||
data["voice"] = v.ToString();
|
||||
|
||||
string sql = "INSERT INTO \"TtsVoice\" (id, name) VALUES (@idd, @voice)";
|
||||
var result = await _database.Execute(sql, data);
|
||||
_logger.Information($"Added a new voice: {data["voice"]} (id: {data["idd"]})");
|
||||
|
||||
data.Remove("idd");
|
||||
return new RequestResult(result == 1, id);
|
||||
}
|
||||
|
||||
private string RandomString(int length)
|
||||
{
|
||||
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return new string(Enumerable.Repeat(chars, length)
|
||||
.Select(s => s[_random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
31
Requests/DeleteTTSVoice.cs
Normal file
31
Requests/DeleteTTSVoice.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class DeleteTTSVoice : IRequest
|
||||
{
|
||||
public string Name => "delete_tts_voice";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
|
||||
public DeleteTTSVoice(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
if (data["voice"] is JsonElement v)
|
||||
data["voice"] = v.ToString();
|
||||
|
||||
string sql = "DELETE FROM \"TtsVoice\" WHERE id = @voice";
|
||||
var result = await _database.Execute(sql, data);
|
||||
_logger.Information($"Deleted a voice by id: {data["voice"]}");
|
||||
return new RequestResult(result == 1, null);
|
||||
}
|
||||
}
|
||||
}
|
29
Requests/GetChatterIds.cs
Normal file
29
Requests/GetChatterIds.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class GetChatterIds : IRequest
|
||||
{
|
||||
public string Name => "get_chatter_ids";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
|
||||
public GetChatterIds(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
IList<long> ids = new List<long>();
|
||||
string sql = $"SELECT id FROM \"Chatter\"";
|
||||
await _database.Execute(sql, data, (r) => ids.Add(r.GetInt64(0)));
|
||||
_logger.Information($"Fetched all chatters.");
|
||||
return new RequestResult(true, ids, notifyClientsOnAccount: false);
|
||||
}
|
||||
}
|
||||
}
|
34
Requests/GetEmotes.cs
Normal file
34
Requests/GetEmotes.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class GetEmotes : IRequest
|
||||
{
|
||||
public string Name => "get_emotes";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
|
||||
public GetEmotes(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
IList<EmoteInfo> emotes = new List<EmoteInfo>();
|
||||
string sql = $"SELECT id, name FROM \"Emote\"";
|
||||
await _database.Execute(sql, data, (r) => emotes.Add(new EmoteInfo()
|
||||
{
|
||||
Id = r.GetString(0),
|
||||
Name = r.GetString(1)
|
||||
}));
|
||||
_logger.Information($"Fetched all emotes.");
|
||||
return new RequestResult(true, emotes, notifyClientsOnAccount: false);
|
||||
}
|
||||
}
|
||||
}
|
31
Requests/GetTTSUsers.cs
Normal file
31
Requests/GetTTSUsers.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class GetTTSUsers : IRequest
|
||||
{
|
||||
public string Name => "get_tts_users";
|
||||
private readonly Database _database;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public GetTTSUsers(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
data["user"] = sender;
|
||||
|
||||
IDictionary<long, string> users = new Dictionary<long, string>();
|
||||
string sql = $"SELECT \"ttsVoiceId\", \"chatterId\" FROM \"TtsChatVoice\" WHERE \"userId\" = @user";
|
||||
await _database.Execute(sql, data, (r) => users.Add(r.GetInt64(1), r.GetString(0)));
|
||||
_logger.Information($"Fetched all chatters' selected tts voice for channel {data["user"]}.");
|
||||
return new RequestResult(true, users, notifyClientsOnAccount: false);
|
||||
}
|
||||
}
|
||||
}
|
33
Requests/GetTTSVoices.cs
Normal file
33
Requests/GetTTSVoices.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class GetTTSVoices : IRequest
|
||||
{
|
||||
public string Name => "get_tts_voices";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
|
||||
public GetTTSVoices(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
IList<VoiceDetails> voices = new List<VoiceDetails>();
|
||||
string sql = "SELECT id, name FROM \"TtsVoice\"";
|
||||
await _database.Execute(sql, data, (r) => voices.Add(new VoiceDetails()
|
||||
{
|
||||
Id = r.GetString(0),
|
||||
Name = r.GetString(1)
|
||||
}));
|
||||
_logger.Information("Fetched all TTS voices.");
|
||||
return new RequestResult(true, voices, notifyClientsOnAccount: false);
|
||||
}
|
||||
}
|
||||
}
|
36
Requests/GetTTSWordFilters.cs
Normal file
36
Requests/GetTTSWordFilters.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using HermesSocketLibrary.Requests.Messages;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class GetTTSWordFilters : IRequest
|
||||
{
|
||||
public string Name => "get_tts_word_filters";
|
||||
private readonly Database _database;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public GetTTSWordFilters(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
data["user"] = sender;
|
||||
|
||||
IList<TTSWordFilter> filters = new List<TTSWordFilter>();
|
||||
string sql = $"SELECT id, search, replace FROM \"TtsWordFilter\" WHERE \"userId\" = @user";
|
||||
await _database.Execute(sql, data, (r) => filters.Add(new TTSWordFilter()
|
||||
{
|
||||
Id = r.GetString(0),
|
||||
Search = r.GetString(1),
|
||||
Replace = r.GetString(2)
|
||||
}));
|
||||
return new RequestResult(true, filters, notifyClientsOnAccount: false);
|
||||
}
|
||||
}
|
||||
}
|
14
Requests/ServerRequestManager.cs
Normal file
14
Requests/ServerRequestManager.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer
|
||||
{
|
||||
public class ServerRequestManager : RequestManager
|
||||
{
|
||||
public ServerRequestManager(IServiceProvider serviceProvider, ILogger logger) : base(serviceProvider, logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string AssemblyName => "SocketServer";
|
||||
}
|
||||
}
|
40
Requests/UpdateTTSUser.cs
Normal file
40
Requests/UpdateTTSUser.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class UpdateTTSUser : IRequest
|
||||
{
|
||||
public string Name => "update_tts_user";
|
||||
private readonly Database _database;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public UpdateTTSUser(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
if (long.TryParse(data["chatter"].ToString(), out long chatterId))
|
||||
data["chatter"] = chatterId;
|
||||
if (data["voice"] is JsonElement v)
|
||||
data["voice"] = v.ToString();
|
||||
data["user"] = sender;
|
||||
|
||||
var check = await _database.ExecuteScalar("SELECT state FROM \"TtsVoiceState\" WHERE \"userId\" = @user AND \"ttsVoiceId\" = @voice", data) ?? false;
|
||||
if (check is not bool state || !state)
|
||||
{
|
||||
return new RequestResult(false, null);
|
||||
}
|
||||
|
||||
string sql = "UPDATE \"TtsChatVoice\" SET \"ttsVoiceId\" = @voice WHERE \"userId\" = @user AND \"chatterId\" = @chatter";
|
||||
var result = await _database.Execute(sql, data);
|
||||
_logger.Information($"Updated {data["chatter"]}'s selected tts voice to {data["voice"]} in channel {data["user"]}.");
|
||||
return new RequestResult(result == 1, null);
|
||||
}
|
||||
}
|
||||
}
|
33
Requests/UpdateTTSVoice.cs
Normal file
33
Requests/UpdateTTSVoice.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class UpdateTTSVoice : IRequest
|
||||
{
|
||||
public string Name => "update_tts_voice";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
|
||||
public UpdateTTSVoice(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
if (data["voice"] is JsonElement v)
|
||||
data["voice"] = v.ToString();
|
||||
if (data["voiceid"] is JsonElement id)
|
||||
data["voiceid"] = id.ToString();
|
||||
|
||||
string sql = "UPDATE \"TtsVoice\" SET name = @voice WHERE id = @voiceid";
|
||||
var result = await _database.Execute(sql, data);
|
||||
_logger.Information($"Updated voice {data["voiceid"]}'s name to {data["voice"]}.");
|
||||
return new RequestResult(result == 1, null);
|
||||
}
|
||||
}
|
||||
}
|
35
Requests/UpdateTTSVoiceState.cs
Normal file
35
Requests/UpdateTTSVoiceState.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Requests
|
||||
{
|
||||
public class UpdateTTSVoiceState : IRequest
|
||||
{
|
||||
public string Name => "update_tts_voice_state";
|
||||
private Database _database;
|
||||
private ILogger _logger;
|
||||
|
||||
public UpdateTTSVoiceState(Database database, ILogger logger)
|
||||
{
|
||||
_database = database;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RequestResult> Grant(string sender, IDictionary<string, object> data)
|
||||
{
|
||||
if (data["voice"] is JsonElement voice)
|
||||
data["voice"] = voice.ToString();
|
||||
if (data["state"] is JsonElement state)
|
||||
data["state"] = state.ToString() == "True";
|
||||
data["user"] = sender;
|
||||
|
||||
//string sql = "UPDATE \"TtsVoiceState\" SET state = @state WHERE \"userId\" = @user AND \"ttsVoiceId\" = @voice";
|
||||
string sql = "INSERT INTO \"TtsVoiceState\" (\"userId\", \"ttsVoiceId\", state) VALUES (@user, @voice, @state) ON CONFLICT (\"userId\", \"ttsVoiceId\") DO UPDATE SET state = @state";
|
||||
var result = await _database.Execute(sql, data);
|
||||
_logger.Information($"Updated voice {data["voice"]}'s state (from {data["user"]}) to {data["state"]}.");
|
||||
return new RequestResult(result == 1, null);
|
||||
}
|
||||
}
|
||||
}
|
106
Server.cs
Normal file
106
Server.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using HermesSocketServer.Socket;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketLibrary
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
private readonly HermesSocketManager _sockets;
|
||||
private readonly SocketHandlerManager _handlers;
|
||||
private readonly JsonSerializerOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
||||
public Server(
|
||||
HermesSocketManager sockets,
|
||||
SocketHandlerManager handlers,
|
||||
JsonSerializerOptions options,
|
||||
ILogger logger
|
||||
)
|
||||
{
|
||||
_sockets = sockets;
|
||||
_handlers = handlers;
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public async Task Handle(WebSocketUser socket, HttpContext context)
|
||||
{
|
||||
_logger.Information($"Socket connected [ip: {socket.IPAddress}]");
|
||||
_sockets.Add(socket);
|
||||
var buffer = new byte[1024 * 8];
|
||||
|
||||
while (socket.State == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await socket.Receive(new ArraySegment<byte>(buffer));
|
||||
if (result == null || result.MessageType == WebSocketMessageType.Close || !socket.Connected)
|
||||
break;
|
||||
|
||||
string message = Encoding.UTF8.GetString(buffer, 0, result.Count).TrimEnd('\0');
|
||||
var obj = JsonSerializer.Deserialize<SocketMessage>(message, _options);
|
||||
if (obj == null || !obj.OpCode.HasValue)
|
||||
continue;
|
||||
|
||||
if (obj.OpCode != 0)
|
||||
_logger.Information("Message: " + message);
|
||||
|
||||
/**
|
||||
* 0: Heartbeat
|
||||
* 1: Login RX
|
||||
* 2: Login Ack TX
|
||||
* 3: Request RX
|
||||
* 4: Request Ack TX
|
||||
* 5: Error RX/TX
|
||||
*/
|
||||
if (obj.Data == null)
|
||||
{
|
||||
await socket.Send(5, new ErrorMessage("Received no data in the message."));
|
||||
continue;
|
||||
}
|
||||
else if (obj.OpCode == 0)
|
||||
obj.Data = JsonSerializer.Deserialize<HeartbeatMessage>(obj.Data.ToString(), _options);
|
||||
else if (obj.OpCode == 1)
|
||||
obj.Data = JsonSerializer.Deserialize<HermesLoginMessage>(obj.Data.ToString(), _options);
|
||||
else if (obj.OpCode == 3)
|
||||
obj.Data = JsonSerializer.Deserialize<RequestMessage>(obj.Data.ToString(), _options);
|
||||
else if (obj.OpCode == 5)
|
||||
obj.Data = JsonSerializer.Deserialize<ErrorMessage>(obj.Data.ToString(), _options);
|
||||
else if (obj.OpCode == 6)
|
||||
obj.Data = JsonSerializer.Deserialize<ChatterMessage>(obj.Data.ToString(), _options);
|
||||
else if (obj.OpCode == 7)
|
||||
obj.Data = JsonSerializer.Deserialize<EmoteDetailsMessage>(obj.Data.ToString(), _options);
|
||||
else if (obj.OpCode == 8)
|
||||
obj.Data = JsonSerializer.Deserialize<EmoteUsageMessage>(obj.Data.ToString(), _options);
|
||||
else
|
||||
{
|
||||
await socket.Send(5, new ErrorMessage("Received an invalid message: " + message));
|
||||
continue;
|
||||
}
|
||||
await _handlers.Execute(socket, obj.OpCode.Value, obj.Data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Error trying to process a socket message");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (socket.Connected)
|
||||
await socket.Close(socket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, socket.CloseStatusDescription, CancellationToken.None);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_sockets.Remove(socket);
|
||||
}
|
||||
_logger.Information($"Client disconnected [username: {socket.Name}][id: {socket.Id}][ip: {socket.IPAddress}]");
|
||||
}
|
||||
}
|
||||
}
|
20
ServerConfiguration.cs
Normal file
20
ServerConfiguration.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace HermesSocketServer
|
||||
{
|
||||
public class ServerConfiguration
|
||||
{
|
||||
public string Environment;
|
||||
public WebsocketServerConfiguration WebsocketServer;
|
||||
public DatabaseConfiguration Database;
|
||||
}
|
||||
|
||||
public class WebsocketServerConfiguration
|
||||
{
|
||||
public string Host;
|
||||
public string Port;
|
||||
}
|
||||
|
||||
public class DatabaseConfiguration
|
||||
{
|
||||
public string ConnectionString;
|
||||
}
|
||||
}
|
57
Socket/Handlers/ChatterHandler.cs
Normal file
57
Socket/Handlers/ChatterHandler.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
Socket/Handlers/EmoteDetailsHandler.cs
Normal file
77
Socket/Handlers/EmoteDetailsHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
75
Socket/Handlers/EmoteUsageHandler.cs
Normal file
75
Socket/Handlers/EmoteUsageHandler.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
29
Socket/Handlers/ErrorHandler.cs
Normal file
29
Socket/Handlers/ErrorHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
33
Socket/Handlers/HeartbeatHandler.cs
Normal file
33
Socket/Handlers/HeartbeatHandler.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
83
Socket/Handlers/HermesLoginHandler.cs
Normal file
83
Socket/Handlers/HermesLoginHandler.cs
Normal 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}]");
|
||||
}
|
||||
}
|
||||
}
|
10
Socket/Handlers/ISocketHandler.cs
Normal file
10
Socket/Handlers/ISocketHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
71
Socket/Handlers/RequestHandler.cs
Normal file
71
Socket/Handlers/RequestHandler.cs
Normal 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}].");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
Socket/SocketHandlerManager.cs
Normal file
34
Socket/SocketHandlerManager.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Text.Json;
|
||||
using CommonSocketLibrary.Abstract;
|
||||
using HermesSocketServer.Socket.Handlers;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Socket
|
||||
{
|
||||
public class SocketHandlerManager : HandlerManager<WebSocketUser, ISocketHandler>
|
||||
{
|
||||
private readonly HermesSocketManager _sockets;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
|
||||
public SocketHandlerManager(HermesSocketManager sockets, IServiceProvider serviceProvider, ILogger logger)
|
||||
: base(logger)
|
||||
{
|
||||
_sockets = sockets;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
Add(0, _serviceProvider.GetRequiredKeyedService<ISocketHandler>("hermes-heartbeat"));
|
||||
Add(1, _serviceProvider.GetRequiredKeyedService<ISocketHandler>("hermes-hermeslogin"));
|
||||
Add(3, _serviceProvider.GetRequiredKeyedService<ISocketHandler>("hermes-request"));
|
||||
Add(5, _serviceProvider.GetRequiredKeyedService<ISocketHandler>("hermes-error"));
|
||||
Add(6, _serviceProvider.GetRequiredKeyedService<ISocketHandler>("hermes-chatter"));
|
||||
Add(7, _serviceProvider.GetRequiredKeyedService<ISocketHandler>("hermes-emotedetails"));
|
||||
Add(8, _serviceProvider.GetRequiredKeyedService<ISocketHandler>("hermes-emoteusage"));
|
||||
}
|
||||
|
||||
protected override async Task Execute<T>(WebSocketUser sender, ISocketHandler handler, T value)
|
||||
{
|
||||
await handler.Execute(sender, value, _sockets);
|
||||
}
|
||||
}
|
||||
}
|
96
Socket/SocketManager.cs
Normal file
96
Socket/SocketManager.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Timers;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Socket
|
||||
{
|
||||
public class HermesSocketManager
|
||||
{
|
||||
private IList<WebSocketUser> _sockets;
|
||||
private System.Timers.Timer _timer;
|
||||
private ILogger _logger;
|
||||
|
||||
|
||||
public HermesSocketManager(ILogger logger)
|
||||
{
|
||||
_sockets = new List<WebSocketUser>();
|
||||
_timer = new System.Timers.Timer(TimeSpan.FromSeconds(1));
|
||||
_timer.AutoReset = true;
|
||||
_timer.Elapsed += async (sender, e) => await HandleHeartbeats(e);
|
||||
_timer.Enabled = true;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public void Add(WebSocketUser socket)
|
||||
{
|
||||
_sockets.Add(socket);
|
||||
}
|
||||
|
||||
public IList<WebSocketUser> GetAllSockets()
|
||||
{
|
||||
return _sockets.AsReadOnly();
|
||||
}
|
||||
|
||||
public IEnumerable<WebSocketUser> GetSockets(string userId)
|
||||
{
|
||||
foreach (var socket in _sockets)
|
||||
{
|
||||
if (socket.Id == userId)
|
||||
yield return socket;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(WebSocketUser socket)
|
||||
{
|
||||
return _sockets.Remove(socket);
|
||||
}
|
||||
|
||||
private async Task HandleHeartbeats(ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var signalTime = e.SignalTime.ToUniversalTime();
|
||||
for (var i = 0; i < _sockets.Count; i++)
|
||||
{
|
||||
var socket = _sockets[i];
|
||||
if (!socket.Connected)
|
||||
{
|
||||
_sockets.RemoveAt(i--);
|
||||
}
|
||||
else if (signalTime - socket.LastHeartbeatReceived > TimeSpan.FromSeconds(30))
|
||||
{
|
||||
if (socket.LastHeartbeatReceived > socket.LastHearbeatSent)
|
||||
{
|
||||
try
|
||||
{
|
||||
socket.LastHearbeatSent = DateTime.UtcNow;
|
||||
await socket.Send(0, new HeartbeatMessage() { DateTime = socket.LastHearbeatSent });
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warning($"Failed to send the heartbeat to socket [ip: {socket.IPAddress}].");
|
||||
await socket.Close(WebSocketCloseStatus.NormalClosure, "Failed to send a heartbeat message.", CancellationToken.None);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!socket.Connected)
|
||||
_sockets.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
else if (signalTime - socket.LastHeartbeatReceived > TimeSpan.FromSeconds(120))
|
||||
{
|
||||
_logger.Debug($"Closing socket [ip: {socket.IPAddress}] for not responding for 2 minutes.");
|
||||
await socket.Close(WebSocketCloseStatus.NormalClosure, "No heartbeat received.", CancellationToken.None);
|
||||
_sockets.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
Socket/WebSocketUser.cs
Normal file
113
Socket/WebSocketUser.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary.Socket.Data;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace HermesSocketServer.Socket
|
||||
{
|
||||
public class WebSocketUser
|
||||
{
|
||||
private readonly WebSocket _socket;
|
||||
private readonly JsonSerializerOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IPAddress? _ipAddress;
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _connected;
|
||||
|
||||
public WebSocketCloseStatus? CloseStatus { get => _socket.CloseStatus; }
|
||||
public string? CloseStatusDescription { get => _socket.CloseStatusDescription; }
|
||||
public WebSocketState State { get => _socket.State; }
|
||||
public IPAddress? IPAddress { get => _ipAddress; }
|
||||
public bool Connected { get => _connected; }
|
||||
public string? Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public DateTime LastHeartbeatReceived { get; set; }
|
||||
public DateTime LastHearbeatSent { get; set; }
|
||||
public CancellationToken Token { get => _cts.Token; }
|
||||
|
||||
|
||||
public WebSocketUser(WebSocket socket, IPAddress? ipAddress, JsonSerializerOptions options, ILogger logger)
|
||||
{
|
||||
_socket = socket;
|
||||
_ipAddress = ipAddress;
|
||||
_options = options;
|
||||
_connected = true;
|
||||
_logger = logger;
|
||||
_cts = new CancellationTokenSource();
|
||||
LastHeartbeatReceived = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
|
||||
public async Task Close(WebSocketCloseStatus status, string? message, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _socket.CloseAsync(status, message ?? CloseStatusDescription, token);
|
||||
}
|
||||
catch (WebSocketException wse) when (wse.Message.StartsWith("The WebSocket is in an invalid state "))
|
||||
{
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Failed to close socket.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connected = false;
|
||||
await _cts.CancelAsync();
|
||||
_cts = new CancellationTokenSource();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Send<Data>(int opcode, Data data)
|
||||
{
|
||||
var message = GenerateMessage(opcode, data);
|
||||
var content = JsonSerializer.Serialize(message, _options);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
var array = new ArraySegment<byte>(bytes);
|
||||
var total = bytes.Length;
|
||||
var current = 0;
|
||||
|
||||
while (current < total)
|
||||
{
|
||||
var size = Encoding.UTF8.GetBytes(content.Substring(current), array);
|
||||
await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, Token);
|
||||
current += size;
|
||||
}
|
||||
|
||||
_logger.Verbose($"TX #{opcode}: {content}");
|
||||
}
|
||||
|
||||
public async Task<WebSocketReceiveResult?> Receive(ArraySegment<byte> bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _socket.ReceiveAsync(bytes, Token);
|
||||
}
|
||||
catch (WebSocketException wse) when (wse.Message.StartsWith("The remote party "))
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to receive a web socket message.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SocketMessage GenerateMessage<Data>(int opcode, Data data)
|
||||
{
|
||||
return new SocketMessage()
|
||||
{
|
||||
OpCode = opcode,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
35
SocketServer.csproj
Normal file
35
SocketServer.csproj
Normal file
@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CommonSocketLibrary\CommonSocketLibrary.csproj" />
|
||||
<ProjectReference Include="..\HermesSocketLibrary\HermesSocketLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MathParser.org-mXparser" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2-dev-00338" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.1-dev-10391" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00972" />
|
||||
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.1-dev-00771" />
|
||||
<PackageReference Include="Serilog.Sinks.Trace" Version="4.0.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="15.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>HermesSocketServer</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
119
Startup.cs
Normal file
119
Startup.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using HermesSocketLibrary;
|
||||
using HermesSocketLibrary.db;
|
||||
using HermesSocketLibrary.Requests;
|
||||
using HermesSocketServer;
|
||||
using HermesSocketServer.Requests;
|
||||
using HermesSocketServer.Socket;
|
||||
using HermesSocketServer.Socket.Handlers;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
var configFileName = "server.config.yml";
|
||||
if (File.Exists("server.config." + Environment.GetEnvironmentVariable("TTS_ENV").ToLower() + ".yml"))
|
||||
configFileName = "server.config." + Environment.GetEnvironmentVariable("TTS_ENV").ToLower() + ".yml";
|
||||
var configContent = File.ReadAllText(configFileName);
|
||||
var configuration = deserializer.Deserialize<ServerConfiguration>(configContent);
|
||||
|
||||
if (configuration.Environment.ToUpper() != "QA" && configuration.Environment.ToUpper() != "PROD")
|
||||
throw new Exception("Invalid environment set.");
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Logging.ClearProviders();
|
||||
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
});
|
||||
|
||||
builder.WebHost.UseUrls($"http://{configuration.WebsocketServer.Host}:{configuration.WebsocketServer.Port}");
|
||||
var logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.File("logs/log.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7)
|
||||
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
|
||||
.CreateLogger();
|
||||
|
||||
builder.Host.UseSerilog(logger);
|
||||
builder.Logging.AddSerilog(logger);
|
||||
var s = builder.Services;
|
||||
|
||||
s.AddSerilog();
|
||||
|
||||
s.AddSingleton<ServerConfiguration>(configuration);
|
||||
s.AddSingleton<Database>();
|
||||
|
||||
// Socket message handlers
|
||||
s.AddSingleton<Serilog.ILogger>(logger);
|
||||
s.AddKeyedSingleton<ISocketHandler, HeartbeatHandler>("hermes-heartbeat");
|
||||
s.AddKeyedSingleton<ISocketHandler, HermesLoginHandler>("hermes-hermeslogin");
|
||||
s.AddKeyedSingleton<ISocketHandler, RequestHandler>("hermes-request");
|
||||
s.AddKeyedSingleton<ISocketHandler, ErrorHandler>("hermes-error");
|
||||
s.AddKeyedSingleton<ISocketHandler, ChatterHandler>("hermes-chatter");
|
||||
s.AddKeyedSingleton<ISocketHandler, EmoteDetailsHandler>("hermes-emotedetails");
|
||||
s.AddKeyedSingleton<ISocketHandler, EmoteUsageHandler>("hermes-emoteusage");
|
||||
|
||||
// Request handlers
|
||||
s.AddKeyedSingleton<IRequest, GetTTSUsers>("BanTTSUser");
|
||||
s.AddKeyedSingleton<IRequest, GetTTSUsers>("GetTTSUsers");
|
||||
s.AddKeyedSingleton<IRequest, GetTTSVoices>("GetTTSVoices");
|
||||
s.AddKeyedSingleton<IRequest, GetTTSWordFilters>("GetTTSWordFilters");
|
||||
s.AddKeyedSingleton<IRequest, CreateTTSUser>("CreateTTSUser");
|
||||
s.AddKeyedSingleton<IRequest, CreateTTSVoice>("CreateTTSVoice");
|
||||
s.AddKeyedSingleton<IRequest, DeleteTTSVoice>("DeleteTTSVoice");
|
||||
s.AddKeyedSingleton<IRequest, UpdateTTSUser>("UpdateTTSUser");
|
||||
s.AddKeyedSingleton<IRequest, UpdateTTSVoice>("UpdateTTSVoice");
|
||||
s.AddKeyedSingleton<IRequest, GetChatterIds>("GetChatterIds");
|
||||
s.AddKeyedSingleton<IRequest, GetEmotes>("GetEmotes");
|
||||
s.AddKeyedSingleton<IRequest, UpdateTTSVoiceState>("UpdateTTSVoiceState");
|
||||
|
||||
s.AddSingleton<HermesSocketManager>();
|
||||
s.AddSingleton<SocketHandlerManager>();
|
||||
s.AddSingleton<RequestManager, ServerRequestManager>();
|
||||
s.AddSingleton<JsonSerializerOptions>(new JsonSerializerOptions()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
s.AddSingleton<Server>();
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseForwardedHeaders();
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseWebSockets(new WebSocketOptions()
|
||||
{
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(30)
|
||||
});
|
||||
|
||||
var options = app.Services.GetRequiredService<JsonSerializerOptions>();
|
||||
var server = app.Services.GetRequiredService<Server>();
|
||||
|
||||
app.Use(async (HttpContext context, RequestDelegate next) =>
|
||||
{
|
||||
if (context.Request.Path != "/")
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||
await server.Handle(new WebSocketUser(webSocket, IPAddress.Parse(context.Request.Headers["X-Forwarded-For"].ToString()), options, logger), context);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
}
|
||||
});
|
||||
|
||||
await app.RunAsync();
|
129
db/Database.cs
Normal file
129
db/Database.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using HermesSocketServer;
|
||||
using Npgsql;
|
||||
|
||||
namespace HermesSocketLibrary.db
|
||||
{
|
||||
public class Database
|
||||
{
|
||||
private NpgsqlDataSource _source;
|
||||
private ServerConfiguration _configuration;
|
||||
|
||||
public NpgsqlDataSource DataSource { get => _source; }
|
||||
|
||||
|
||||
public Database(ServerConfiguration configuration)
|
||||
{
|
||||
NpgsqlDataSourceBuilder builder = new NpgsqlDataSourceBuilder(configuration.Database.ConnectionString);
|
||||
_source = builder.Build();
|
||||
}
|
||||
|
||||
public async Task Execute(string sql, IDictionary<string, object>? values, Action<NpgsqlDataReader> reading)
|
||||
{
|
||||
using (var connection = await _source.OpenConnectionAsync())
|
||||
{
|
||||
using (var command = new NpgsqlCommand(sql, connection))
|
||||
{
|
||||
if (values != null)
|
||||
{
|
||||
foreach (var entry in values)
|
||||
command.Parameters.AddWithValue(entry.Key, entry.Value);
|
||||
}
|
||||
await command.PrepareAsync();
|
||||
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
reading(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Execute(string sql, Action<NpgsqlCommand> action, Action<NpgsqlDataReader> reading)
|
||||
{
|
||||
using (var connection = await _source.OpenConnectionAsync())
|
||||
{
|
||||
using (var command = new NpgsqlCommand(sql, connection))
|
||||
{
|
||||
action(command);
|
||||
await command.PrepareAsync();
|
||||
|
||||
using (var reader = await command.ExecuteReaderAsync())
|
||||
{
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
reading(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> Execute(string sql, IDictionary<string, object>? values)
|
||||
{
|
||||
using (var connection = await _source.OpenConnectionAsync())
|
||||
{
|
||||
using (var command = new NpgsqlCommand(sql, connection))
|
||||
{
|
||||
if (values != null)
|
||||
{
|
||||
foreach (var entry in values)
|
||||
command.Parameters.AddWithValue(entry.Key, entry.Value);
|
||||
}
|
||||
await command.PrepareAsync();
|
||||
|
||||
return await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> Execute(string sql, Action<NpgsqlCommand> action)
|
||||
{
|
||||
using (var connection = await _source.OpenConnectionAsync())
|
||||
{
|
||||
using (var command = new NpgsqlCommand(sql, connection))
|
||||
{
|
||||
action(command);
|
||||
await command.PrepareAsync();
|
||||
|
||||
return await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object?> ExecuteScalar(string sql, IDictionary<string, object>? values = null)
|
||||
{
|
||||
using (var connection = await _source.OpenConnectionAsync())
|
||||
{
|
||||
using (var command = new NpgsqlCommand(sql, connection))
|
||||
{
|
||||
if (values != null)
|
||||
{
|
||||
foreach (var entry in values)
|
||||
command.Parameters.AddWithValue(entry.Key, entry.Value);
|
||||
}
|
||||
|
||||
await command.PrepareAsync();
|
||||
|
||||
return await command.ExecuteScalarAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object?> ExecuteScalar(string sql, Action<NpgsqlCommand> action)
|
||||
{
|
||||
using (var connection = await _source.OpenConnectionAsync())
|
||||
{
|
||||
using (var command = new NpgsqlCommand(sql, connection))
|
||||
{
|
||||
action(command);
|
||||
await command.PrepareAsync();
|
||||
|
||||
return await command.ExecuteScalarAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user