Initial commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "6.0.9",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace MareSynchronosServer.Controllers;
|
||||
|
||||
[Route("/msgc")]
|
||||
[Authorize(Policy = "Internal")]
|
||||
public class ClientMessageController : Controller
|
||||
{
|
||||
private ILogger<ClientMessageController> _logger;
|
||||
private IHubContext<MareHub, IMareHub> _hubContext;
|
||||
|
||||
public ClientMessageController(ILogger<ClientMessageController> logger, IHubContext<MareHub, IMareHub> hubContext)
|
||||
{
|
||||
_logger = logger;
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
[Route("sendMessage")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SendMessage([FromBody] ClientMessage msg)
|
||||
{
|
||||
bool hasUid = !string.IsNullOrEmpty(msg.UID);
|
||||
|
||||
if (!hasUid)
|
||||
{
|
||||
_logger.LogInformation("Sending Message of severity {severity} to all online users: {message}", msg.Severity, msg.Message);
|
||||
await _hubContext.Clients.All.Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Sending Message of severity {severity} to user {uid}: {message}", msg.Severity, msg.UID, msg.Message);
|
||||
await _hubContext.Clients.User(msg.UID).Client_ReceiveServerMessage(msg.Severity, msg.Message).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MareSynchronos.API.Routes;
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using MareSynchronosServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace MareSynchronosServer.Controllers;
|
||||
|
||||
[Route(MareFiles.Main)]
|
||||
public class MainController : Controller
|
||||
{
|
||||
private IHubContext<MareHub, IMareHub> _hubContext;
|
||||
|
||||
public MainController(ILogger<MainController> logger, IHubContext<MareHub, IMareHub> hubContext)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
[HttpGet(MareFiles.Main_SendReady)]
|
||||
[Authorize(Policy = "Internal")]
|
||||
public IActionResult SendReadyToClients(string uid, Guid requestId)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await _hubContext.Clients.User(uid).Client_DownloadReady(requestId);
|
||||
});
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Models;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataFullDto?> CharaDataCreate()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
int uploadCount = DbContext.CharaData.Count(c => c.UploaderUID == UserUID);
|
||||
User user = DbContext.Users.Single(u => u.UID == UserUID);
|
||||
int maximumUploads = _maxCharaDataByUser;
|
||||
if (uploadCount >= maximumUploads)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string charaDataId = null;
|
||||
while (charaDataId == null)
|
||||
{
|
||||
charaDataId = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFHIJKLMNOPQRSTUVWXYZ");
|
||||
bool idExists = await DbContext.CharaData.AnyAsync(c => c.UploaderUID == UserUID && c.Id == charaDataId).ConfigureAwait(false);
|
||||
if (idExists)
|
||||
{
|
||||
charaDataId = null;
|
||||
}
|
||||
}
|
||||
|
||||
DateTime createdDate = DateTime.UtcNow;
|
||||
CharaData charaData = new()
|
||||
{
|
||||
Id = charaDataId,
|
||||
UploaderUID = UserUID,
|
||||
CreatedDate = createdDate,
|
||||
UpdatedDate = createdDate,
|
||||
AccessType = CharaDataAccess.Individuals,
|
||||
ShareType = CharaDataShare.Private,
|
||||
CustomizeData = string.Empty,
|
||||
GlamourerData = string.Empty,
|
||||
ExpiryDate = DateTime.MaxValue,
|
||||
Description = string.Empty,
|
||||
};
|
||||
|
||||
await DbContext.CharaData.AddAsync(charaData).ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", charaDataId));
|
||||
|
||||
return GetCharaDataFullDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> CharaDataDelete(string id)
|
||||
{
|
||||
var existingData = await DbContext.CharaData.SingleOrDefaultAsync(u => u.Id == id && u.UploaderUID == UserUID).ConfigureAwait(false);
|
||||
if (existingData == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
|
||||
|
||||
DbContext.Remove(existingData);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCallWarning(MareHubLogger.Args("FAILURE", id, ex.Message));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataDownloadDto?> CharaDataDownload(string id)
|
||||
{
|
||||
CharaData charaData = await GetCharaDataById(id, nameof(CharaDataDownload)).ConfigureAwait(false);
|
||||
|
||||
if (!string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
|
||||
{
|
||||
charaData.DownloadCount++;
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
|
||||
|
||||
return GetCharaDataDownloadDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataMetaInfoDto?> CharaDataGetMetainfo(string id)
|
||||
{
|
||||
var charaData = await GetCharaDataById(id, nameof(CharaDataGetMetainfo)).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", id));
|
||||
|
||||
return GetCharaDataMetaInfoDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<CharaDataFullDto>> CharaDataGetOwn()
|
||||
{
|
||||
var ownCharaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedUser)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedGroup)
|
||||
.Include(u => u.Poses)
|
||||
.AsSplitQuery()
|
||||
.Where(c => c.UploaderUID == UserUID).ToListAsync().ConfigureAwait(false);
|
||||
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS"));
|
||||
return [.. ownCharaData.Select(GetCharaDataFullDto)];
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataFullDto?> CharaDataAttemptRestore(string id)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(id));
|
||||
var charaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedUser)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedGroup)
|
||||
.Include(u => u.Poses)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(s => s.Id == id && s.UploaderUID == UserUID)
|
||||
.ConfigureAwait(false);
|
||||
if (charaData == null)
|
||||
return null;
|
||||
|
||||
var currentHashes = charaData.Files.Select(f => f.FileCacheHash).ToList();
|
||||
var missingFiles = charaData.OriginalFiles.Where(c => !currentHashes.Contains(c.Hash, StringComparer.Ordinal)).ToList();
|
||||
|
||||
// now let's see what's on the db still
|
||||
var existingDbFiles = await DbContext.Files
|
||||
.Where(f => missingFiles.Select(k => k.Hash).Distinct().Contains(f.Hash))
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// now shove it all back into the db
|
||||
foreach (var dbFile in existingDbFiles)
|
||||
{
|
||||
var missingFileEntry = missingFiles.First(f => string.Equals(f.Hash, dbFile.Hash, StringComparison.Ordinal));
|
||||
charaData.Files.Add(new CharaDataFile()
|
||||
{
|
||||
FileCache = dbFile,
|
||||
GamePath = missingFileEntry.GamePath,
|
||||
Parent = charaData
|
||||
});
|
||||
missingFiles.Remove(missingFileEntry);
|
||||
}
|
||||
|
||||
if (existingDbFiles.Any())
|
||||
{
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return GetCharaDataFullDto(charaData);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<CharaDataMetaInfoDto>> CharaDataGetShared()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
List<CharaData> sharedCharaData = [];
|
||||
var groups = await DbContext.GroupPairs
|
||||
.Where(u => u.GroupUserUID == UserUID)
|
||||
.Select(k => k.GroupGID)
|
||||
.AsNoTracking()
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var individualPairs = await GetDirectPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var allPairs = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
|
||||
var allSharedDataByPair = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.Include(u => u.Poses)
|
||||
.Include(u => u.Uploader)
|
||||
.Where(p => p.UploaderUID != UserUID && p.ShareType == CharaDataShare.Shared)
|
||||
.Where(p =>
|
||||
(individualPairs.Contains(p.UploaderUID) && p.AccessType == CharaDataAccess.ClosePairs)
|
||||
|| (allPairs.Contains(p.UploaderUID) && p.AccessType == CharaDataAccess.AllPairs)
|
||||
|| (p.AllowedIndividiuals.Any(u => u.AllowedUserUID == UserUID || (u.AllowedGroupGID != null && groups.Contains(u.AllowedGroupGID)))))
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking()
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
||||
foreach (var charaData in allSharedDataByPair)
|
||||
{
|
||||
sharedCharaData.Add(charaData);
|
||||
}
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", sharedCharaData.Count));
|
||||
|
||||
return [.. sharedCharaData.Select(GetCharaDataMetaInfoDto)];
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<CharaDataFullDto?> CharaDataUpdate(CharaDataUpdateDto updateDto)
|
||||
{
|
||||
var charaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.OriginalFiles)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedUser)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.ThenInclude(u => u.AllowedGroup)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.Poses)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(u => u.Id == updateDto.Id && u.UploaderUID == UserUID).ConfigureAwait(false);
|
||||
|
||||
if (charaData == null)
|
||||
return null;
|
||||
|
||||
bool anyChanges = false;
|
||||
|
||||
if (updateDto.Description != null)
|
||||
{
|
||||
charaData.Description = updateDto.Description;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.ExpiryDate != null)
|
||||
{
|
||||
charaData.ExpiryDate = updateDto.ExpiryDate;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.GlamourerData != null)
|
||||
{
|
||||
charaData.GlamourerData = updateDto.GlamourerData;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.CustomizeData != null)
|
||||
{
|
||||
charaData.CustomizeData = updateDto.CustomizeData;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.ManipulationData != null)
|
||||
{
|
||||
charaData.ManipulationData = updateDto.ManipulationData;
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.AccessType != null)
|
||||
{
|
||||
charaData.AccessType = GetAccessType(updateDto.AccessType.Value);
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.ShareType != null)
|
||||
{
|
||||
charaData.ShareType = GetShareType(updateDto.ShareType.Value);
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.AllowedUsers != null)
|
||||
{
|
||||
var individuals = charaData.AllowedIndividiuals.Where(k => k.AllowedGroup == null).ToList();
|
||||
var allowedUserList = updateDto.AllowedUsers.ToList();
|
||||
foreach (var user in updateDto.AllowedUsers)
|
||||
{
|
||||
if (charaData.AllowedIndividiuals.Any(k => k.AllowedUser != null && (string.Equals(k.AllowedUser.UID, user, StringComparison.Ordinal) || string.Equals(k.AllowedUser.Alias, user, StringComparison.Ordinal))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == user || u.Alias == user).ConfigureAwait(false);
|
||||
if (dbUser != null)
|
||||
{
|
||||
charaData.AllowedIndividiuals.Add(new CharaDataAllowance()
|
||||
{
|
||||
AllowedUser = dbUser,
|
||||
Parent = charaData
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dataUser in individuals.Where(k => !updateDto.AllowedUsers.Contains(k.AllowedUser.UID, StringComparer.Ordinal) && !updateDto.AllowedUsers.Contains(k.AllowedUser.Alias, StringComparer.Ordinal)))
|
||||
{
|
||||
DbContext.Remove(dataUser);
|
||||
charaData.AllowedIndividiuals.Remove(dataUser);
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.AllowedGroups != null)
|
||||
{
|
||||
var individualGroups = charaData.AllowedIndividiuals.Where(k => k.AllowedUser == null).ToList();
|
||||
var allowedGroups = updateDto.AllowedGroups.ToList();
|
||||
foreach (var group in updateDto.AllowedGroups)
|
||||
{
|
||||
if (charaData.AllowedIndividiuals.Any(k => k.AllowedGroup != null && (string.Equals(k.AllowedGroup.GID, group, StringComparison.Ordinal) || string.Equals(k.AllowedGroup.Alias, group, StringComparison.Ordinal))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupUser = await DbContext.GroupPairs.Include(u => u.Group).SingleOrDefaultAsync(u => (u.Group.GID == group || u.Group.Alias == group) && u.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
if (groupUser != null)
|
||||
{
|
||||
charaData.AllowedIndividiuals.Add(new CharaDataAllowance()
|
||||
{
|
||||
AllowedGroup = groupUser.Group,
|
||||
Parent = charaData
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dataGroup in individualGroups.Where(k => !updateDto.AllowedGroups.Contains(k.AllowedGroup.GID, StringComparer.Ordinal) && !updateDto.AllowedGroups.Contains(k.AllowedGroup.Alias, StringComparer.Ordinal)))
|
||||
{
|
||||
DbContext.Remove(dataGroup);
|
||||
charaData.AllowedIndividiuals.Remove(dataGroup);
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.FileGamePaths != null)
|
||||
{
|
||||
var originalFiles = charaData.OriginalFiles.ToList();
|
||||
charaData.OriginalFiles.Clear();
|
||||
DbContext.RemoveRange(originalFiles);
|
||||
var files = charaData.Files.ToList();
|
||||
charaData.Files.Clear();
|
||||
DbContext.RemoveRange(files);
|
||||
foreach (var file in updateDto.FileGamePaths)
|
||||
{
|
||||
charaData.Files.Add(new CharaDataFile()
|
||||
{
|
||||
FileCacheHash = file.HashOrFileSwap,
|
||||
GamePath = file.GamePath,
|
||||
Parent = charaData
|
||||
});
|
||||
|
||||
charaData.OriginalFiles.Add(new CharaDataOriginalFile()
|
||||
{
|
||||
Hash = file.HashOrFileSwap,
|
||||
Parent = charaData,
|
||||
GamePath = file.GamePath
|
||||
});
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.FileSwaps != null)
|
||||
{
|
||||
var fileSwaps = charaData.FileSwaps.ToList();
|
||||
charaData.FileSwaps.Clear();
|
||||
DbContext.RemoveRange(fileSwaps);
|
||||
foreach (var file in updateDto.FileSwaps)
|
||||
{
|
||||
charaData.FileSwaps.Add(new CharaDataFileSwap()
|
||||
{
|
||||
FilePath = file.HashOrFileSwap,
|
||||
GamePath = file.GamePath,
|
||||
Parent = charaData
|
||||
});
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
if (updateDto.Poses != null)
|
||||
{
|
||||
foreach (var pose in updateDto.Poses)
|
||||
{
|
||||
if (pose.Id == null)
|
||||
{
|
||||
charaData.Poses.Add(new CharaDataPose()
|
||||
{
|
||||
Description = pose.Description,
|
||||
Parent = charaData,
|
||||
ParentUploaderUID = UserUID,
|
||||
PoseData = pose.PoseData,
|
||||
WorldData = pose.WorldData == null ? string.Empty : JsonSerializer.Serialize(pose.WorldData),
|
||||
});
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var associatedPose = charaData.Poses.FirstOrDefault(p => p.Id == pose.Id);
|
||||
if (associatedPose == null)
|
||||
continue;
|
||||
|
||||
if (pose.Description == null && pose.PoseData == null && pose.WorldData == null)
|
||||
{
|
||||
charaData.Poses.Remove(associatedPose);
|
||||
DbContext.Remove(associatedPose);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pose.Description != null)
|
||||
associatedPose.Description = pose.Description;
|
||||
if (pose.WorldData != null)
|
||||
{
|
||||
if (pose.WorldData.Value == default) associatedPose.WorldData = string.Empty;
|
||||
else associatedPose.WorldData = JsonSerializer.Serialize(pose.WorldData.Value);
|
||||
}
|
||||
if (pose.PoseData != null)
|
||||
associatedPose.PoseData = pose.PoseData;
|
||||
}
|
||||
|
||||
anyChanges = true;
|
||||
}
|
||||
|
||||
var overflowingPoses = charaData.Poses.Skip(10).ToList();
|
||||
foreach (var overflowing in overflowingPoses)
|
||||
{
|
||||
charaData.Poses.Remove(overflowing);
|
||||
DbContext.Remove(overflowing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChanges)
|
||||
{
|
||||
charaData.UpdatedDate = DateTime.UtcNow;
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_logger.LogCallInfo(MareHubLogger.Args("SUCCESS", anyChanges));
|
||||
}
|
||||
|
||||
return GetCharaDataFullDto(charaData);
|
||||
}
|
||||
|
||||
private static CharaDataAccess GetAccessType(AccessTypeDto dataAccess) => dataAccess switch
|
||||
{
|
||||
AccessTypeDto.Public => CharaDataAccess.Public,
|
||||
AccessTypeDto.AllPairs => CharaDataAccess.AllPairs,
|
||||
AccessTypeDto.ClosePairs => CharaDataAccess.ClosePairs,
|
||||
AccessTypeDto.Individuals => CharaDataAccess.Individuals,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private static AccessTypeDto GetAccessTypeDto(CharaDataAccess dataAccess) => dataAccess switch
|
||||
{
|
||||
CharaDataAccess.Public => AccessTypeDto.Public,
|
||||
CharaDataAccess.AllPairs => AccessTypeDto.AllPairs,
|
||||
CharaDataAccess.ClosePairs => AccessTypeDto.ClosePairs,
|
||||
CharaDataAccess.Individuals => AccessTypeDto.Individuals,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private static CharaDataDownloadDto GetCharaDataDownloadDto(CharaData charaData)
|
||||
{
|
||||
return new CharaDataDownloadDto(charaData.Id, charaData.Uploader.ToUserData())
|
||||
{
|
||||
CustomizeData = charaData.CustomizeData,
|
||||
Description = charaData.Description,
|
||||
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
|
||||
GlamourerData = charaData.GlamourerData,
|
||||
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
|
||||
ManipulationData = charaData.ManipulationData,
|
||||
};
|
||||
}
|
||||
|
||||
private CharaDataFullDto GetCharaDataFullDto(CharaData charaData)
|
||||
{
|
||||
return new CharaDataFullDto(charaData.Id, new(UserUID))
|
||||
{
|
||||
AccessType = GetAccessTypeDto(charaData.AccessType),
|
||||
ShareType = GetShareTypeDto(charaData.ShareType),
|
||||
AllowedUsers = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedUserUID)).Select(u => new UserData(u.AllowedUser.UID, u.AllowedUser.Alias))],
|
||||
AllowedGroups = [.. charaData.AllowedIndividiuals.Where(k => !string.IsNullOrEmpty(k.AllowedGroupGID)).Select(k => new GroupData(k.AllowedGroup.GID, k.AllowedGroup.Alias))],
|
||||
CustomizeData = charaData.CustomizeData,
|
||||
Description = charaData.Description,
|
||||
ExpiryDate = charaData.ExpiryDate ?? DateTime.MaxValue,
|
||||
OriginalFiles = charaData.OriginalFiles.Select(k => new GamePathEntry(k.Hash, k.GamePath)).ToList(),
|
||||
FileGamePaths = charaData.Files.Select(k => new GamePathEntry(k.FileCacheHash, k.GamePath)).ToList(),
|
||||
FileSwaps = charaData.FileSwaps.Select(k => new GamePathEntry(k.FilePath, k.GamePath)).ToList(),
|
||||
GlamourerData = charaData.GlamourerData,
|
||||
CreatedDate = charaData.CreatedDate,
|
||||
UpdatedDate = charaData.UpdatedDate,
|
||||
ManipulationData = charaData.ManipulationData,
|
||||
DownloadCount = charaData.DownloadCount,
|
||||
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
|
||||
{
|
||||
WorldData data = default;
|
||||
|
||||
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
|
||||
return new PoseEntry(k.Id)
|
||||
{
|
||||
Description = k.Description,
|
||||
PoseData = k.PoseData,
|
||||
WorldData = data
|
||||
};
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
private static CharaDataMetaInfoDto GetCharaDataMetaInfoDto(CharaData charaData)
|
||||
{
|
||||
var allOrigHashes = charaData.OriginalFiles.Select(k => k.Hash).ToList();
|
||||
var allFileHashes = charaData.Files.Select(f => f.FileCacheHash).ToList();
|
||||
var allHashesPresent = allOrigHashes.TrueForAll(h => allFileHashes.Contains(h, StringComparer.Ordinal));
|
||||
var canBeDownloaded = allHashesPresent &= !string.IsNullOrEmpty(charaData.GlamourerData);
|
||||
return new CharaDataMetaInfoDto(charaData.Id, charaData.Uploader.ToUserData())
|
||||
{
|
||||
CanBeDownloaded = canBeDownloaded,
|
||||
Description = charaData.Description,
|
||||
UpdatedDate = charaData.UpdatedDate,
|
||||
PoseData = [.. charaData.Poses.OrderBy(p => p.Id).Select(k =>
|
||||
{
|
||||
WorldData data = default;
|
||||
if(!string.IsNullOrEmpty(k.WorldData)) data = JsonSerializer.Deserialize<WorldData>(k.WorldData);
|
||||
return new PoseEntry(k.Id)
|
||||
{
|
||||
Description = k.Description,
|
||||
PoseData = k.PoseData,
|
||||
WorldData = data
|
||||
};
|
||||
})],
|
||||
};
|
||||
}
|
||||
|
||||
private static CharaDataShare GetShareType(ShareTypeDto dataShare) => dataShare switch
|
||||
{
|
||||
ShareTypeDto.Shared => CharaDataShare.Shared,
|
||||
ShareTypeDto.Private => CharaDataShare.Private,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private static ShareTypeDto GetShareTypeDto(CharaDataShare dataShare) => dataShare switch
|
||||
{
|
||||
CharaDataShare.Shared => ShareTypeDto.Shared,
|
||||
CharaDataShare.Private => ShareTypeDto.Private,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
private async Task<bool> CheckCharaDataAllowance(CharaData charaData, List<string> joinedGroups)
|
||||
{
|
||||
// check for self
|
||||
if (string.Equals(charaData.UploaderUID, UserUID, StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
// check for public access
|
||||
if (charaData.AccessType == CharaDataAccess.Public)
|
||||
return true;
|
||||
|
||||
// check for individuals
|
||||
if (charaData.AllowedIndividiuals.Any(u => string.Equals(u.AllowedUserUID, UserUID, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
var pairInfoUploader = await GetAllPairedUnpausedUsers(charaData.UploaderUID).ConfigureAwait(false);
|
||||
|
||||
// check for all pairs
|
||||
if (charaData.AccessType == CharaDataAccess.AllPairs)
|
||||
{
|
||||
if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for individual pairs
|
||||
if (charaData.AccessType == CharaDataAccess.ClosePairs)
|
||||
{
|
||||
if (pairInfoUploader.Any(pair => string.Equals(pair, UserUID, StringComparison.Ordinal)))
|
||||
{
|
||||
ClientPair callerPair =
|
||||
await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == charaData.UploaderUID).ConfigureAwait(false);
|
||||
ClientPair uploaderPair =
|
||||
await DbContext.ClientPairs.AsNoTracking().SingleOrDefaultAsync(w => w.UserUID == charaData.UploaderUID && w.OtherUserUID == UserUID).ConfigureAwait(false);
|
||||
return (callerPair != null && uploaderPair != null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<CharaData> GetCharaDataById(string id, string methodName)
|
||||
{
|
||||
var splitid = id.Split(":", StringSplitOptions.None);
|
||||
if (splitid.Length != 2)
|
||||
{
|
||||
_logger.LogCallWarning(MareHubLogger.Args("INVALID", id));
|
||||
throw new InvalidOperationException($"Id {id} not in expected format");
|
||||
}
|
||||
|
||||
var charaData = await DbContext.CharaData
|
||||
.Include(u => u.Files)
|
||||
.Include(u => u.FileSwaps)
|
||||
.Include(u => u.AllowedIndividiuals)
|
||||
.Include(u => u.Poses)
|
||||
.Include(u => u.Uploader)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(c => c.Id == splitid[1] && c.UploaderUID == splitid[0]).ConfigureAwait(false);
|
||||
|
||||
if (charaData == null)
|
||||
{
|
||||
_logger.LogCallWarning(MareHubLogger.Args("NOT FOUND", id));
|
||||
throw new InvalidDataException($"No chara data with {id} found");
|
||||
}
|
||||
|
||||
var groups = await DbContext.GroupPairs.Where(u => u.GroupUserUID == UserUID).Select(k => k.GroupGID).ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!await CheckCharaDataAllowance(charaData, groups).ConfigureAwait(false))
|
||||
{
|
||||
_logger.LogCallWarning(MareHubLogger.Args("UNAUTHORIZED", id));
|
||||
throw new UnauthorizedAccessException($"User is not allowed to download {id}");
|
||||
}
|
||||
|
||||
return charaData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronosServer.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
[Authorize(Policy = "Identified")]
|
||||
public Task UserChatSendMsg(UserDto dto, ChatMessage message)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
// TODO
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupChatSendMsg(GroupDto dto, ChatMessage message)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (userExists, groupPair) = await TryValidateUserInGroup(dto.GID, UserUID).ConfigureAwait(false);
|
||||
if (!userExists) return;
|
||||
|
||||
var group = await DbContext.Groups.AsNoTracking().SingleAsync(g => g.GID == dto.GID).ConfigureAwait(false);
|
||||
var sender = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
var groupPairs = await DbContext.GroupPairs.AsNoTracking().Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
if (group == null || sender == null) return;
|
||||
|
||||
// TODO: Add and check chat permissions
|
||||
if (group.Alias?.Equals("Loporrit", StringComparison.Ordinal) ?? false)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Chat is disabled for syncshell '{dto.GroupAliasOrGID}'.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// TOOO: Sign the message
|
||||
var signedMessage = new SignedChatMessage(message, sender.ToUserData())
|
||||
{
|
||||
Timestamp = 0,
|
||||
Signature = "",
|
||||
};
|
||||
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupChatMsg(new(new(group.ToGroupData()), signedMessage)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronos.API.Dto.Chat;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
|
||||
namespace MareSynchronosServer.Hubs
|
||||
{
|
||||
public partial class MareHub
|
||||
{
|
||||
public Task Client_DownloadReady(Guid requestId) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupDelete(GroupDto groupDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupPairLeft(GroupPairDto groupPairDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GroupSendInfo(GroupInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserAddClientPair(UserPairDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserChatMsg(UserChatMsgDto userChatMsgDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserReceiveUploadStatus(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserRemoveClientPair(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserSendOffline(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserSendOnline(OnlineUserIdentDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserUpdateProfile(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
|
||||
public Task Client_GposeLobbyJoin(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyLeave(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using MareSynchronosShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Utils;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
public string UserCharaIdent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.CharaIdent, StringComparison.Ordinal))?.Value ?? throw new Exception("No Chara Ident in Claims");
|
||||
|
||||
public string UserUID => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? throw new Exception("No UID in Claims");
|
||||
|
||||
public string Continent => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Continent, StringComparison.Ordinal))?.Value ?? "UNK";
|
||||
|
||||
private async Task DeleteUser(User user)
|
||||
{
|
||||
var ownPairData = await DbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
var auth = await DbContext.Auth.SingleAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
|
||||
var lodestone = await DbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == user.UID).ConfigureAwait(false);
|
||||
var groupPairs = await DbContext.GroupPairs.Where(g => g.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
var userProfileData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.UID).ConfigureAwait(false);
|
||||
var bannedEntries = await DbContext.GroupBans.Where(u => u.BannedUserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
DbContext.Remove(lodestone);
|
||||
}
|
||||
|
||||
if (userProfileData != null)
|
||||
{
|
||||
DbContext.Remove(userProfileData);
|
||||
}
|
||||
|
||||
DbContext.ClientPairs.RemoveRange(ownPairData);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
var otherPairData = await DbContext.ClientPairs.Include(u => u.User)
|
||||
.Where(u => u.OtherUser.UID == user.UID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
foreach (var pair in otherPairData)
|
||||
{
|
||||
await Clients.User(pair.UserUID).Client_UserRemoveClientPair(new(user.ToUserData())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var pair in groupPairs)
|
||||
{
|
||||
await UserLeaveGroup(new GroupDto(new GroupData(pair.GroupGID)), user.UID).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1);
|
||||
|
||||
DbContext.GroupBans.RemoveRange(bannedEntries);
|
||||
DbContext.ClientPairs.RemoveRange(otherPairData);
|
||||
DbContext.Users.Remove(user);
|
||||
DbContext.Auth.Remove(auth);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<PausedEntry>> GetAllPairedClientsWithPauseState(string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
|
||||
var query = await (from userPair in DbContext.ClientPairs
|
||||
join otherUserPair in DbContext.ClientPairs on userPair.OtherUserUID equals otherUserPair.UserUID
|
||||
where otherUserPair.OtherUserUID == uid && userPair.UserUID == uid
|
||||
select new
|
||||
{
|
||||
UID = Convert.ToString(userPair.OtherUserUID),
|
||||
GID = "DIRECT",
|
||||
PauseStateSelf = userPair.IsPaused,
|
||||
PauseStateOther = otherUserPair.IsPaused,
|
||||
})
|
||||
.Union(
|
||||
(from userGroupPair in DbContext.GroupPairs
|
||||
join otherGroupPair in DbContext.GroupPairs on userGroupPair.GroupGID equals otherGroupPair.GroupGID
|
||||
where
|
||||
userGroupPair.GroupUserUID == uid
|
||||
&& otherGroupPair.GroupUserUID != uid
|
||||
select new
|
||||
{
|
||||
UID = Convert.ToString(otherGroupPair.GroupUserUID),
|
||||
GID = Convert.ToString(otherGroupPair.GroupGID),
|
||||
PauseStateSelf = userGroupPair.IsPaused,
|
||||
PauseStateOther = otherGroupPair.IsPaused,
|
||||
})
|
||||
).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
return query.GroupBy(g => g.UID, g => (g.GID, g.PauseStateSelf, g.PauseStateOther),
|
||||
(key, g) => new PausedEntry
|
||||
{
|
||||
UID = key,
|
||||
PauseStates = g.Select(p => new PauseState() { GID = string.Equals(p.GID, "DIRECT", StringComparison.Ordinal) ? null : p.GID, IsSelfPaused = p.PauseStateSelf, IsOtherPaused = p.PauseStateOther })
|
||||
.ToList(),
|
||||
}, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetAllPairedUnpausedUsers(string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
var ret = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false);
|
||||
return ret.Where(k => !k.IsPaused).Select(k => k.UID).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetDirectPairedUnpausedUsers(string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
|
||||
var query = await (from userPair in DbContext.ClientPairs
|
||||
join otherUserPair in DbContext.ClientPairs on userPair.OtherUserUID equals otherUserPair.UserUID
|
||||
where otherUserPair.OtherUserUID == uid && userPair.UserUID == uid && !userPair.IsPaused && !otherUserPair.IsPaused
|
||||
select Convert.ToString(userPair.OtherUserUID)).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> GetOnlineUsers(List<string> uids)
|
||||
{
|
||||
var result = await _redis.GetAllAsync<string>(uids.Select(u => "UID:" + u).ToHashSet(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
return uids.Where(u => result.TryGetValue("UID:" + u, out var ident) && !string.IsNullOrEmpty(ident)).ToDictionary(u => u, u => result["UID:" + u], StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserIdent(string uid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid)) return string.Empty;
|
||||
return await _redis.GetAsync<string>("UID:" + uid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RemoveUserFromRedis()
|
||||
{
|
||||
await _redis.RemoveAsync("UID:" + UserUID, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendGroupDeletedToAll(List<GroupPair> groupUsers)
|
||||
{
|
||||
foreach (var pair in groupUsers)
|
||||
{
|
||||
var pairIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(pairIdent)) continue;
|
||||
|
||||
var pairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupUsers.Where(g => !string.Equals(g.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, pairs, pairIdent, pair.GroupUserUID).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<string>> SendOfflineToAllPairedUsers()
|
||||
{
|
||||
var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var self = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
await Clients.Users(usersToSendDataTo).Client_UserSendOffline(new(self.ToUserData())).ConfigureAwait(false);
|
||||
|
||||
return usersToSendDataTo;
|
||||
}
|
||||
|
||||
private async Task<List<string>> SendOnlineToAllPairedUsers()
|
||||
{
|
||||
var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var self = await DbContext.Users.AsNoTracking().SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
await Clients.Users(usersToSendDataTo).Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false);
|
||||
|
||||
return usersToSendDataTo;
|
||||
}
|
||||
|
||||
private async Task<(bool IsValid, Group ReferredGroup)> TryValidateGroupModeratorOrOwner(string gid)
|
||||
{
|
||||
var isOwnerResult = await TryValidateOwner(gid).ConfigureAwait(false);
|
||||
if (isOwnerResult.isValid) return (true, isOwnerResult.ReferredGroup);
|
||||
|
||||
if (isOwnerResult.ReferredGroup == null) return (false, null);
|
||||
|
||||
var groupPairSelf = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
if (groupPairSelf == null || !groupPairSelf.IsModerator) return (false, null);
|
||||
|
||||
return (true, isOwnerResult.ReferredGroup);
|
||||
}
|
||||
|
||||
private async Task<(bool isValid, Group ReferredGroup)> TryValidateOwner(string gid)
|
||||
{
|
||||
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null) return (false, null);
|
||||
|
||||
return (string.Equals(group.OwnerUID, UserUID, StringComparison.Ordinal), group);
|
||||
}
|
||||
|
||||
private async Task<(bool IsValid, GroupPair ReferredPair)> TryValidateUserInGroup(string gid, string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
|
||||
var groupPair = await DbContext.GroupPairs.Include(c => c.GroupUser)
|
||||
.SingleOrDefaultAsync(g => g.GroupGID == gid && (g.GroupUserUID == uid || g.GroupUser.Alias == uid)).ConfigureAwait(false);
|
||||
if (groupPair == null) return (false, null);
|
||||
|
||||
return (true, groupPair);
|
||||
}
|
||||
|
||||
private async Task UpdateUserOnRedis()
|
||||
{
|
||||
await _redis.AddAsync("UID:" + UserUID, UserCharaIdent, TimeSpan.FromSeconds(60), StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags.FireAndForget).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task UserGroupLeave(GroupPair groupUserPair, List<PausedEntry> allUserPairs, string userIdent, string? uid = null)
|
||||
{
|
||||
uid ??= UserUID;
|
||||
var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal));
|
||||
if (userPair != null)
|
||||
{
|
||||
if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) return;
|
||||
if (userPair.IsPausedPerGroup is PauseInfo.Unpaused) return;
|
||||
}
|
||||
|
||||
var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
await Clients.User(uid).Client_UserSendOffline(new(new(groupUserPair.GroupUserUID))).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID).Client_UserSendOffline(new(new(uid))).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UserLeaveGroup(GroupDto dto, string userUid)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (exists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, userUid).ConfigureAwait(false);
|
||||
if (!exists) return;
|
||||
|
||||
var group = await DbContext.Groups.SingleOrDefaultAsync(g => g.GID == dto.Group.GID).ConfigureAwait(false);
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToListAsync().ConfigureAwait(false);
|
||||
var groupPairsWithoutSelf = groupPairs.Where(p => !string.Equals(p.GroupUserUID, userUid, StringComparison.Ordinal)).ToList();
|
||||
|
||||
DbContext.GroupPairs.Remove(groupPair);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.User(userUid).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false);
|
||||
|
||||
bool ownerHasLeft = string.Equals(group.OwnerUID, userUid, StringComparison.Ordinal);
|
||||
if (ownerHasLeft)
|
||||
{
|
||||
if (!groupPairsWithoutSelf.Any())
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Deleted"));
|
||||
|
||||
DbContext.Groups.Remove(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(DbContext, group, groupPairsWithoutSelf, _maxExistingGroupsByUser).ConfigureAwait(false);
|
||||
|
||||
if (groupHasMigrated.Item1)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Migrated", groupHasMigrated.Item2));
|
||||
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == groupHasMigrated.Item2).ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(),
|
||||
user.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Deleted"));
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupDelete(dto).ConfigureAwait(false);
|
||||
|
||||
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == userUid).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).Client_GroupPairLeft(new GroupPairDto(dto.Group, groupPair.GroupUser.ToUserData())).ConfigureAwait(false);
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
|
||||
var ident = await GetUserIdent(userUid).ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupPairsWithoutSelf)
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, allUserPairs, ident, userUid).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
private async Task<string?> GetUserGposeLobby()
|
||||
{
|
||||
return await _redis.GetAsync<string>(GposeLobbyUser).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetUsersInLobby(string lobbyId, bool includeSelf = false)
|
||||
{
|
||||
var users = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}").ConfigureAwait(false);
|
||||
return users?.Where(u => includeSelf || !string.Equals(u, UserUID, StringComparison.Ordinal)).ToList() ?? [];
|
||||
}
|
||||
|
||||
private async Task AddUserToLobby(string lobbyId, List<string> priorUsers)
|
||||
{
|
||||
_mareMetrics.IncGauge(MetricsAPI.GaugeGposeLobbyUsers);
|
||||
if (priorUsers.Count == 0)
|
||||
_mareMetrics.IncGauge(MetricsAPI.GaugeGposeLobbies);
|
||||
|
||||
await _redis.AddAsync(GposeLobbyUser, lobbyId).ConfigureAwait(false);
|
||||
await _redis.AddAsync($"GposeLobby:{lobbyId}", priorUsers.Concat([UserUID])).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RemoveUserFromLobby(string lobbyId, List<string> priorUsers)
|
||||
{
|
||||
await _redis.RemoveAsync(GposeLobbyUser).ConfigureAwait(false);
|
||||
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugeGposeLobbyUsers);
|
||||
|
||||
if (priorUsers.Count == 1)
|
||||
{
|
||||
await _redis.RemoveAsync($"GposeLobby:{lobbyId}").ConfigureAwait(false);
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugeGposeLobbies);
|
||||
}
|
||||
else
|
||||
{
|
||||
priorUsers.Remove(UserUID);
|
||||
await _redis.AddAsync($"GposeLobby:{lobbyId}", priorUsers).ConfigureAwait(false);
|
||||
await Clients.Users(priorUsers).Client_GposeLobbyLeave(new(UserUID)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private string GposeLobbyUser => $"GposeLobbyUser:{UserUID}";
|
||||
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<string> GposeLobbyCreate()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var alreadyInLobby = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(alreadyInLobby))
|
||||
{
|
||||
throw new HubException("Already in GPose Lobby, cannot join another");
|
||||
}
|
||||
|
||||
string lobbyId = string.Empty;
|
||||
while (string.IsNullOrEmpty(lobbyId))
|
||||
{
|
||||
lobbyId = StringUtils.GenerateRandomString(30, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
|
||||
var result = await _redis.GetAsync<List<string>>($"GposeLobby:{lobbyId}").ConfigureAwait(false);
|
||||
if (result != null)
|
||||
lobbyId = string.Empty;
|
||||
}
|
||||
|
||||
await AddUserToLobby(lobbyId, []).ConfigureAwait(false);
|
||||
|
||||
return lobbyId;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<UserData>> GposeLobbyJoin(string lobbyId)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var existingLobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(existingLobbyId))
|
||||
await GposeLobbyLeave().ConfigureAwait(false);
|
||||
|
||||
var lobbyUsers = await GetUsersInLobby(lobbyId).ConfigureAwait(false);
|
||||
if (!lobbyUsers.Any())
|
||||
return [];
|
||||
|
||||
await AddUserToLobby(lobbyId, lobbyUsers).ConfigureAwait(false);
|
||||
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
await Clients.Users(lobbyUsers.Where(u => !string.Equals(u, UserUID, StringComparison.Ordinal)))
|
||||
.Client_GposeLobbyJoin(user.ToUserData()).ConfigureAwait(false);
|
||||
|
||||
var users = await DbContext.Users.Where(u => lobbyUsers.Contains(u.UID))
|
||||
.Select(u => u.ToUserData())
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> GposeLobbyLeave()
|
||||
{
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return true;
|
||||
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var lobbyUsers = await GetUsersInLobby(lobbyId, true).ConfigureAwait(false);
|
||||
await RemoveUserFromLobby(lobbyId, lobbyUsers).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDataDownloadDto)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return;
|
||||
|
||||
var lobbyUsers = await GetUsersInLobby(lobbyId).ConfigureAwait(false);
|
||||
await Clients.Users(lobbyUsers).Client_GposeLobbyPushCharacterData(charaDataDownloadDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GposeLobbyPushPoseData(PoseData poseData)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return;
|
||||
|
||||
await _gPoseLobbyDistributionService.PushPoseData(lobbyId, UserUID, poseData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GposeLobbyPushWorldData(WorldData worldData)
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var lobbyId = await GetUserGposeLobby().ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(lobbyId))
|
||||
return;
|
||||
|
||||
await _gPoseLobbyDistributionService.PushWorldData(lobbyId, UserUID, worldData).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
562
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs
Normal file
562
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs
Normal file
@@ -0,0 +1,562 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.Chat;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Models;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupBanUser(GroupPairDto dto, string reason)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, reason));
|
||||
|
||||
var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!userHasRights) return;
|
||||
|
||||
var (userExists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!userExists) return;
|
||||
|
||||
if (groupPair.IsModerator || string.Equals(group.OwnerUID, dto.User.UID, StringComparison.Ordinal)) return;
|
||||
|
||||
var alias = string.IsNullOrEmpty(groupPair.GroupUser.Alias) ? "-" : groupPair.GroupUser.Alias;
|
||||
var ban = new GroupBan()
|
||||
{
|
||||
BannedByUID = UserUID,
|
||||
BannedReason = $"{reason} (Alias at time of ban: {alias})",
|
||||
BannedOn = DateTime.UtcNow,
|
||||
BannedUserUID = dto.User.UID,
|
||||
GroupGID = dto.Group.GID,
|
||||
};
|
||||
|
||||
DbContext.Add(ban);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await GroupRemoveUser(dto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return;
|
||||
|
||||
group.InvitesEnabled = !dto.Permissions.HasFlag(GroupPermissions.DisableInvites);
|
||||
group.DisableSounds = dto.Permissions.HasFlag(GroupPermissions.DisableSounds);
|
||||
group.DisableAnimations = dto.Permissions.HasFlag(GroupPermissions.DisableAnimations);
|
||||
group.DisableVFX = dto.Permissions.HasFlag(GroupPermissions.DisableVFX);
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToList();
|
||||
await Clients.Users(groupPairs).Client_GroupChangePermissions(new GroupPermissionDto(dto.Group, dto.Permissions)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupChangeIndividualPermissionState(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (inGroup, groupPair) = await TryValidateUserInGroup(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!inGroup) return;
|
||||
|
||||
var wasPaused = groupPair.IsPaused;
|
||||
groupPair.DisableSounds = dto.GroupPairPermissions.IsDisableSounds();
|
||||
groupPair.DisableAnimations = dto.GroupPairPermissions.IsDisableAnimations();
|
||||
groupPair.IsPaused = dto.GroupPairPermissions.IsPaused();
|
||||
groupPair.DisableVFX = dto.GroupPairPermissions.IsDisableVFX();
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupPairs = DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToList();
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairChangePermissions(dto).ConfigureAwait(false);
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
|
||||
if (wasPaused == groupPair.IsPaused) return;
|
||||
|
||||
foreach (var groupUserPair in groupPairs.Where(u => !string.Equals(u.GroupUserUID, UserUID, StringComparison.Ordinal)).ToList())
|
||||
{
|
||||
var userPair = allUserPairs.SingleOrDefault(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal));
|
||||
if (userPair != null)
|
||||
{
|
||||
if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue;
|
||||
if (userPair.IsPausedExcludingGroup(dto.Group.GID) is PauseInfo.Unpaused) continue;
|
||||
if (userPair.IsOtherPausedForSpecificGroup(dto.Group.GID) is PauseInfo.Paused) continue;
|
||||
}
|
||||
|
||||
var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
if (!groupPair.IsPaused)
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(groupUserPair.ToUserData(), groupUserIdent)).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID)
|
||||
.Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOffline(new(groupUserPair.ToUserData())).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID)
|
||||
.Client_UserSendOffline(new(self.ToUserData())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupChangeOwnership(GroupPairDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (isOwner, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!isOwner) return;
|
||||
|
||||
var (isInGroup, newOwnerPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!isInGroup) return;
|
||||
|
||||
var ownedShells = await DbContext.Groups.CountAsync(g => g.OwnerUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (ownedShells >= _maxExistingGroupsByUser) return;
|
||||
|
||||
var prevOwner = await DbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
prevOwner.IsPinned = false;
|
||||
group.Owner = newOwnerPair.GroupUser;
|
||||
group.Alias = null;
|
||||
newOwnerPair.IsPinned = true;
|
||||
newOwnerPair.IsModerator = false;
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs).Client_GroupSendInfo(new GroupInfoDto(group.ToGroupData(), newOwnerPair.GroupUser.ToUserData(), group.GetGroupPermissions())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> GroupChangePassword(GroupPasswordDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (isOwner, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!isOwner || dto.Password.Length < 10) return false;
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
group.HashedPassword = StringUtils.Sha256String(dto.Password);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupClear(GroupDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return;
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Where(p => !p.IsPinned && !p.IsModerator).Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
var notPinned = groupPairs.Where(g => !g.IsPinned && !g.IsModerator).ToList();
|
||||
|
||||
DbContext.GroupPairs.RemoveRange(notPinned);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var pair in notPinned)
|
||||
{
|
||||
await Clients.Users(groupPairs.Where(p => p.IsPinned).Select(g => g.GroupUserUID)).Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false);
|
||||
|
||||
var pairIdent = await GetUserIdent(pair.GroupUserUID).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(pairIdent)) continue;
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false);
|
||||
|
||||
var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == pair.GroupUserUID).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||
|
||||
foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, allUserPairs, pairIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<GroupPasswordDto> GroupCreate()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
var existingGroupsByUser = await DbContext.Groups.CountAsync(u => u.OwnerUID == UserUID).ConfigureAwait(false);
|
||||
var existingJoinedGroups = await DbContext.GroupPairs.CountAsync(u => u.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser)
|
||||
{
|
||||
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
|
||||
}
|
||||
|
||||
var gid = StringUtils.GenerateRandomString(9);
|
||||
while (await DbContext.Groups.AnyAsync(g => g.GID == "LSS-" + gid).ConfigureAwait(false))
|
||||
{
|
||||
gid = StringUtils.GenerateRandomString(9);
|
||||
}
|
||||
gid = "LSS-" + gid;
|
||||
|
||||
var passwd = StringUtils.GenerateRandomString(16);
|
||||
var sha = SHA256.Create();
|
||||
var hashedPw = StringUtils.Sha256String(passwd);
|
||||
|
||||
Group newGroup = new()
|
||||
{
|
||||
GID = gid,
|
||||
HashedPassword = hashedPw,
|
||||
InvitesEnabled = true,
|
||||
OwnerUID = UserUID,
|
||||
};
|
||||
|
||||
GroupPair initialPair = new()
|
||||
{
|
||||
GroupGID = newGroup.GID,
|
||||
GroupUserUID = UserUID,
|
||||
IsPaused = false,
|
||||
IsPinned = true,
|
||||
};
|
||||
|
||||
await DbContext.Groups.AddAsync(newGroup).ConfigureAwait(false);
|
||||
await DbContext.GroupPairs.AddAsync(initialPair).ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var self = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
|
||||
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(newGroup.ToGroupData(), self.ToUserData(), GroupPermissions.NoneSet, GroupUserPermissions.NoneSet, GroupUserInfo.None))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(gid));
|
||||
|
||||
return new GroupPasswordDto(newGroup.ToGroupData(), passwd);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<string>> GroupCreateTempInvite(GroupDto dto, int amount)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, amount));
|
||||
List<string> inviteCodes = new();
|
||||
List<GroupTempInvite> tempInvites = new();
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return new();
|
||||
|
||||
var existingInvites = await DbContext.GroupTempInvites.Where(g => g.GroupGID == group.GID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
bool hasValidInvite = false;
|
||||
string invite = string.Empty;
|
||||
string hashedInvite = string.Empty;
|
||||
while (!hasValidInvite)
|
||||
{
|
||||
invite = StringUtils.GenerateRandomString(10, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
|
||||
hashedInvite = StringUtils.Sha256String(invite);
|
||||
if (existingInvites.Any(i => string.Equals(i.Invite, hashedInvite, StringComparison.Ordinal))) continue;
|
||||
hasValidInvite = true;
|
||||
inviteCodes.Add(invite);
|
||||
}
|
||||
|
||||
tempInvites.Add(new GroupTempInvite()
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.AddDays(1),
|
||||
GroupGID = group.GID,
|
||||
Invite = hashedInvite,
|
||||
});
|
||||
}
|
||||
|
||||
DbContext.GroupTempInvites.AddRange(tempInvites);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return inviteCodes;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupDelete(GroupDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Where(p => p.GroupGID == dto.Group.GID).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.RemoveRange(groupPairs);
|
||||
DbContext.Remove(group);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).Client_GroupDelete(new GroupDto(group.ToGroupData())).ConfigureAwait(false);
|
||||
|
||||
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<BannedGroupUserDto>> GroupGetBannedUsers(GroupDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (userHasRights, group) = await TryValidateGroupModeratorOrOwner(dto.GID).ConfigureAwait(false);
|
||||
if (!userHasRights) return new List<BannedGroupUserDto>();
|
||||
|
||||
var banEntries = await DbContext.GroupBans.Include(b => b.BannedUser).Where(g => g.GroupGID == dto.Group.GID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
List<BannedGroupUserDto> bannedGroupUsers = banEntries.Select(b =>
|
||||
new BannedGroupUserDto(group.ToGroupData(), b.BannedUser.ToUserData(), b.BannedReason, b.BannedOn,
|
||||
b.BannedByUID)).ToList();
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, bannedGroupUsers.Count));
|
||||
|
||||
return bannedGroupUsers;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<bool> GroupJoin(GroupPasswordDto dto)
|
||||
{
|
||||
var aliasOrGid = dto.Group.GID.Trim();
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto.Group));
|
||||
|
||||
var group = await DbContext.Groups.Include(g => g.Owner).AsNoTracking().SingleOrDefaultAsync(g => g.GID == aliasOrGid || g.Alias == aliasOrGid).ConfigureAwait(false);
|
||||
var groupGid = group?.GID ?? string.Empty;
|
||||
var existingPair = await DbContext.GroupPairs.AsNoTracking().SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
var hashedPw = StringUtils.Sha256String(dto.Password);
|
||||
var existingUserCount = await DbContext.GroupPairs.AsNoTracking().CountAsync(g => g.GroupGID == groupGid).ConfigureAwait(false);
|
||||
var joinedGroups = await DbContext.GroupPairs.CountAsync(g => g.GroupUserUID == UserUID).ConfigureAwait(false);
|
||||
var isBanned = await DbContext.GroupBans.AnyAsync(g => g.GroupGID == groupGid && g.BannedUserUID == UserUID).ConfigureAwait(false);
|
||||
var oneTimeInvite = await DbContext.GroupTempInvites.SingleOrDefaultAsync(g => g.GroupGID == groupGid && g.Invite == hashedPw).ConfigureAwait(false);
|
||||
|
||||
if (group == null
|
||||
|| (!string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal) && oneTimeInvite == null)
|
||||
|| existingPair != null
|
||||
|| existingUserCount >= _maxGroupUserCount
|
||||
|| !group.InvitesEnabled
|
||||
|| joinedGroups >= _maxJoinedGroupsByUser
|
||||
|| isBanned)
|
||||
return false;
|
||||
|
||||
if (oneTimeInvite != null)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(aliasOrGid, "TempInvite", oneTimeInvite.Invite));
|
||||
DbContext.Remove(oneTimeInvite);
|
||||
}
|
||||
|
||||
GroupPair newPair = new()
|
||||
{
|
||||
GroupGID = group.GID,
|
||||
GroupUserUID = UserUID,
|
||||
DisableAnimations = false,
|
||||
DisableSounds = false,
|
||||
DisableVFX = false
|
||||
};
|
||||
|
||||
await DbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(aliasOrGid, "Success"));
|
||||
|
||||
await Clients.User(UserUID).Client_GroupSendFullInfo(new GroupFullInfoDto(group.ToGroupData(), group.Owner.ToUserData(), group.GetGroupPermissions(), newPair.GetGroupPairPermissions(), newPair.GetGroupPairUserInfo())).ConfigureAwait(false);
|
||||
|
||||
var self = DbContext.Users.Single(u => u.UID == UserUID);
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.Include(p => p.GroupUser).Where(p => p.GroupGID == group.GID && p.GroupUserUID != UserUID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID))
|
||||
.Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(), self.ToUserData(), newPair.GetGroupPairUserInfo(), newPair.GetGroupPairPermissions())).ConfigureAwait(false);
|
||||
foreach (var pair in groupPairs)
|
||||
{
|
||||
await Clients.User(UserUID).Client_GroupPairJoined(new GroupPairFullInfoDto(group.ToGroupData(), pair.ToUserData(), pair.GetGroupPairUserInfo(), pair.GetGroupPairPermissions())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupPairs)
|
||||
{
|
||||
var userPair = allUserPairs.Single(p => string.Equals(p.UID, groupUserPair.GroupUserUID, StringComparison.Ordinal));
|
||||
if (userPair.IsDirectlyPaused != PauseInfo.NoConnection) continue;
|
||||
if (userPair.IsPausedExcludingGroup(group.GID) is PauseInfo.Unpaused) continue;
|
||||
if (userPair.IsPausedPerGroup is PauseInfo.Paused) continue;
|
||||
|
||||
var groupUserIdent = await GetUserIdent(groupUserPair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(groupUserPair.ToUserData(), groupUserIdent)).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID)
|
||||
.Client_UserSendOnline(new(self.ToUserData(), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupLeave(GroupDto dto)
|
||||
{
|
||||
await UserLeaveGroup(dto, UserUID).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<int> GroupPrune(GroupDto dto, int days, bool execute)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, days, execute));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return -1;
|
||||
|
||||
var allGroupUsers = await DbContext.GroupPairs.Include(p => p.GroupUser).Include(p => p.Group)
|
||||
.Where(g => g.GroupGID == dto.Group.GID)
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
var usersToPrune = allGroupUsers.Where(p => !p.IsPinned && !p.IsModerator
|
||||
&& p.GroupUserUID != UserUID
|
||||
&& p.Group.OwnerUID != p.GroupUserUID
|
||||
&& p.GroupUser.LastLoggedIn.AddDays(days) < DateTime.UtcNow);
|
||||
|
||||
if (!execute) return usersToPrune.Count();
|
||||
|
||||
DbContext.GroupPairs.RemoveRange(usersToPrune);
|
||||
|
||||
foreach (var pair in usersToPrune)
|
||||
{
|
||||
await Clients.Users(allGroupUsers.Where(p => !usersToPrune.Contains(p)).Select(g => g.GroupUserUID))
|
||||
.Client_GroupPairLeft(new GroupPairDto(dto.Group, pair.GroupUser.ToUserData())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return usersToPrune.Count();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupRemoveUser(GroupPairDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (hasRights, group) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!hasRights) return;
|
||||
|
||||
var (userExists, groupPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!userExists) return;
|
||||
|
||||
if (groupPair.IsModerator || string.Equals(group.OwnerUID, dto.User.UID, StringComparison.Ordinal)) return;
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
DbContext.GroupPairs.Remove(groupPair);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupPairs = DbContext.GroupPairs.Where(p => p.GroupGID == group.GID).AsNoTracking().ToList();
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).Client_GroupPairLeft(dto).ConfigureAwait(false);
|
||||
|
||||
var sharedData = await DbContext.CharaDataAllowances.Where(u => u.AllowedGroup != null && u.AllowedGroupGID == dto.GID && u.ParentUploaderUID == dto.UID).ToListAsync().ConfigureAwait(false);
|
||||
DbContext.CharaDataAllowances.RemoveRange(sharedData);
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var userIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false);
|
||||
if (userIdent == null) return;
|
||||
|
||||
await Clients.User(dto.User.UID).Client_GroupDelete(new GroupDto(dto.Group)).ConfigureAwait(false);
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState(dto.User.UID).ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupPairs)
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, allUserPairs, userIdent, dto.User.UID).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupSetUserInfo(GroupPairUserInfoDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (userExists, userPair) = await TryValidateUserInGroup(dto.Group.GID, dto.User.UID).ConfigureAwait(false);
|
||||
if (!userExists) return;
|
||||
|
||||
var (userIsOwner, _) = await TryValidateOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
var (userIsModerator, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
|
||||
if (dto.GroupUserInfo.HasFlag(GroupUserInfo.IsPinned) && userIsModerator && !userPair.IsPinned)
|
||||
{
|
||||
userPair.IsPinned = true;
|
||||
}
|
||||
else if (userIsModerator && userPair.IsPinned)
|
||||
{
|
||||
userPair.IsPinned = false;
|
||||
}
|
||||
|
||||
if (dto.GroupUserInfo.HasFlag(GroupUserInfo.IsModerator) && userIsOwner && !userPair.IsModerator)
|
||||
{
|
||||
userPair.IsModerator = true;
|
||||
}
|
||||
else if (userIsOwner && userPair.IsModerator)
|
||||
{
|
||||
userPair.IsModerator = false;
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupPairs = await DbContext.GroupPairs.AsNoTracking().Where(p => p.GroupGID == dto.Group.GID).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
|
||||
await Clients.Users(groupPairs).Client_GroupPairChangeUserInfo(new GroupPairUserInfoDto(dto.Group, dto.User, userPair.GetGroupPairUserInfo())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var groups = await DbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
return groups.Select(g => new GroupFullInfoDto(g.Group.ToGroupData(), g.Group.Owner.ToUserData(),
|
||||
g.Group.GetGroupPermissions(), g.GetGroupPairPermissions(), g.GetGroupPairUserInfo())).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<GroupPairFullInfoDto>> GroupsGetUsersInGroup(GroupDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (inGroup, _) = await TryValidateUserInGroup(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!inGroup) return new List<GroupPairFullInfoDto>();
|
||||
|
||||
var group = await DbContext.Groups.SingleAsync(g => g.GID == dto.Group.GID).ConfigureAwait(false);
|
||||
var allPairs = await DbContext.GroupPairs.Include(g => g.GroupUser).Where(g => g.GroupGID == dto.Group.GID && g.GroupUserUID != UserUID).AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
return allPairs.Select(p => new GroupPairFullInfoDto(group.ToGroupData(), p.GroupUser.ToUserData(), p.GetGroupPairUserInfo(), p.GetGroupPairPermissions())).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task GroupUnbanUser(GroupPairDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
var (userHasRights, _) = await TryValidateGroupModeratorOrOwner(dto.Group.GID).ConfigureAwait(false);
|
||||
if (!userHasRights) return;
|
||||
|
||||
var banEntry = await DbContext.GroupBans.SingleOrDefaultAsync(g => g.GroupGID == dto.Group.GID && g.BannedUserUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (banEntry == null) return;
|
||||
|
||||
DbContext.Remove(banEntry);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
}
|
||||
}
|
||||
514
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs
Normal file
514
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.User.cs
Normal file
@@ -0,0 +1,514 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronos.API.Dto.User;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
private static readonly string[] AllowedExtensionsForGamePaths = { ".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk" };
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserAddPair(UserDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
// don't allow adding nothing
|
||||
var uid = dto.User.UID.Trim();
|
||||
if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal) || string.IsNullOrWhiteSpace(dto.User.UID)) return;
|
||||
|
||||
// grab other user, check if it exists and if a pair already exists
|
||||
var otherUser = await DbContext.Users.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false);
|
||||
if (otherUser == null)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, UID does not exist").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(otherUser.UID, UserUID, StringComparison.Ordinal))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"My god you can't pair with yourself why would you do that please stop").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var existingEntry =
|
||||
await DbContext.ClientPairs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.User.UID == UserUID && p.OtherUserUID == otherUser.UID).ConfigureAwait(false);
|
||||
|
||||
if (existingEntry != null)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Warning, $"Cannot pair with {dto.User.UID}, already paired").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// grab self create new client pair and save
|
||||
var user = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
ClientPair wl = new ClientPair()
|
||||
{
|
||||
IsPaused = false,
|
||||
OtherUser = otherUser,
|
||||
User = user,
|
||||
};
|
||||
await DbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
// get the opposite entry of the client pair
|
||||
var otherEntry = OppositeEntry(otherUser.UID);
|
||||
var otherIdent = await GetUserIdent(otherUser.UID).ConfigureAwait(false);
|
||||
|
||||
var ownPerm = UserPermissions.Paired;
|
||||
var otherPerm = UserPermissions.NoneSet;
|
||||
otherPerm.SetPaired(otherEntry != null);
|
||||
otherPerm.SetPaused(otherEntry?.IsPaused ?? false);
|
||||
var userPairResponse = new UserPairDto(otherUser.ToUserData(), ownPerm, otherPerm);
|
||||
await Clients.User(user.UID).Client_UserAddClientPair(userPairResponse).ConfigureAwait(false);
|
||||
|
||||
// check if other user is online
|
||||
if (otherIdent == null || otherEntry == null) return;
|
||||
|
||||
// send push with update to other user if other user is online
|
||||
await Clients.User(otherUser.UID).Client_UserAddClientPair(new UserPairDto(user.ToUserData(), otherPerm, ownPerm)).ConfigureAwait(false);
|
||||
|
||||
if (!otherPerm.IsPaused())
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(otherUser.ToUserData(), otherIdent)).ConfigureAwait(false);
|
||||
await Clients.User(otherUser.UID).Client_UserSendOnline(new(user.ToUserData(), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserDelete()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var userEntry = await DbContext.Users.SingleAsync(u => u.UID == UserUID).ConfigureAwait(false);
|
||||
var secondaryUsers = await DbContext.Auth.Include(u => u.User).Where(u => u.PrimaryUserUID == UserUID).Select(c => c.User).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var user in secondaryUsers)
|
||||
{
|
||||
await DeleteUser(user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await DeleteUser(userEntry).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<OnlineUserIdentDto>> UserGetOnlinePairs()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||
|
||||
await SendOnlineToAllPairedUsers().ConfigureAwait(false);
|
||||
|
||||
return pairs.Select(p => new OnlineUserIdentDto(new UserData(p.Key), p.Value)).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<List<UserPairDto>> UserGetPairedClients()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
var query =
|
||||
from userToOther in DbContext.ClientPairs
|
||||
join otherToUser in DbContext.ClientPairs
|
||||
on new
|
||||
{
|
||||
user = userToOther.UserUID,
|
||||
other = userToOther.OtherUserUID,
|
||||
} equals new
|
||||
{
|
||||
user = otherToUser.OtherUserUID,
|
||||
other = otherToUser.UserUID,
|
||||
} into leftJoin
|
||||
from otherEntry in leftJoin.DefaultIfEmpty()
|
||||
where
|
||||
userToOther.UserUID == UserUID
|
||||
select new
|
||||
{
|
||||
userToOther.OtherUser.Alias,
|
||||
userToOther.IsPaused,
|
||||
OtherIsPaused = otherEntry != null && otherEntry.IsPaused,
|
||||
userToOther.OtherUserUID,
|
||||
IsSynced = otherEntry != null,
|
||||
DisableOwnAnimations = userToOther.DisableAnimations,
|
||||
DisableOwnSounds = userToOther.DisableSounds,
|
||||
DisableOwnVFX = userToOther.DisableVFX,
|
||||
DisableOtherAnimations = otherEntry == null ? false : otherEntry.DisableAnimations,
|
||||
DisableOtherSounds = otherEntry == null ? false : otherEntry.DisableSounds,
|
||||
DisableOtherVFX = otherEntry == null ? false : otherEntry.DisableVFX
|
||||
};
|
||||
|
||||
var results = await query.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
|
||||
return results.Select(c =>
|
||||
{
|
||||
var ownPerm = UserPermissions.Paired;
|
||||
ownPerm.SetPaused(c.IsPaused);
|
||||
ownPerm.SetDisableAnimations(c.DisableOwnAnimations);
|
||||
ownPerm.SetDisableSounds(c.DisableOwnSounds);
|
||||
ownPerm.SetDisableVFX(c.DisableOwnVFX);
|
||||
var otherPerm = UserPermissions.NoneSet;
|
||||
otherPerm.SetPaired(c.IsSynced);
|
||||
otherPerm.SetPaused(c.OtherIsPaused);
|
||||
otherPerm.SetDisableAnimations(c.DisableOtherAnimations);
|
||||
otherPerm.SetDisableSounds(c.DisableOtherSounds);
|
||||
otherPerm.SetDisableVFX(c.DisableOtherVFX);
|
||||
return new UserPairDto(new(c.OtherUserUID, c.Alias), ownPerm, otherPerm);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<UserProfileDto> UserGetProfile(UserDto user)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(user));
|
||||
|
||||
var allUserPairs = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
|
||||
if (!allUserPairs.Contains(user.User.UID, StringComparer.Ordinal) && !string.Equals(user.User.UID, UserUID, StringComparison.Ordinal))
|
||||
{
|
||||
return new UserProfileDto(user.User, false, null, null, "Due to the pause status you cannot access this users profile.");
|
||||
}
|
||||
|
||||
var data = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == user.User.UID).ConfigureAwait(false);
|
||||
if (data == null) return new UserProfileDto(user.User, false, null, null, null);
|
||||
|
||||
if (data.FlaggedForReport) return new UserProfileDto(user.User, true, null, null, "This profile is flagged for report and pending evaluation");
|
||||
if (data.ProfileDisabled) return new UserProfileDto(user.User, true, null, null, "This profile was permanently disabled");
|
||||
|
||||
return new UserProfileDto(user.User, false, data.IsNSFW, data.Base64ProfileImage, data.UserDescription);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserPushData(UserCharaDataMessageDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto.CharaData.FileReplacements.Count));
|
||||
|
||||
// check for honorific containing . and /
|
||||
try
|
||||
{
|
||||
var honorificJson = Encoding.Default.GetString(Convert.FromBase64String(dto.CharaData.HonorificData));
|
||||
var deserialized = JsonSerializer.Deserialize<JsonElement>(honorificJson);
|
||||
if (deserialized.TryGetProperty("Title", out var honorificTitle))
|
||||
{
|
||||
var title = honorificTitle.GetString().Normalize(NormalizationForm.FormKD);
|
||||
if (UrlRegex().IsMatch(title))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your data was not pushed: The usage of URLs the Honorific titles is prohibited. Remove them to be able to continue to push data.").ConfigureAwait(false);
|
||||
throw new HubException("Invalid data provided, Honorific title invalid: " + title);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HubException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// swallow
|
||||
}
|
||||
|
||||
bool hadInvalidData = false;
|
||||
List<string> invalidGamePaths = new();
|
||||
List<string> invalidFileSwapPaths = new();
|
||||
foreach (var replacement in dto.CharaData.FileReplacements.SelectMany(p => p.Value))
|
||||
{
|
||||
var invalidPaths = replacement.GamePaths.Where(p => !GamePathRegex().IsMatch(p)).ToList();
|
||||
invalidPaths.AddRange(replacement.GamePaths.Where(p => !AllowedExtensionsForGamePaths.Any(e => p.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||
replacement.GamePaths = replacement.GamePaths.Where(p => !invalidPaths.Contains(p, StringComparer.OrdinalIgnoreCase)).ToArray();
|
||||
bool validGamePaths = replacement.GamePaths.Any();
|
||||
bool validHash = string.IsNullOrEmpty(replacement.Hash) || HashRegex().IsMatch(replacement.Hash);
|
||||
bool validFileSwapPath = string.IsNullOrEmpty(replacement.FileSwapPath) || GamePathRegex().IsMatch(replacement.FileSwapPath);
|
||||
if (!validGamePaths || !validHash || !validFileSwapPath)
|
||||
{
|
||||
_logger.LogCallWarning(MareHubLogger.Args("Invalid Data", "GamePaths", validGamePaths, string.Join(",", invalidPaths), "Hash", validHash, replacement.Hash, "FileSwap", validFileSwapPath, replacement.FileSwapPath));
|
||||
hadInvalidData = true;
|
||||
if (!validFileSwapPath) invalidFileSwapPaths.Add(replacement.FileSwapPath);
|
||||
if (!validGamePaths) invalidGamePaths.AddRange(replacement.GamePaths);
|
||||
if (!validHash) invalidFileSwapPaths.Add(replacement.Hash);
|
||||
}
|
||||
}
|
||||
|
||||
if (hadInvalidData)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "One or more of your supplied mods were rejected from the server. Consult /xllog for more information.").ConfigureAwait(false);
|
||||
throw new HubException("Invalid data provided, contact the appropriate mod creator to resolve those issues"
|
||||
+ Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, invalidGamePaths.Select(p => "Invalid Game Path: " + p))
|
||||
+ Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, invalidFileSwapPaths.Select(p => "Invalid FileSwap Path: " + p)));
|
||||
}
|
||||
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var idents = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||
|
||||
var recipients = allPairedUsers.Where(f => dto.Recipients.Select(r => r.UID).Contains(f, StringComparer.Ordinal)).ToList();
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(idents.Count, recipients.Count()));
|
||||
|
||||
await Clients.Users(recipients).Client_UserReceiveCharacterData(new OnlineUserCharaDataDto(new UserData(UserUID), dto.CharaData)).ConfigureAwait(false);
|
||||
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushData);
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, recipients.Count());
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserRemovePair(UserDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) return;
|
||||
|
||||
// check if client pair even exists
|
||||
ClientPair callerPair =
|
||||
await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (callerPair == null) return;
|
||||
|
||||
bool callerHadPaused = callerPair.IsPaused;
|
||||
|
||||
// delete from database, send update info to users pair list
|
||||
DbContext.ClientPairs.Remove(callerPair);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
await Clients.User(UserUID).Client_UserRemoveClientPair(dto).ConfigureAwait(false);
|
||||
|
||||
// check if opposite entry exists
|
||||
var oppositeClientPair = OppositeEntry(dto.User.UID);
|
||||
if (oppositeClientPair == null) return;
|
||||
|
||||
// check if other user is online, if no then there is no need to do anything further
|
||||
var otherIdent = await GetUserIdent(dto.User.UID).ConfigureAwait(false);
|
||||
if (otherIdent == null) return;
|
||||
|
||||
// get own ident and
|
||||
await Clients.User(dto.User.UID)
|
||||
.Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(new UserData(UserUID),
|
||||
UserPermissions.NoneSet)).ConfigureAwait(false);
|
||||
|
||||
// if the other user had paused the user the state will be offline for either, do nothing
|
||||
bool otherHadPaused = oppositeClientPair.IsPaused;
|
||||
if (!callerHadPaused && otherHadPaused) return;
|
||||
|
||||
var allUsers = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
var pauseEntry = allUsers.SingleOrDefault(f => string.Equals(f.UID, dto.User.UID, StringComparison.Ordinal));
|
||||
var isPausedInGroup = pauseEntry == null || pauseEntry.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection;
|
||||
|
||||
// if neither user had paused each other and both are in unpaused groups, state will be online for both, do nothing
|
||||
if (!callerHadPaused && !otherHadPaused && !isPausedInGroup) return;
|
||||
|
||||
// if neither user had paused each other and either is not in an unpaused group with each other, change state to offline
|
||||
if (!callerHadPaused && !otherHadPaused && isPausedInGroup)
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false);
|
||||
await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// if the caller had paused other but not the other has paused the caller and they are in an unpaused group together, change state to online
|
||||
if (callerHadPaused && !otherHadPaused && !isPausedInGroup)
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(dto.User, otherIdent)).ConfigureAwait(false);
|
||||
await Clients.User(dto.User.UID).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserReportProfile(UserProfileReportDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
UserProfileDataReport report = await DbContext.UserProfileReports.SingleOrDefaultAsync(u => u.ReportedUserUID == dto.User.UID && u.ReportingUserUID == UserUID).ConfigureAwait(false);
|
||||
if (report != null)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "You already reported this profile and it's pending validation").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
UserProfileData profile = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (profile == null)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "This user has no profile").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
UserProfileDataReport reportToAdd = new()
|
||||
{
|
||||
ReportDate = DateTime.UtcNow,
|
||||
ReportingUserUID = UserUID,
|
||||
ReportReason = dto.ProfileReport,
|
||||
ReportedUserUID = dto.User.UID,
|
||||
};
|
||||
|
||||
profile.FlaggedForReport = true;
|
||||
|
||||
await DbContext.UserProfileReports.AddAsync(reportToAdd).ConfigureAwait(false);
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers(dto.User.UID).ConfigureAwait(false);
|
||||
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(pairs.Select(p => p.Key)).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false);
|
||||
await Clients.Users(dto.User.UID).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserSetPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
if (string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) return;
|
||||
ClientPair pair = await DbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == UserUID && w.OtherUserUID == dto.User.UID).ConfigureAwait(false);
|
||||
if (pair == null) return;
|
||||
|
||||
var pauseChange = pair.IsPaused != dto.Permissions.IsPaused();
|
||||
|
||||
pair.IsPaused = dto.Permissions.IsPaused();
|
||||
pair.DisableAnimations = dto.Permissions.IsDisableAnimations();
|
||||
pair.DisableSounds = dto.Permissions.IsDisableSounds();
|
||||
pair.DisableVFX = dto.Permissions.IsDisableVFX();
|
||||
DbContext.Update(pair);
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto, "Success"));
|
||||
|
||||
var otherEntry = OppositeEntry(dto.User.UID);
|
||||
|
||||
await Clients.User(UserUID).Client_UserUpdateSelfPairPermissions(dto).ConfigureAwait(false);
|
||||
|
||||
if (otherEntry != null)
|
||||
{
|
||||
await Clients.User(dto.User.UID).Client_UserUpdateOtherPairPermissions(new UserPermissionsDto(new UserData(UserUID), dto.Permissions)).ConfigureAwait(false);
|
||||
|
||||
if (pauseChange)
|
||||
{
|
||||
var otherCharaIdent = await GetUserIdent(pair.OtherUserUID).ConfigureAwait(false);
|
||||
|
||||
if (UserCharaIdent == null || otherCharaIdent == null || otherEntry.IsPaused) return;
|
||||
|
||||
if (dto.Permissions.IsPaused())
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOffline(dto).ConfigureAwait(false);
|
||||
await Clients.User(dto.User.UID).Client_UserSendOffline(new(new(UserUID))).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Clients.User(UserUID).Client_UserSendOnline(new(dto.User, otherCharaIdent)).ConfigureAwait(false);
|
||||
await Clients.User(dto.User.UID).Client_UserSendOnline(new(new(UserUID), UserCharaIdent)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task UserSetProfile(UserProfileDto dto)
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(dto));
|
||||
|
||||
if (!string.Equals(dto.User.UID, UserUID, StringComparison.Ordinal)) throw new HubException("Cannot modify profile data for anyone but yourself");
|
||||
|
||||
var existingData = await DbContext.UserProfileData.SingleOrDefaultAsync(u => u.UserUID == dto.User.UID).ConfigureAwait(false);
|
||||
|
||||
if (existingData?.FlaggedForReport ?? false)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile is currently flagged for report and cannot be edited").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingData?.ProfileDisabled ?? false)
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your profile was permanently disabled and cannot be edited").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.ProfilePictureBase64))
|
||||
{
|
||||
byte[] imageData = Convert.FromBase64String(dto.ProfilePictureBase64);
|
||||
using MemoryStream ms = new(imageData);
|
||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is not in PNG format").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
using var image = Image.Load<Rgba32>(imageData);
|
||||
|
||||
if (image.Width > 256 || image.Height > 256 || (imageData.Length > 250 * 1024))
|
||||
{
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Error, "Your provided image file is larger than 256x256 or more than 250KiB.").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingData != null)
|
||||
{
|
||||
if (string.Equals("", dto.ProfilePictureBase64, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
existingData.Base64ProfileImage = null;
|
||||
}
|
||||
else if (dto.ProfilePictureBase64 != null)
|
||||
{
|
||||
existingData.Base64ProfileImage = dto.ProfilePictureBase64;
|
||||
}
|
||||
|
||||
if (dto.IsNSFW != null)
|
||||
{
|
||||
existingData.IsNSFW = dto.IsNSFW.Value;
|
||||
}
|
||||
|
||||
if (dto.Description != null)
|
||||
{
|
||||
existingData.UserDescription = dto.Description;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UserProfileData userProfileData = new()
|
||||
{
|
||||
UserUID = dto.User.UID,
|
||||
Base64ProfileImage = dto.ProfilePictureBase64 ?? null,
|
||||
UserDescription = dto.Description ?? null,
|
||||
IsNSFW = dto.IsNSFW ?? false
|
||||
};
|
||||
|
||||
await DbContext.UserProfileData.AddAsync(userProfileData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
var pairs = await GetOnlineUsers(allPairedUsers).ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(pairs.Select(p => p.Key)).Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false);
|
||||
await Clients.Caller.Client_UserUpdateProfile(new(dto.User)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^([a-z0-9_ '+&,\.\-\{\}]+\/)+([a-z0-9_ '+&,\.\-\{\}]+\.[a-z]{3,4})$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)]
|
||||
private static partial Regex GamePathRegex();
|
||||
|
||||
[GeneratedRegex(@"^[A-Z0-9]{40}$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ECMAScript)]
|
||||
private static partial Regex HashRegex();
|
||||
|
||||
[GeneratedRegex("^[-a-zA-Z0-9@:%._\\+~#=]{1,256}[\\.,][a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$")]
|
||||
private static partial Regex UrlRegex();
|
||||
|
||||
private ClientPair OppositeEntry(string otherUID) =>
|
||||
DbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == UserUID);
|
||||
}
|
||||
147
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs
Normal file
147
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MareSynchronosServer.Services;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis.Extensions.Core.Abstractions;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
{
|
||||
private readonly MareMetrics _mareMetrics;
|
||||
private readonly SystemInfoService _systemInfoService;
|
||||
private readonly IHttpContextAccessor _contextAccessor;
|
||||
private readonly MareHubLogger _logger;
|
||||
private readonly string _shardName;
|
||||
private readonly int _maxExistingGroupsByUser;
|
||||
private readonly int _maxJoinedGroupsByUser;
|
||||
private readonly int _maxGroupUserCount;
|
||||
private readonly IRedisDatabase _redis;
|
||||
private readonly GPoseLobbyDistributionService _gPoseLobbyDistributionService;
|
||||
private readonly Uri _fileServerAddress;
|
||||
private readonly Version _expectedClientVersion;
|
||||
private readonly int _maxCharaDataByUser;
|
||||
|
||||
private readonly Lazy<MareDbContext> _dbContextLazy;
|
||||
private MareDbContext DbContext => _dbContextLazy.Value;
|
||||
|
||||
public MareHub(MareMetrics mareMetrics,
|
||||
IDbContextFactory<MareDbContext> mareDbContextFactory, ILogger<MareHub> logger, SystemInfoService systemInfoService,
|
||||
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
|
||||
IRedisDatabase redisDb, GPoseLobbyDistributionService gPoseLobbyDistributionService)
|
||||
{
|
||||
_mareMetrics = mareMetrics;
|
||||
_systemInfoService = systemInfoService;
|
||||
_shardName = configuration.GetValue<string>(nameof(ServerConfiguration.ShardName));
|
||||
_maxExistingGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3);
|
||||
_maxJoinedGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxJoinedGroupsByUser), 6);
|
||||
_maxGroupUserCount = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 100);
|
||||
_fileServerAddress = configuration.GetValue<Uri>(nameof(ServerConfiguration.CdnFullUrl));
|
||||
_expectedClientVersion = configuration.GetValueOrDefault(nameof(ServerConfiguration.ExpectedClientVersion), new Version(0, 0, 0));
|
||||
_maxCharaDataByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUser), 10);
|
||||
_contextAccessor = contextAccessor;
|
||||
_redis = redisDb;
|
||||
_gPoseLobbyDistributionService = gPoseLobbyDistributionService;
|
||||
_logger = new MareHubLogger(this, logger);
|
||||
_dbContextLazy = new Lazy<MareDbContext>(() => mareDbContextFactory.CreateDbContext());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
DbContext.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Identified")]
|
||||
public async Task<ConnectionDto> GetConnectionDto()
|
||||
{
|
||||
_logger.LogCallInfo();
|
||||
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterInitializedConnections);
|
||||
|
||||
await Clients.Caller.Client_UpdateSystemInfo(_systemInfoService.SystemInfoDto).ConfigureAwait(false);
|
||||
|
||||
var dbUser = DbContext.Users.SingleOrDefault(f => f.UID == UserUID);
|
||||
dbUser.LastLoggedIn = DateTime.UtcNow;
|
||||
await DbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Loporrit, Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false);
|
||||
|
||||
return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias))
|
||||
{
|
||||
CurrentClientVersion = _expectedClientVersion,
|
||||
ServerVersion = IMareHub.ApiVersion,
|
||||
IsAdmin = dbUser.IsAdmin,
|
||||
IsModerator = dbUser.IsModerator,
|
||||
ServerInfo = new ServerInfo()
|
||||
{
|
||||
MaxGroupsCreatedByUser = _maxExistingGroupsByUser,
|
||||
ShardName = _shardName,
|
||||
MaxGroupsJoinedByUser = _maxJoinedGroupsByUser,
|
||||
MaxGroupUserCount = _maxGroupUserCount,
|
||||
FileServerAddress = _fileServerAddress,
|
||||
MaxCharaData = _maxCharaDataByUser
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public async Task<bool> CheckClientHealth()
|
||||
{
|
||||
await UpdateUserOnRedis().ConfigureAwait(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
_mareMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent));
|
||||
|
||||
await UpdateUserOnRedis().ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
|
||||
await base.OnConnectedAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
_mareMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent));
|
||||
if (exception != null)
|
||||
_logger.LogCallWarning(MareHubLogger.Args(_contextAccessor.GetIpAddress(), exception.Message, exception.StackTrace));
|
||||
|
||||
await GposeLobbyLeave().ConfigureAwait(false);
|
||||
await RemoveUserFromRedis().ConfigureAwait(false);
|
||||
|
||||
await SendOfflineToAllPairedUsers().ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
|
||||
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using AspNetCoreRateLimit;
|
||||
using MareSynchronosShared;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
public class SignalRLimitFilter : IHubFilter
|
||||
{
|
||||
private readonly IRateLimitProcessor _processor;
|
||||
private readonly IHttpContextAccessor accessor;
|
||||
private readonly ILogger<SignalRLimitFilter> logger;
|
||||
private static readonly SemaphoreSlim ConnectionLimiterSemaphore = new(20, 20);
|
||||
private static readonly SemaphoreSlim DisconnectLimiterSemaphore = new(20, 20);
|
||||
|
||||
public SignalRLimitFilter(
|
||||
IOptions<IpRateLimitOptions> options, IProcessingStrategy processing, IIpPolicyStore policyStore, IHttpContextAccessor accessor, ILogger<SignalRLimitFilter> logger)
|
||||
{
|
||||
_processor = new IpRateLimitProcessor(options?.Value, policyStore, processing);
|
||||
this.accessor = accessor;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask<object> InvokeMethodAsync(
|
||||
HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
|
||||
{
|
||||
var ip = accessor.GetIpAddress();
|
||||
var client = new ClientRequestIdentity
|
||||
{
|
||||
ClientIp = ip,
|
||||
Path = invocationContext.HubMethodName,
|
||||
HttpVerb = "ws",
|
||||
ClientId = invocationContext.Context.UserIdentifier,
|
||||
};
|
||||
foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false))
|
||||
{
|
||||
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
|
||||
if (counter.Count > rule.Limit)
|
||||
{
|
||||
var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => string.Equals(c.Type, MareClaimTypes.Uid, StringComparison.Ordinal))?.Value ?? "Unknown";
|
||||
var retry = counter.Timestamp.RetryAfterFrom(rule);
|
||||
logger.LogWarning("Method rate limit triggered from {ip}/{authUserId}: {method}", ip, authUserId, invocationContext.HubMethodName);
|
||||
throw new HubException($"call limit {retry}");
|
||||
}
|
||||
}
|
||||
|
||||
return await next(invocationContext).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Optional method
|
||||
public async Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
|
||||
{
|
||||
await ConnectionLimiterSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var ip = accessor.GetIpAddress();
|
||||
var client = new ClientRequestIdentity
|
||||
{
|
||||
ClientIp = ip,
|
||||
Path = "Connect",
|
||||
HttpVerb = "ws",
|
||||
};
|
||||
foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false))
|
||||
{
|
||||
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
|
||||
if (counter.Count > rule.Limit)
|
||||
{
|
||||
var retry = counter.Timestamp.RetryAfterFrom(rule);
|
||||
logger.LogWarning("Connection rate limit triggered from {ip}", ip);
|
||||
ConnectionLimiterSemaphore.Release();
|
||||
throw new HubException($"Connection rate limit {retry}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await Task.Delay(25).ConfigureAwait(false);
|
||||
await next(context).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error on OnConnectedAsync");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ConnectionLimiterSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnDisconnectedAsync(
|
||||
HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next)
|
||||
{
|
||||
await DisconnectLimiterSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
if (exception != null)
|
||||
{
|
||||
logger.LogWarning(exception, "InitialException on OnDisconnectedAsync");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await next(context, exception).ConfigureAwait(false);
|
||||
await Task.Delay(25).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "ThrownException on OnDisconnectedAsync");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisconnectLimiterSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<UserSecretsId>aspnet-MareSynchronosServer-BA82A12A-0B30-463C-801D-B7E81318CD50</UserSecretsId>
|
||||
<AssemblyVersion>1.1.0.0</AssemblyVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="appsettings.Development.json" />
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.Development.json" />
|
||||
<None Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
<PackageReference Include="IDisposableAnalyzers" Version="4.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.149">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.5.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
77
MareSynchronosServer/MareSynchronosServer/Program.cs
Normal file
77
MareSynchronosServer/MareSynchronosServer/Program.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
|
||||
namespace MareSynchronosServer;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var hostBuilder = CreateHostBuilder(args);
|
||||
var host = hostBuilder.Build();
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
using var context = services.GetRequiredService<MareDbContext>();
|
||||
var options = services.GetRequiredService<IConfigurationService<ServerConfiguration>>();
|
||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
if (options.IsMain)
|
||||
{
|
||||
context.Database.Migrate();
|
||||
context.SaveChanges();
|
||||
|
||||
// clean up residuals
|
||||
var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null);
|
||||
context.RemoveRange(unfinishedRegistrations);
|
||||
context.SaveChanges();
|
||||
|
||||
logger.LogInformation(options.ToString());
|
||||
}
|
||||
|
||||
var metrics = services.GetRequiredService<MareMetrics>();
|
||||
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, context.Users.AsNoTracking().Count());
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.AsNoTracking().Count());
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.AsNoTracking().Count(p => p.IsPaused));
|
||||
|
||||
}
|
||||
|
||||
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
|
||||
{
|
||||
try
|
||||
{
|
||||
host.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args)
|
||||
{
|
||||
var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddConsole();
|
||||
});
|
||||
var logger = loggerFactory.CreateLogger<Startup>();
|
||||
return Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseContentRoot(AppContext.BaseDirectory);
|
||||
webBuilder.ConfigureLogging((ctx, builder) =>
|
||||
{
|
||||
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
|
||||
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
|
||||
});
|
||||
webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profiles": {
|
||||
"MareSynchronosServer": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": false,
|
||||
//"applicationUrl": "https://localhost:5001;http://localhost:5000;https://192.168.1.124:5001;http://192.168.1.124:5000",
|
||||
"applicationUrl": "http://localhost:5000;https://localhost:5001;https://darkarchon.internet-box.ch:5001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using MareSynchronosShared.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public class CharaDataCleanupService : IHostedService
|
||||
{
|
||||
private readonly ILogger<CharaDataCleanupService> _logger;
|
||||
private readonly IDbContextFactory<MareDbContext> _dbContextFactory;
|
||||
private readonly CancellationTokenSource _cleanupCts = new();
|
||||
|
||||
public CharaDataCleanupService(ILogger<CharaDataCleanupService> logger, IDbContextFactory<MareDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = Cleanup(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Cleanup(CancellationToken ct)
|
||||
{
|
||||
_logger.LogInformation("CharaData Cleanup Service started");
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
using (var db = await _dbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
var dateTime = DateTime.UtcNow;
|
||||
var expiredData = await db.CharaData.Where(c => c.ExpiryDate <= DateTime.UtcNow).ToListAsync(cancellationToken: ct).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Removing {count} expired Chara Data entries", expiredData.Count);
|
||||
|
||||
db.RemoveRange(expiredData);
|
||||
await db.SaveChangesAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromHours(12), ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cleanupCts?.Cancel();
|
||||
_cleanupCts?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using StackExchange.Redis.Extensions.Core.Abstractions;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public sealed class GPoseLobbyDistributionService : IHostedService, IDisposable
|
||||
{
|
||||
private CancellationTokenSource _runtimeCts = new();
|
||||
private readonly Dictionary<string, Dictionary<string, WorldData>> _lobbyWorldData = [];
|
||||
private readonly Dictionary<string, Dictionary<string, PoseData>> _lobbyPoseData = [];
|
||||
private readonly SemaphoreSlim _lobbyPoseDataModificationSemaphore = new(1, 1);
|
||||
private readonly SemaphoreSlim _lobbyWorldDataModificationSemaphore = new(1, 1);
|
||||
|
||||
public GPoseLobbyDistributionService(ILogger<GPoseLobbyDistributionService> logger, IRedisDatabase redisDb,
|
||||
IHubContext<MareHub, IMareHub> hubContext)
|
||||
{
|
||||
_logger = logger;
|
||||
_redisDb = redisDb;
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private readonly ILogger<GPoseLobbyDistributionService> _logger;
|
||||
private readonly IRedisDatabase _redisDb;
|
||||
private readonly IHubContext<MareHub, IMareHub> _hubContext;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_runtimeCts.Cancel();
|
||||
_runtimeCts.Dispose();
|
||||
_lobbyPoseDataModificationSemaphore.Dispose();
|
||||
_lobbyWorldDataModificationSemaphore.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async Task PushWorldData(string lobby, string user, WorldData worldData)
|
||||
{
|
||||
await _lobbyWorldDataModificationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!_lobbyWorldData.TryGetValue(lobby, out var worldDataDict))
|
||||
{
|
||||
_lobbyWorldData[lobby] = worldDataDict = new(StringComparer.Ordinal);
|
||||
}
|
||||
worldDataDict[user] = worldData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pushing World Data for Lobby {lobby} by User {user}", lobby, user);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyWorldDataModificationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushPoseData(string lobby, string user, PoseData poseData)
|
||||
{
|
||||
await _lobbyPoseDataModificationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!_lobbyPoseData.TryGetValue(lobby, out var poseDataDict))
|
||||
{
|
||||
_lobbyPoseData[lobby] = poseDataDict = new(StringComparer.Ordinal);
|
||||
}
|
||||
poseDataDict[user] = poseData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pushing World Data for Lobby {lobby} by User {user}", lobby, user);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyPoseDataModificationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = WorldDataDistribution(_runtimeCts.Token);
|
||||
_ = PoseDataDistribution(_runtimeCts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task WorldDataDistribution(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DistributeWorldData(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during World Data Distribution");
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PoseDataDistribution(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DistributePoseData(token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pose Data Distribution");
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DistributeWorldData(CancellationToken token)
|
||||
{
|
||||
await _lobbyWorldDataModificationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
Dictionary<string, Dictionary<string, WorldData>> clone = [];
|
||||
try
|
||||
{
|
||||
clone = _lobbyWorldData.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
|
||||
_lobbyWorldData.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Distributing World Data Clone generation");
|
||||
_lobbyWorldData.Clear();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyWorldDataModificationSemaphore.Release();
|
||||
}
|
||||
|
||||
foreach (var lobbyId in clone)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (!lobbyId.Value.Values.Any())
|
||||
continue;
|
||||
|
||||
var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false);
|
||||
if (gposeLobbyUsers == null)
|
||||
continue;
|
||||
|
||||
foreach (var data in lobbyId.Value)
|
||||
{
|
||||
await _hubContext.Clients.Users(gposeLobbyUsers.Where(k => !string.Equals(k, data.Key, StringComparison.Ordinal)))
|
||||
.Client_GposeLobbyPushWorldData(new(data.Key), data.Value).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during World Data Distribution for Lobby {lobby}", lobbyId.Key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DistributePoseData(CancellationToken token)
|
||||
{
|
||||
await _lobbyPoseDataModificationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
Dictionary<string, Dictionary<string, PoseData>> clone = [];
|
||||
try
|
||||
{
|
||||
clone = _lobbyPoseData.ToDictionary(k => k.Key, k => k.Value, StringComparer.Ordinal);
|
||||
_lobbyPoseData.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Distributing Pose Data Clone generation");
|
||||
_lobbyPoseData.Clear();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lobbyPoseDataModificationSemaphore.Release();
|
||||
}
|
||||
|
||||
foreach (var lobbyId in clone)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (!lobbyId.Value.Values.Any())
|
||||
continue;
|
||||
|
||||
var gposeLobbyUsers = await _redisDb.GetAsync<List<string>>($"GposeLobby:{lobbyId.Key}").ConfigureAwait(false);
|
||||
if (gposeLobbyUsers == null)
|
||||
continue;
|
||||
|
||||
foreach (var data in lobbyId.Value)
|
||||
{
|
||||
await _hubContext.Clients.Users(gposeLobbyUsers.Where(k => !string.Equals(k, data.Key, StringComparison.Ordinal)))
|
||||
.Client_GposeLobbyPushPoseData(new(data.Key), data.Value).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Pose Data Distribution for Lobby {lobby}", lobbyId.Key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_runtimeCts.Cancel();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using MareSynchronos.API.Dto;
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StackExchange.Redis.Extensions.Core.Abstractions;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public class SystemInfoService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly MareMetrics _mareMetrics;
|
||||
private readonly IConfigurationService<ServerConfiguration> _config;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<SystemInfoService> _logger;
|
||||
private readonly IHubContext<MareHub, IMareHub> _hubContext;
|
||||
private readonly IRedisDatabase _redis;
|
||||
private Timer _timer;
|
||||
public SystemInfoDto SystemInfoDto { get; private set; } = new();
|
||||
|
||||
public SystemInfoService(MareMetrics mareMetrics, IConfigurationService<ServerConfiguration> configurationService, IServiceProvider services,
|
||||
ILogger<SystemInfoService> logger, IHubContext<MareHub, IMareHub> hubContext, IRedisDatabase redisDb)
|
||||
{
|
||||
_mareMetrics = mareMetrics;
|
||||
_config = configurationService;
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
_hubContext = hubContext;
|
||||
_redis = redisDb;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("System Info Service started");
|
||||
|
||||
var timeOut = _config.IsMain ? 5 : 15;
|
||||
|
||||
_timer = new Timer(PushSystemInfo, null, TimeSpan.Zero, TimeSpan.FromSeconds(timeOut));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void PushSystemInfo(object state)
|
||||
{
|
||||
try
|
||||
{
|
||||
ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads);
|
||||
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads);
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
|
||||
|
||||
var onlineUsers = (_redis.SearchKeysAsync("UID:*").GetAwaiter().GetResult()).Count();
|
||||
SystemInfoDto = new SystemInfoDto()
|
||||
{
|
||||
OnlineUsers = onlineUsers,
|
||||
};
|
||||
|
||||
if (_config.IsMain)
|
||||
{
|
||||
_logger.LogTrace("Sending System Info, Online Users: {onlineUsers}", onlineUsers);
|
||||
|
||||
_hubContext.Clients.All.Client_UpdateSystemInfo(SystemInfoDto);
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
using var db = scope.ServiceProvider.GetService<MareDbContext>()!;
|
||||
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAuthorizedConnections, onlineUsers);
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.AsNoTracking().Count());
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.ClientPairs.AsNoTracking().Count(p => p.IsPaused));
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.AsNoTracking().Count());
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.AsNoTracking().Count());
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairsPaused, db.GroupPairs.AsNoTracking().Count(p => p.IsPaused));
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeUsersRegistered, db.Users.AsNoTracking().Count());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to push system info");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_timer?.Change(Timeout.Infinite, 0);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Models;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosServer.Services;
|
||||
|
||||
public class UserCleanupService : IHostedService
|
||||
{
|
||||
private readonly MareMetrics metrics;
|
||||
private readonly ILogger<UserCleanupService> _logger;
|
||||
private readonly IDbContextFactory<MareDbContext> _mareDbContextFactory;
|
||||
private readonly IConfigurationService<ServerConfiguration> _configuration;
|
||||
private CancellationTokenSource _cleanupCts;
|
||||
|
||||
public UserCleanupService(MareMetrics metrics, ILogger<UserCleanupService> logger, IDbContextFactory<MareDbContext> mareDbContextFactory, IConfigurationService<ServerConfiguration> configuration)
|
||||
{
|
||||
this.metrics = metrics;
|
||||
_logger = logger;
|
||||
_mareDbContextFactory = mareDbContextFactory;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Cleanup Service started");
|
||||
_cleanupCts = new();
|
||||
|
||||
_ = CleanUp(_cleanupCts.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CleanUp(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
using (var dbContext = await _mareDbContextFactory.CreateDbContextAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
CleanUpOutdatedLodestoneAuths(dbContext);
|
||||
|
||||
await PurgeUnusedAccounts(dbContext).ConfigureAwait(false);
|
||||
|
||||
await PurgeTempInvites(dbContext).ConfigureAwait(false);
|
||||
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
TimeOnly currentTime = new(now.Hour, now.Minute, now.Second);
|
||||
TimeOnly futureTime = new(now.Hour, now.Minute - now.Minute % 10, 0);
|
||||
var span = futureTime.AddMinutes(10) - currentTime;
|
||||
|
||||
_logger.LogInformation("User Cleanup Complete, next run at {date}", now.Add(span));
|
||||
await Task.Delay(span, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PurgeTempInvites(MareDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempInvites = await dbContext.GroupTempInvites.ToListAsync().ConfigureAwait(false);
|
||||
dbContext.RemoveRange(tempInvites.Where(i => i.ExpirationDate < DateTime.UtcNow));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during Temp Invite purge");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PurgeUnusedAccounts(MareDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccounts), false))
|
||||
{
|
||||
var usersOlderThanDays = _configuration.GetValueOrDefault(nameof(ServerConfiguration.PurgeUnusedAccountsPeriodInDays), 14);
|
||||
var maxGroupsByUser = _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 3);
|
||||
|
||||
_logger.LogInformation("Cleaning up users older than {usersOlderThanDays} days", usersOlderThanDays);
|
||||
|
||||
var allUsers = dbContext.Users.Where(u => string.IsNullOrEmpty(u.Alias)).ToList();
|
||||
List<User> usersToRemove = new();
|
||||
foreach (var user in allUsers)
|
||||
{
|
||||
if (user.LastLoggedIn < DateTime.UtcNow - TimeSpan.FromDays(usersOlderThanDays))
|
||||
{
|
||||
_logger.LogInformation("User outdated: {userUID}", user.UID);
|
||||
usersToRemove.Add(user);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var user in usersToRemove)
|
||||
{
|
||||
await SharedDbFunctions.PurgeUser(_logger, user, dbContext, maxGroupsByUser).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Cleaning up unauthorized users");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during user purge");
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanUpOutdatedLodestoneAuths(MareDbContext dbContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Cleaning up expired lodestone authentications");
|
||||
var lodestoneAuths = dbContext.LodeStoneAuth.Include(u => u.User).Where(a => a.StartedAt != null).ToList();
|
||||
List<LodeStoneAuth> expiredAuths = new List<LodeStoneAuth>();
|
||||
foreach (var auth in lodestoneAuths)
|
||||
{
|
||||
if (auth.StartedAt < DateTime.UtcNow - TimeSpan.FromMinutes(15))
|
||||
{
|
||||
expiredAuths.Add(auth);
|
||||
}
|
||||
}
|
||||
|
||||
dbContext.Users.RemoveRange(expiredAuths.Where(u => u.User != null).Select(a => a.User));
|
||||
dbContext.RemoveRange(expiredAuths);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during expired auths cleanup");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PurgeUser(User user, MareDbContext dbContext)
|
||||
{
|
||||
_logger.LogInformation("Purging user: {uid}", user.UID);
|
||||
|
||||
var lodestone = dbContext.LodeStoneAuth.SingleOrDefault(a => a.User.UID == user.UID);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
dbContext.Remove(lodestone);
|
||||
}
|
||||
|
||||
var auth = dbContext.Auth.Single(a => a.UserUID == user.UID);
|
||||
|
||||
var ownPairData = dbContext.ClientPairs.Where(u => u.User.UID == user.UID).ToList();
|
||||
dbContext.ClientPairs.RemoveRange(ownPairData);
|
||||
var otherPairData = dbContext.ClientPairs.Include(u => u.User)
|
||||
.Where(u => u.OtherUser.UID == user.UID).ToList();
|
||||
dbContext.ClientPairs.RemoveRange(otherPairData);
|
||||
|
||||
var userJoinedGroups = await dbContext.GroupPairs.Include(g => g.Group).Where(u => u.GroupUserUID == user.UID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
foreach (var userGroupPair in userJoinedGroups)
|
||||
{
|
||||
bool ownerHasLeft = string.Equals(userGroupPair.Group.OwnerUID, user.UID, StringComparison.Ordinal);
|
||||
|
||||
if (ownerHasLeft)
|
||||
{
|
||||
var groupPairs = await dbContext.GroupPairs.Where(g => g.GroupGID == userGroupPair.GroupGID && g.GroupUserUID != user.UID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
if (!groupPairs.Any())
|
||||
{
|
||||
_logger.LogInformation("Group {gid} has no new owner, deleting", userGroupPair.GroupGID);
|
||||
dbContext.Groups.Remove(userGroupPair.Group);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = await SharedDbFunctions.MigrateOrDeleteGroup(dbContext, userGroupPair.Group, groupPairs, _configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
dbContext.GroupPairs.Remove(userGroupPair);
|
||||
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation("User purged: {uid}", user.UID);
|
||||
|
||||
dbContext.Auth.Remove(auth);
|
||||
dbContext.Users.Remove(user);
|
||||
|
||||
await dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_cleanupCts.Cancel();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
343
MareSynchronosServer/MareSynchronosServer/Startup.cs
Normal file
343
MareSynchronosServer/MareSynchronosServer/Startup.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using AspNetCoreRateLimit;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosServer.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using MareSynchronosShared.Services;
|
||||
using Prometheus;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using StackExchange.Redis;
|
||||
using StackExchange.Redis.Extensions.Core.Configuration;
|
||||
using System.Net;
|
||||
using StackExchange.Redis.Extensions.System.Text.Json;
|
||||
using MareSynchronos.API.SignalR;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using MareSynchronosServer.Controllers;
|
||||
using MareSynchronosShared.RequirementHandlers;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
|
||||
namespace MareSynchronosServer;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
private readonly ILogger<Startup> _logger;
|
||||
|
||||
public Startup(IConfiguration configuration, ILogger<Startup> logger)
|
||||
{
|
||||
Configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.AddTransient(_ => Configuration);
|
||||
|
||||
var mareConfig = Configuration.GetRequiredSection("MareSynchronos");
|
||||
|
||||
// configure metrics
|
||||
ConfigureMetrics(services);
|
||||
|
||||
// configure database
|
||||
ConfigureDatabase(services, mareConfig);
|
||||
|
||||
// configure authentication and authorization
|
||||
ConfigureAuthorization(services);
|
||||
|
||||
// configure rate limiting
|
||||
ConfigureIpRateLimiting(services);
|
||||
|
||||
// configure SignalR
|
||||
ConfigureSignalR(services, mareConfig);
|
||||
|
||||
// configure mare specific services
|
||||
ConfigureMareServices(services, mareConfig);
|
||||
|
||||
services.AddHealthChecks();
|
||||
services.AddControllers().ConfigureApplicationPartManager(a =>
|
||||
{
|
||||
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
|
||||
if (mareConfig.GetValue<Uri>(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null)
|
||||
{
|
||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(MareServerConfigurationController), typeof(MareBaseConfigurationController), typeof(ClientMessageController), typeof(MainController)));
|
||||
}
|
||||
else
|
||||
{
|
||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureMareServices(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
{
|
||||
bool isMainServer = mareConfig.GetValue<Uri>(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null;
|
||||
|
||||
services.Configure<ServerConfiguration>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
services.Configure<MareConfigurationBase>(Configuration.GetRequiredSection("MareSynchronos"));
|
||||
|
||||
services.AddSingleton<ServerTokenGenerator>();
|
||||
services.AddSingleton<SystemInfoService>();
|
||||
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
|
||||
// configure services based on main server status
|
||||
ConfigureServicesBasedOnShardType(services, mareConfig, isMainServer);
|
||||
|
||||
if (isMainServer)
|
||||
{
|
||||
services.AddSingleton<UserCleanupService>();
|
||||
services.AddHostedService(provider => provider.GetService<UserCleanupService>());
|
||||
services.AddSingleton<CharaDataCleanupService>();
|
||||
services.AddHostedService(provider => provider.GetService<CharaDataCleanupService>());
|
||||
}
|
||||
|
||||
services.AddSingleton<GPoseLobbyDistributionService>();
|
||||
services.AddHostedService(provider => provider.GetService<GPoseLobbyDistributionService>());
|
||||
}
|
||||
|
||||
private static void ConfigureSignalR(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
{
|
||||
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
||||
|
||||
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
|
||||
{
|
||||
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
|
||||
hubOptions.EnableDetailedErrors = true;
|
||||
hubOptions.MaximumParallelInvocationsPerClient = 10;
|
||||
hubOptions.StreamBufferCapacity = 200;
|
||||
|
||||
hubOptions.AddFilter<SignalRLimitFilter>();
|
||||
}).AddMessagePackProtocol(opt =>
|
||||
{
|
||||
var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance,
|
||||
BuiltinResolver.Instance,
|
||||
AttributeFormatterResolver.Instance,
|
||||
// replace enum resolver
|
||||
DynamicEnumAsStringResolver.Instance,
|
||||
DynamicGenericResolver.Instance,
|
||||
DynamicUnionResolver.Instance,
|
||||
DynamicObjectResolver.Instance,
|
||||
PrimitiveObjectResolver.Instance,
|
||||
// final fallback(last priority)
|
||||
StandardResolver.Instance);
|
||||
|
||||
opt.SerializerOptions = MessagePackSerializerOptions.Standard
|
||||
.WithCompression(MessagePackCompression.Lz4Block)
|
||||
.WithResolver(resolver);
|
||||
});
|
||||
|
||||
// configure redis for SignalR
|
||||
var redisConnection = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
|
||||
signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { });
|
||||
|
||||
var options = ConfigurationOptions.Parse(redisConnection);
|
||||
|
||||
var endpoint = options.EndPoints[0];
|
||||
string address = "";
|
||||
int port = 0;
|
||||
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; }
|
||||
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; }
|
||||
var redisConfiguration = new RedisConfiguration()
|
||||
{
|
||||
AbortOnConnectFail = true,
|
||||
KeyPrefix = "",
|
||||
Hosts = new RedisHost[]
|
||||
{
|
||||
new RedisHost(){ Host = address, Port = port },
|
||||
},
|
||||
AllowAdmin = true,
|
||||
ConnectTimeout = options.ConnectTimeout,
|
||||
Database = 0,
|
||||
Ssl = false,
|
||||
Password = options.Password,
|
||||
ServerEnumerationStrategy = new ServerEnumerationStrategy()
|
||||
{
|
||||
Mode = ServerEnumerationStrategy.ModeOptions.All,
|
||||
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
|
||||
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw,
|
||||
},
|
||||
MaxValueLength = 1024,
|
||||
PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
|
||||
SyncTimeout = options.SyncTimeout,
|
||||
};
|
||||
|
||||
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);
|
||||
}
|
||||
|
||||
private void ConfigureIpRateLimiting(IServiceCollection services)
|
||||
{
|
||||
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
|
||||
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
|
||||
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
services.AddMemoryCache();
|
||||
services.AddInMemoryRateLimiting();
|
||||
}
|
||||
|
||||
private static void ConfigureAuthorization(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
|
||||
|
||||
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
||||
.Configure<IConfigurationService<MareConfigurationBase>>((options, config) =>
|
||||
{
|
||||
options.TokenValidationParameters = new()
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateLifetime = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(MareConfigurationBase.Jwt)))),
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.DefaultPolicy = new AuthorizationPolicyBuilder()
|
||||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser().Build();
|
||||
options.AddPolicy("Authenticated", policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
|
||||
policy.RequireAuthenticatedUser();
|
||||
});
|
||||
options.AddPolicy("Identified", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified));
|
||||
});
|
||||
options.AddPolicy("Admin", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator));
|
||||
});
|
||||
options.AddPolicy("Moderator", policy =>
|
||||
{
|
||||
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator));
|
||||
});
|
||||
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(MareClaimTypes.Internal, "true").Build());
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureDatabase(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
{
|
||||
services.AddDbContextPool<MareDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("MareSynchronosShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
|
||||
services.AddDbContextFactory<MareDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("MareSynchronosShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureMetrics(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<MareMetrics>(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string>
|
||||
{
|
||||
MetricsAPI.CounterInitializedConnections,
|
||||
MetricsAPI.CounterUserPushData,
|
||||
MetricsAPI.CounterUserPushDataTo,
|
||||
MetricsAPI.CounterUsersRegisteredDeleted,
|
||||
MetricsAPI.CounterAuthenticationCacheHits,
|
||||
MetricsAPI.CounterAuthenticationFailures,
|
||||
MetricsAPI.CounterAuthenticationRequests,
|
||||
MetricsAPI.CounterAuthenticationSuccesses,
|
||||
}, new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeAuthorizedConnections,
|
||||
MetricsAPI.GaugeConnections,
|
||||
MetricsAPI.GaugePairs,
|
||||
MetricsAPI.GaugePairsPaused,
|
||||
MetricsAPI.GaugeAvailableIOWorkerThreads,
|
||||
MetricsAPI.GaugeAvailableWorkerThreads,
|
||||
MetricsAPI.GaugeGroups,
|
||||
MetricsAPI.GaugeGroupPairs,
|
||||
MetricsAPI.GaugeGroupPairsPaused,
|
||||
MetricsAPI.GaugeUsersRegistered,
|
||||
MetricsAPI.GaugeGposeLobbies,
|
||||
MetricsAPI.GaugeGposeLobbyUsers
|
||||
}));
|
||||
}
|
||||
|
||||
private static void ConfigureServicesBasedOnShardType(IServiceCollection services, IConfigurationSection mareConfig, bool isMainServer)
|
||||
{
|
||||
if (!isMainServer)
|
||||
{
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, MareConfigurationServiceClient<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<MareConfigurationBase>, MareConfigurationServiceClient<MareConfigurationBase>>();
|
||||
|
||||
services.AddHostedService(p => (MareConfigurationServiceClient<ServerConfiguration>)p.GetService<IConfigurationService<ServerConfiguration>>());
|
||||
services.AddHostedService(p => (MareConfigurationServiceClient<MareConfigurationBase>)p.GetService<IConfigurationService<MareConfigurationBase>>());
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IConfigurationService<ServerConfiguration>, MareConfigurationServiceServer<ServerConfiguration>>();
|
||||
services.AddSingleton<IConfigurationService<MareConfigurationBase>, MareConfigurationServiceServer<MareConfigurationBase>>();
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
|
||||
{
|
||||
logger.LogInformation("Running Configure");
|
||||
|
||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
|
||||
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseWebSockets();
|
||||
app.UseHttpMetrics();
|
||||
|
||||
var metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(MareConfigurationBase.MetricsPort), 4980));
|
||||
metricServer.Start();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapHub<MareHub>(IMareHub.Path, options =>
|
||||
{
|
||||
options.ApplicationMaxBufferSize = 5242880;
|
||||
options.TransportMaxBufferSize = 5242880;
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
|
||||
});
|
||||
|
||||
endpoints.MapHealthChecks("/health").AllowAnonymous();
|
||||
endpoints.MapControllers();
|
||||
|
||||
foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>())
|
||||
{
|
||||
if (source == null) continue;
|
||||
_logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Data.Enum;
|
||||
using MareSynchronos.API.Data.Extensions;
|
||||
using MareSynchronosShared.Models;
|
||||
|
||||
namespace MareSynchronosServer.Utils
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static GroupData ToGroupData(this Group group)
|
||||
{
|
||||
return new GroupData(group.GID, group.Alias);
|
||||
}
|
||||
|
||||
public static UserData ToUserData(this GroupPair pair)
|
||||
{
|
||||
return new UserData(pair.GroupUser.UID, pair.GroupUser.Alias);
|
||||
}
|
||||
|
||||
public static UserData ToUserData(this User user)
|
||||
{
|
||||
return new UserData(user.UID, user.Alias);
|
||||
}
|
||||
|
||||
public static GroupPermissions GetGroupPermissions(this Group group)
|
||||
{
|
||||
var permissions = GroupPermissions.NoneSet;
|
||||
permissions.SetDisableAnimations(group.DisableAnimations);
|
||||
permissions.SetDisableSounds(group.DisableSounds);
|
||||
permissions.SetDisableInvites(!group.InvitesEnabled);
|
||||
permissions.SetDisableVFX(group.DisableVFX);
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public static GroupUserPermissions GetGroupPairPermissions(this GroupPair groupPair)
|
||||
{
|
||||
var permissions = GroupUserPermissions.NoneSet;
|
||||
permissions.SetDisableAnimations(groupPair.DisableAnimations);
|
||||
permissions.SetDisableSounds(groupPair.DisableSounds);
|
||||
permissions.SetPaused(groupPair.IsPaused);
|
||||
permissions.SetDisableVFX(groupPair.DisableVFX);
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public static GroupUserInfo GetGroupPairUserInfo(this GroupPair groupPair)
|
||||
{
|
||||
var groupUserInfo = GroupUserInfo.None;
|
||||
groupUserInfo.SetPinned(groupPair.IsPinned);
|
||||
groupUserInfo.SetModerator(groupPair.IsModerator);
|
||||
return groupUserInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using MareSynchronosServer.Hubs;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public class MareHubLogger
|
||||
{
|
||||
private readonly MareHub _hub;
|
||||
private readonly ILogger<MareHub> _logger;
|
||||
|
||||
public MareHubLogger(MareHub hub, ILogger<MareHub> logger)
|
||||
{
|
||||
_hub = hub;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static object[] Args(params object[] args)
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
public void LogCallInfo(object[] args = null, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
string formattedArgs = args != null && args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogInformation("{uid}:{method}{args}", _hub.UserUID, methodName, formattedArgs);
|
||||
}
|
||||
|
||||
public void LogCallWarning(object[] args = null, [CallerMemberName] string methodName = "")
|
||||
{
|
||||
string formattedArgs = args != null && args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogWarning("{uid}:{method}{args}", _hub.UserUID, methodName, formattedArgs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public enum PauseInfo
|
||||
{
|
||||
NoConnection,
|
||||
Paused,
|
||||
Unpaused,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public record PauseState
|
||||
{
|
||||
public string GID { get; set; }
|
||||
public bool IsPaused => IsSelfPaused || IsOtherPaused;
|
||||
public bool IsSelfPaused { get; set; }
|
||||
public bool IsOtherPaused { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public record PausedEntry
|
||||
{
|
||||
public string UID { get; set; }
|
||||
public List<PauseState> PauseStates { get; set; } = new();
|
||||
|
||||
public PauseInfo IsDirectlyPaused => PauseStateWithoutGroups == null ? PauseInfo.NoConnection
|
||||
: PauseStates.First(g => g.GID == null).IsPaused ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
|
||||
public PauseInfo IsPausedPerGroup => !PauseStatesWithoutDirect.Any() ? PauseInfo.NoConnection
|
||||
: PauseStatesWithoutDirect.All(p => p.IsPaused) ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
|
||||
private IEnumerable<PauseState> PauseStatesWithoutDirect => PauseStates.Where(f => f.GID != null);
|
||||
private PauseState PauseStateWithoutGroups => PauseStates.SingleOrDefault(p => p.GID == null);
|
||||
|
||||
public bool IsPaused
|
||||
{
|
||||
get
|
||||
{
|
||||
var isDirectlyPaused = IsDirectlyPaused;
|
||||
bool result;
|
||||
if (isDirectlyPaused != PauseInfo.NoConnection)
|
||||
{
|
||||
result = isDirectlyPaused == PauseInfo.Paused;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = IsPausedPerGroup == PauseInfo.Paused;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public PauseInfo IsOtherPausedForSpecificGroup(string gid)
|
||||
{
|
||||
var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal));
|
||||
if (state == null) return PauseInfo.NoConnection;
|
||||
return state.IsOtherPaused ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
}
|
||||
|
||||
public PauseInfo IsPausedForSpecificGroup(string gid)
|
||||
{
|
||||
var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal));
|
||||
if (state == null) return PauseInfo.NoConnection;
|
||||
return state.IsPaused ? PauseInfo.Paused : PauseInfo.NoConnection;
|
||||
}
|
||||
|
||||
public PauseInfo IsPausedExcludingGroup(string gid)
|
||||
{
|
||||
var states = PauseStatesWithoutDirect.Where(f => !string.Equals(f.GID, gid, StringComparison.Ordinal)).ToList();
|
||||
if (!states.Any()) return PauseInfo.NoConnection;
|
||||
var result = states.All(p => p.IsPaused);
|
||||
if (result) return PauseInfo.Paused;
|
||||
return PauseInfo.Unpaused;
|
||||
}
|
||||
}
|
||||
12
MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs
Normal file
12
MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
private record UserPair
|
||||
{
|
||||
public string UserUID { get; set; }
|
||||
public string OtherUserUID { get; set; }
|
||||
public bool UserPausedOther { get; set; }
|
||||
public bool OtherPausedUser { get; set; }
|
||||
}
|
||||
}
|
||||
61
MareSynchronosServer/MareSynchronosServer/appsettings.json
Normal file
61
MareSynchronosServer/MareSynchronosServer/appsettings.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=mare;Username=postgres"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"MareSynchronosServer.Authentication": "Warning",
|
||||
"System.IO.IOException": "Warning"
|
||||
},
|
||||
"File": {
|
||||
"BasePath": "logs",
|
||||
"FileAccessMode": "KeepOpenAndAutoFlush",
|
||||
"FileEncodingName": "utf-8",
|
||||
"DateFormat": "yyyMMdd",
|
||||
"MaxFileSize": 10485760,
|
||||
"Files": [
|
||||
{
|
||||
"Path": "mare-<counter>.log"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"MareSynchronos": {
|
||||
"DbContextPoolSize": 2000,
|
||||
"CdnFullUrl": "https://<url or ip to your server>/cache/",
|
||||
"ServiceAddress": "http://localhost:5002",
|
||||
"StaticFileServiceAddress": "http://localhost:5003"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Https": {
|
||||
"Url": "https://+:5000",
|
||||
"Certificate": {
|
||||
"Subject": "darkarchon.internet-box.ch",
|
||||
"Store": "My",
|
||||
"Location": "LocalMachine"
|
||||
//"AllowInvalid": false
|
||||
// "Path": "", //use path, keypath and password to provide a valid certificate if not using windows key store
|
||||
// "KeyPath": ""
|
||||
// "Password": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"IpRateLimiting": {
|
||||
"EnableEndpointRateLimiting": false,
|
||||
"StackBlockedRequests": false,
|
||||
"RealIpHeader": "X-Real-IP",
|
||||
"ClientIdHeader": "X-ClientId",
|
||||
"HttpStatusCode": 429,
|
||||
"IpWhitelist": [ ],
|
||||
"GeneralRules": [ ]
|
||||
},
|
||||
"IPRateLimitPolicies": {
|
||||
"IpRules": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user