Syncshells (#11)
* some groups stuff * further groups rework * fixes for pause changes * adjsut timeout interval * fixes and namespace change to file scoped * more fixes * further implement groups * fix change group ownership * add some more stuff for groups * more fixes and additions * some fixes based on analyzers, add shard info to ui * add discord command, cleanup * fix regex * add group migration and deletion on user deletion * add api method for client to check health of connection * adjust regex for vanity * fixes for server and bot * fixes some string comparison in linq queries * fixes group leave and sets alias to null * fix syntax in changeownership * add better logging, fixes for group leaving * fixes for group leave Co-authored-by: Stanley Dimant <root.darkarchon@outlook.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "6.0.6",
|
||||
"version": "6.0.9",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace MareSynchronosServer.Hubs
|
||||
{
|
||||
public class IdBasedUserIdProvider : IUserIdProvider
|
||||
{
|
||||
public string GetUserId(HubConnectionContext context)
|
||||
{
|
||||
return context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,165 +8,164 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosServer.Hubs
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
public partial class MareHub
|
||||
private bool IsAdmin => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsAdmin;
|
||||
|
||||
private bool IsModerator => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsModerator || IsAdmin;
|
||||
|
||||
private List<string> OnlineAdmins => _dbContext.Users.Where(u => (u.IsModerator || u.IsAdmin)).Select(u => u.UID).ToList();
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminChangeModeratorStatus)]
|
||||
public async Task ChangeModeratorStatus(string uid, bool isModerator)
|
||||
{
|
||||
private bool IsAdmin => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsAdmin;
|
||||
if (!IsAdmin) return;
|
||||
var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
|
||||
private bool IsModerator => _dbContext.Users.Single(b => b.UID == AuthenticatedUserId).IsModerator || IsAdmin;
|
||||
if (user == null) return;
|
||||
|
||||
private List<string> OnlineAdmins => _dbContext.Users.Where(u => (u.IsModerator || u.IsAdmin)).Select(u => u.UID).ToList();
|
||||
user.IsModerator = isModerator;
|
||||
_dbContext.Update(user);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminChangeModeratorStatus)]
|
||||
public async Task ChangeModeratorStatus(string uid, bool isModerator)
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminDeleteBannedUser)]
|
||||
public async Task DeleteBannedUser(BannedUserDto dto)
|
||||
{
|
||||
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
|
||||
|
||||
var existingUser =
|
||||
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
|
||||
if (existingUser == null)
|
||||
{
|
||||
if (!IsAdmin) return;
|
||||
var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
|
||||
if (user == null) return;
|
||||
|
||||
user.IsModerator = isModerator;
|
||||
_dbContext.Update(user);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(user.UID).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminDeleteBannedUser)]
|
||||
public async Task DeleteBannedUser(BannedUserDto dto)
|
||||
_dbContext.Remove(existingUser);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminDeleteForbiddenFile)]
|
||||
public async Task DeleteForbiddenFile(ForbiddenFileDto dto)
|
||||
{
|
||||
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
|
||||
|
||||
var existingFile =
|
||||
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
|
||||
if (existingFile == null)
|
||||
{
|
||||
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
|
||||
|
||||
var existingUser =
|
||||
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
|
||||
if (existingUser == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dbContext.Remove(existingUser);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteBannedUser, dto).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminDeleteForbiddenFile)]
|
||||
public async Task DeleteForbiddenFile(ForbiddenFileDto dto)
|
||||
_dbContext.Remove(existingFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeAdminGetBannedUsers)]
|
||||
public async Task<List<BannedUserDto>> GetBannedUsers()
|
||||
{
|
||||
if (!IsModerator) return null;
|
||||
|
||||
return await _dbContext.BannedUsers.AsNoTracking().Select(b => new BannedUserDto()
|
||||
{
|
||||
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
|
||||
CharacterHash = b.CharacterIdentification,
|
||||
Reason = b.Reason
|
||||
}).ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var existingFile =
|
||||
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
|
||||
if (existingFile == null)
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeAdminGetForbiddenFiles)]
|
||||
public async Task<List<ForbiddenFileDto>> GetForbiddenFiles()
|
||||
{
|
||||
if (!IsModerator) return null;
|
||||
|
||||
return await _dbContext.ForbiddenUploadEntries.AsNoTracking().Select(b => new ForbiddenFileDto()
|
||||
{
|
||||
Hash = b.Hash,
|
||||
ForbiddenBy = b.ForbiddenBy
|
||||
}).ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeAdminGetOnlineUsers)]
|
||||
public async Task<List<OnlineUserDto>> AdminGetOnlineUsers()
|
||||
{
|
||||
if (!IsModerator) return null;
|
||||
|
||||
var users = await _dbContext.Users.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
return users.Where(c => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(c.UID).Result)).Select(async b => new OnlineUserDto
|
||||
{
|
||||
CharacterNameHash = await _clientIdentService.GetCharacterIdentForUid(b.UID).ConfigureAwait(false),
|
||||
UID = b.UID,
|
||||
IsModerator = b.IsModerator,
|
||||
IsAdmin = b.IsAdmin
|
||||
}).Select(c => c.Result).ToList();
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminUpdateOrAddBannedUser)]
|
||||
public async Task UpdateOrAddBannedUser(BannedUserDto dto)
|
||||
{
|
||||
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
|
||||
|
||||
var existingUser =
|
||||
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
|
||||
if (existingUser != null)
|
||||
{
|
||||
existingUser.Reason = dto.Reason;
|
||||
_dbContext.Update(existingUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _dbContext.BannedUsers.AddAsync(new Banned
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dbContext.Remove(existingFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminDeleteForbiddenFile, dto).ConfigureAwait(false);
|
||||
CharacterIdentification = dto.CharacterHash,
|
||||
Reason = dto.Reason
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeAdminGetBannedUsers)]
|
||||
public async Task<List<BannedUserDto>> GetBannedUsers()
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false);
|
||||
var bannedUser = await _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(bannedUser))
|
||||
{
|
||||
if (!IsModerator) return null;
|
||||
|
||||
return await _dbContext.BannedUsers.AsNoTracking().Select(b => new BannedUserDto()
|
||||
{
|
||||
CharacterHash = b.CharacterIdentification,
|
||||
Reason = b.Reason
|
||||
}).ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeAdminGetForbiddenFiles)]
|
||||
public async Task<List<ForbiddenFileDto>> GetForbiddenFiles()
|
||||
{
|
||||
if (!IsModerator) return null;
|
||||
|
||||
return await _dbContext.ForbiddenUploadEntries.AsNoTracking().Select(b => new ForbiddenFileDto()
|
||||
{
|
||||
Hash = b.Hash,
|
||||
ForbiddenBy = b.ForbiddenBy
|
||||
}).ToListAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeAdminGetOnlineUsers)]
|
||||
public async Task<List<OnlineUserDto>> AdminGetOnlineUsers()
|
||||
{
|
||||
if (!IsModerator) return null;
|
||||
|
||||
var users = await _dbContext.Users.AsNoTracking().ToListAsync().ConfigureAwait(false);
|
||||
return users.Where(c => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(c.UID))).Select(b => new OnlineUserDto
|
||||
{
|
||||
CharacterNameHash = _clientIdentService.GetCharacterIdentForUid(b.UID),
|
||||
UID = b.UID,
|
||||
IsModerator = b.IsModerator,
|
||||
IsAdmin = b.IsAdmin
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminUpdateOrAddBannedUser)]
|
||||
public async Task UpdateOrAddBannedUser(BannedUserDto dto)
|
||||
{
|
||||
if (!IsModerator || string.IsNullOrEmpty(dto.CharacterHash)) return;
|
||||
|
||||
var existingUser =
|
||||
await _dbContext.BannedUsers.SingleOrDefaultAsync(b => b.CharacterIdentification == dto.CharacterHash).ConfigureAwait(false);
|
||||
if (existingUser != null)
|
||||
{
|
||||
existingUser.Reason = dto.Reason;
|
||||
_dbContext.Update(existingUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _dbContext.BannedUsers.AddAsync(new Banned
|
||||
{
|
||||
CharacterIdentification = dto.CharacterHash,
|
||||
Reason = dto.Reason
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddBannedUser, dto).ConfigureAwait(false);
|
||||
var bannedUser = _clientIdentService.GetUidForCharacterIdent(dto.CharacterHash);
|
||||
if (!string.IsNullOrEmpty(bannedUser))
|
||||
{
|
||||
await Clients.User(bannedUser).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)]
|
||||
public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto)
|
||||
{
|
||||
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
|
||||
|
||||
var existingForbiddenFile =
|
||||
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
|
||||
if (existingForbiddenFile != null)
|
||||
{
|
||||
existingForbiddenFile.ForbiddenBy = dto.ForbiddenBy;
|
||||
_dbContext.Update(existingForbiddenFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _dbContext.ForbiddenUploadEntries.AddAsync(new ForbiddenUploadEntry
|
||||
{
|
||||
Hash = dto.Hash,
|
||||
ForbiddenBy = dto.ForbiddenBy
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false);
|
||||
await Clients.User(bannedUser).SendAsync(Api.OnAdminForcedReconnect).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendAdminUpdateOrAddForbiddenFile)]
|
||||
public async Task UpdateOrAddForbiddenFile(ForbiddenFileDto dto)
|
||||
{
|
||||
if (!IsAdmin || string.IsNullOrEmpty(dto.Hash)) return;
|
||||
|
||||
var existingForbiddenFile =
|
||||
await _dbContext.ForbiddenUploadEntries.SingleOrDefaultAsync(b => b.Hash == dto.Hash).ConfigureAwait(false);
|
||||
if (existingForbiddenFile != null)
|
||||
{
|
||||
existingForbiddenFile.ForbiddenBy = dto.ForbiddenBy;
|
||||
_dbContext.Update(existingForbiddenFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _dbContext.ForbiddenUploadEntries.AddAsync(new ForbiddenUploadEntry
|
||||
{
|
||||
Hash = dto.Hash,
|
||||
ForbiddenBy = dto.ForbiddenBy
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(OnlineAdmins).SendAsync(Api.OnAdminUpdateOrAddForbiddenFile, dto).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,228 +16,233 @@ using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosServer.Hubs
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
public partial class MareHub
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendFileAbortUpload)]
|
||||
public async Task AbortUpload()
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendFileAbortUpload)]
|
||||
public async Task AbortUpload()
|
||||
{
|
||||
_logger.LogInformation("User {AuthenticatedUserId} aborted upload", AuthenticatedUserId);
|
||||
var userId = AuthenticatedUserId;
|
||||
var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList();
|
||||
_dbContext.RemoveRange(notUploadedFiles);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
_logger.LogCallInfo(Api.SendFileAbortUpload);
|
||||
var userId = AuthenticatedUserId;
|
||||
var notUploadedFiles = _dbContext.Files.Where(f => !f.Uploaded && f.Uploader.UID == userId).ToList();
|
||||
_dbContext.RemoveRange(notUploadedFiles);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendFileDeleteAllFiles)]
|
||||
public async Task DeleteAllFiles()
|
||||
{
|
||||
_logger.LogInformation("User {AuthenticatedUserId} deleted all their files", AuthenticatedUserId);
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendFileDeleteAllFiles)]
|
||||
public async Task DeleteAllFiles()
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendFileDeleteAllFiles);
|
||||
|
||||
var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
|
||||
var request = new DeleteFilesRequest();
|
||||
request.Hash.AddRange(ownFiles.Select(f => f.Hash));
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
|
||||
};
|
||||
_ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeGetFilesSizes)]
|
||||
public async Task<List<DownloadFileDto>> GetFilesSizes(List<string> hashes)
|
||||
{
|
||||
var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
|
||||
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.
|
||||
Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
|
||||
List<DownloadFileDto> response = new();
|
||||
|
||||
FileSizeRequest request = new FileSizeRequest();
|
||||
request.Hash.AddRange(hashes);
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
|
||||
};
|
||||
var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request, headers).ConfigureAwait(false);
|
||||
|
||||
foreach (var hash in grpcResponse.HashToFileSize)
|
||||
var ownFiles = await _dbContext.Files.Where(f => f.Uploaded && f.Uploader.UID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
|
||||
var request = new DeleteFilesRequest();
|
||||
request.Hash.AddRange(ownFiles.Select(f => f.Hash));
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
var forbiddenFile = forbiddenFiles.SingleOrDefault(f => f.Hash == hash.Key);
|
||||
var downloadFile = allFiles.SingleOrDefault(f => f.Hash == hash.Key);
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
|
||||
};
|
||||
_ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
response.Add(new DownloadFileDto
|
||||
{
|
||||
FileExists = hash.Value > 0,
|
||||
ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty,
|
||||
IsForbidden = forbiddenFile != null,
|
||||
Hash = hash.Key,
|
||||
Size = hash.Value,
|
||||
Url = new Uri(_cdnFullUri, hash.Key.ToUpperInvariant()).ToString()
|
||||
});
|
||||
}
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeGetFilesSizes)]
|
||||
public async Task<List<DownloadFileDto>> GetFilesSizes(List<string> hashes)
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeGetFilesSizes, hashes.Count.ToString());
|
||||
|
||||
return response;
|
||||
}
|
||||
var allFiles = await _dbContext.Files.Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
|
||||
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.
|
||||
Where(f => hashes.Contains(f.Hash)).ToListAsync().ConfigureAwait(false);
|
||||
List<DownloadFileDto> response = new();
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeFileIsUploadFinished)]
|
||||
public async Task<bool> IsUploadFinished()
|
||||
FileSizeRequest request = new FileSizeRequest();
|
||||
request.Hash.AddRange(hashes);
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
var userUid = AuthenticatedUserId;
|
||||
return await _dbContext.Files.AsNoTracking()
|
||||
.AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false);
|
||||
}
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
|
||||
};
|
||||
var grpcResponse = await _fileServiceClient.GetFileSizesAsync(request, headers).ConfigureAwait(false);
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeFileSendFiles)]
|
||||
public async Task<List<UploadFileDto>> SendFiles(List<string> fileListHashes)
|
||||
foreach (var hash in grpcResponse.HashToFileSize)
|
||||
{
|
||||
var userSentHashes = new HashSet<string>(fileListHashes.Distinct());
|
||||
_logger.LogInformation("User {AuthenticatedUserId} sending files: {count}", AuthenticatedUserId, userSentHashes.Count);
|
||||
var notCoveredFiles = new Dictionary<string, UploadFileDto>();
|
||||
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
|
||||
var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
|
||||
var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
var forbiddenFile = forbiddenFiles.SingleOrDefault(f => string.Equals(f.Hash, hash.Key, StringComparison.Ordinal));
|
||||
var downloadFile = allFiles.SingleOrDefault(f => string.Equals(f.Hash, hash.Key, StringComparison.Ordinal));
|
||||
|
||||
List<FileCache> fileCachesToUpload = new();
|
||||
foreach (var file in userSentHashes)
|
||||
response.Add(new DownloadFileDto
|
||||
{
|
||||
// Skip empty file hashes, duplicate file hashes, forbidden file hashes and existing file hashes
|
||||
if (string.IsNullOrEmpty(file)) { continue; }
|
||||
if (notCoveredFiles.ContainsKey(file)) { continue; }
|
||||
if (forbiddenFiles.ContainsKey(file))
|
||||
{
|
||||
notCoveredFiles[file] = new UploadFileDto()
|
||||
{
|
||||
ForbiddenBy = forbiddenFiles[file].ForbiddenBy,
|
||||
Hash = file,
|
||||
IsForbidden = true
|
||||
};
|
||||
FileExists = hash.Value > 0,
|
||||
ForbiddenBy = forbiddenFile?.ForbiddenBy ?? string.Empty,
|
||||
IsForbidden = forbiddenFile != null,
|
||||
Hash = hash.Key,
|
||||
Size = hash.Value,
|
||||
Url = new Uri(_cdnFullUri, hash.Key.ToUpperInvariant()).ToString()
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
if (existingFiles.ContainsKey(file)) { continue; }
|
||||
return response;
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {AuthenticatedUserId} needs upload: {file}", AuthenticatedUserId, file);
|
||||
var userId = AuthenticatedUserId;
|
||||
fileCachesToUpload.Add(new FileCache()
|
||||
{
|
||||
Hash = file,
|
||||
Uploaded = false,
|
||||
Uploader = uploader
|
||||
});
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeFileIsUploadFinished)]
|
||||
public async Task<bool> IsUploadFinished()
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeFileIsUploadFinished);
|
||||
var userUid = AuthenticatedUserId;
|
||||
return await _dbContext.Files.AsNoTracking()
|
||||
.AnyAsync(f => f.Uploader.UID == userUid && !f.Uploaded).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeFileSendFiles)]
|
||||
public async Task<List<UploadFileDto>> SendFiles(List<string> fileListHashes)
|
||||
{
|
||||
var userSentHashes = new HashSet<string>(fileListHashes.Distinct(StringComparer.Ordinal), StringComparer.Ordinal);
|
||||
_logger.LogCallInfo(Api.InvokeFileSendFiles, userSentHashes.Count.ToString());
|
||||
var notCoveredFiles = new Dictionary<string, UploadFileDto>(StringComparer.Ordinal);
|
||||
var forbiddenFiles = await _dbContext.ForbiddenUploadEntries.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
|
||||
var existingFiles = await _dbContext.Files.AsNoTracking().Where(f => userSentHashes.Contains(f.Hash)).ToDictionaryAsync(f => f.Hash, f => f).ConfigureAwait(false);
|
||||
var uploader = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
|
||||
List<FileCache> fileCachesToUpload = new();
|
||||
foreach (var file in userSentHashes)
|
||||
{
|
||||
// Skip empty file hashes, duplicate file hashes, forbidden file hashes and existing file hashes
|
||||
if (string.IsNullOrEmpty(file)) { continue; }
|
||||
if (notCoveredFiles.ContainsKey(file)) { continue; }
|
||||
if (forbiddenFiles.ContainsKey(file))
|
||||
{
|
||||
notCoveredFiles[file] = new UploadFileDto()
|
||||
{
|
||||
ForbiddenBy = forbiddenFiles[file].ForbiddenBy,
|
||||
Hash = file,
|
||||
IsForbidden = true
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
//Save bulk
|
||||
await _dbContext.Files.AddRangeAsync(fileCachesToUpload).ConfigureAwait(false);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
return notCoveredFiles.Values.ToList();
|
||||
if (existingFiles.ContainsKey(file)) { continue; }
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeFileSendFiles, file, "Missing");
|
||||
|
||||
var userId = AuthenticatedUserId;
|
||||
fileCachesToUpload.Add(new FileCache()
|
||||
{
|
||||
Hash = file,
|
||||
Uploaded = false,
|
||||
Uploader = uploader
|
||||
});
|
||||
|
||||
notCoveredFiles[file] = new UploadFileDto()
|
||||
{
|
||||
Hash = file,
|
||||
};
|
||||
}
|
||||
//Save bulk
|
||||
await _dbContext.Files.AddRangeAsync(fileCachesToUpload).ConfigureAwait(false);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
return notCoveredFiles.Values.ToList();
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendFileUploadFileStreamAsync)]
|
||||
public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable<byte[]> fileContent)
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendFileUploadFileStreamAsync)]
|
||||
public async Task UploadFileStreamAsync(string hash, IAsyncEnumerable<byte[]> fileContent)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash);
|
||||
|
||||
var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && !f.Uploaded);
|
||||
if (relatedFile == null) return;
|
||||
var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash);
|
||||
if (forbiddenFile != null) return;
|
||||
|
||||
var tempFileName = Path.GetTempFileName();
|
||||
using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate);
|
||||
long length = 0;
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("User {AuthenticatedUserId} uploading file: {hash}", AuthenticatedUserId, hash);
|
||||
await foreach (var chunk in fileContent.ConfigureAwait(false))
|
||||
{
|
||||
length += chunk.Length;
|
||||
await fileStream.WriteAsync(chunk).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var relatedFile = _dbContext.Files.SingleOrDefault(f => f.Hash == hash && f.Uploader.UID == AuthenticatedUserId && f.Uploaded == false);
|
||||
if (relatedFile == null) return;
|
||||
var forbiddenFile = _dbContext.ForbiddenUploadEntries.SingleOrDefault(f => f.Hash == hash);
|
||||
if (forbiddenFile != null) return;
|
||||
|
||||
var tempFileName = Path.GetTempFileName();
|
||||
using var fileStream = new FileStream(tempFileName, FileMode.OpenOrCreate);
|
||||
long length = 0;
|
||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||
await fileStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var chunk in fileContent.ConfigureAwait(false))
|
||||
{
|
||||
length += chunk.Length;
|
||||
await fileStream.WriteAsync(chunk).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||
await fileStream.DisposeAsync().ConfigureAwait(false);
|
||||
_dbContext.Files.Remove(relatedFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||
await fileStream.DisposeAsync().ConfigureAwait(false);
|
||||
_dbContext.Files.Remove(relatedFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// already removed
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFileName);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {AuthenticatedUserId} upload finished: {hash}, size: {length}", AuthenticatedUserId, hash, length);
|
||||
|
||||
try
|
||||
{
|
||||
var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false));
|
||||
using var sha1 = SHA1.Create();
|
||||
using var ms = new MemoryStream(decodedFile);
|
||||
var computedHash = await sha1.ComputeHashAsync(ms).ConfigureAwait(false);
|
||||
var computedHashString = BitConverter.ToString(computedHash).Replace("-", "");
|
||||
if (hash != computedHashString)
|
||||
{
|
||||
_logger.LogWarning("Computed file hash was not expected file hash. Computed: {computedHashString}, Expected {hash}", computedHashString, hash);
|
||||
_dbContext.Remove(relatedFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.Authentication)?.Value }
|
||||
};
|
||||
var streamingCall = _fileServiceClient.UploadFile(headers);
|
||||
using var tempFileStream = new FileStream(tempFileName, FileMode.Open, FileAccess.Read);
|
||||
int size = 1024 * 1024;
|
||||
byte[] data = new byte[size];
|
||||
int readBytes;
|
||||
while ((readBytes = tempFileStream.Read(data, 0, size)) > 0)
|
||||
{
|
||||
await streamingCall.RequestStream.WriteAsync(new UploadFileRequest()
|
||||
{
|
||||
FileData = ByteString.CopyFrom(data, 0, readBytes),
|
||||
Hash = computedHashString,
|
||||
Uploader = AuthenticatedUserId
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
await streamingCall.RequestStream.CompleteAsync();
|
||||
tempFileStream.Close();
|
||||
await tempFileStream.DisposeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Upload failed");
|
||||
_dbContext.Remove(relatedFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
// already removed
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFileName);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Uploaded");
|
||||
|
||||
try
|
||||
{
|
||||
var decodedFile = LZ4.LZ4Codec.Unwrap(await File.ReadAllBytesAsync(tempFileName).ConfigureAwait(false));
|
||||
using var sha1 = SHA1.Create();
|
||||
using var ms = new MemoryStream(decodedFile);
|
||||
var computedHash = await sha1.ComputeHashAsync(ms).ConfigureAwait(false);
|
||||
var computedHashString = BitConverter.ToString(computedHash).Replace("-", "", StringComparison.Ordinal);
|
||||
if (!string.Equals(hash, computedHashString, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, hash, "Invalid", computedHashString);
|
||||
_dbContext.Remove(relatedFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
|
||||
};
|
||||
var streamingCall = _fileServiceClient.UploadFile(headers);
|
||||
using var tempFileStream = new FileStream(tempFileName, FileMode.Open, FileAccess.Read);
|
||||
int size = 1024 * 1024;
|
||||
byte[] data = new byte[size];
|
||||
int readBytes;
|
||||
while ((readBytes = tempFileStream.Read(data, 0, size)) > 0)
|
||||
{
|
||||
await streamingCall.RequestStream.WriteAsync(new UploadFileRequest()
|
||||
{
|
||||
FileData = ByteString.CopyFrom(data, 0, readBytes),
|
||||
Hash = computedHashString,
|
||||
Uploader = AuthenticatedUserId
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
await streamingCall.RequestStream.CompleteAsync().ConfigureAwait(false);
|
||||
tempFileStream.Close();
|
||||
await tempFileStream.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendFileUploadFileStreamAsync, hash, "Pushed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCallWarning(Api.SendFileUploadFileStreamAsync, "Failed", hash, ex.Message);
|
||||
_dbContext.Remove(relatedFile);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using MareSynchronosShared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Globalization;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosServer.Utils;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
private async Task<List<PausedEntry>> GetAllPairedClientsWithPauseState(string? uid = null)
|
||||
{
|
||||
uid ??= AuthenticatedUserId;
|
||||
|
||||
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",
|
||||
PauseState = (userPair.IsPaused || 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),
|
||||
PauseState = (userGroupPair.IsPaused || otherGroupPair.IsPaused)
|
||||
})
|
||||
).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
return query.GroupBy(g => g.UID, g => (g.GID, g.PauseState),
|
||||
(key, g) => new PausedEntry
|
||||
{
|
||||
UID = key,
|
||||
PauseStates = g.Select(p => new PauseState() { GID = string.Equals(p.GID, "DIRECT", StringComparison.Ordinal) ? null : p.GID, IsPaused = p.PauseState })
|
||||
.ToList()
|
||||
}, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetAllPairedUnpausedUsers(string? uid = null)
|
||||
{
|
||||
uid ??= AuthenticatedUserId;
|
||||
var ret = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false);
|
||||
return ret.Where(k => !k.IsPaused).Select(k => k.UID).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<string>> SendDataToAllPairedUsers(string apiMethod, object arg)
|
||||
{
|
||||
var usersToSendDataTo = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
await Clients.Users(usersToSendDataTo).SendAsync(apiMethod, arg).ConfigureAwait(false);
|
||||
|
||||
return usersToSendDataTo;
|
||||
}
|
||||
|
||||
public string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value ?? "Unknown";
|
||||
|
||||
protected async Task<User> GetAuthenticatedUserUntrackedAsync()
|
||||
{
|
||||
return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task UserGroupLeave(GroupPair groupUserPair, List<PausedEntry> allUserPairs, string userIdent, string? uid = null)
|
||||
{
|
||||
uid ??= AuthenticatedUserId;
|
||||
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 _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
await Clients.User(uid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendGroupDeletedToAll(List<GroupPair> groupUsers)
|
||||
{
|
||||
foreach (var pair in groupUsers)
|
||||
{
|
||||
var pairIdent = await _clientIdentService.GetCharacterIdentForUid(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
548
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs
Normal file
548
MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Groups.cs
Normal file
@@ -0,0 +1,548 @@
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Authentication;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Models;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeGroupCreate)]
|
||||
public async Task<GroupCreatedDto> CreateGroup()
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeGroupCreate);
|
||||
var existingGroupsByUser = _dbContext.Groups.Count(u => u.OwnerUID == AuthenticatedUserId);
|
||||
var existingJoinedGroups = _dbContext.GroupPairs.Count(u => u.GroupUserUID == AuthenticatedUserId);
|
||||
if (existingGroupsByUser >= _maxExistingGroupsByUser || existingJoinedGroups >= _maxJoinedGroupsByUser)
|
||||
{
|
||||
throw new System.Exception($"Max groups for user is {_maxExistingGroupsByUser}, max joined groups is {_maxJoinedGroupsByUser}.");
|
||||
}
|
||||
|
||||
var gid = StringUtils.GenerateRandomString(12);
|
||||
while (await _dbContext.Groups.AnyAsync(g => g.GID == "MSS-" + gid).ConfigureAwait(false))
|
||||
{
|
||||
gid = StringUtils.GenerateRandomString(12);
|
||||
}
|
||||
gid = "MSS-" + 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 = AuthenticatedUserId
|
||||
};
|
||||
|
||||
GroupPair initialPair = new()
|
||||
{
|
||||
GroupGID = newGroup.GID,
|
||||
GroupUserUID = AuthenticatedUserId,
|
||||
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 = _dbContext.Users.Single(u => u.UID == AuthenticatedUserId);
|
||||
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = newGroup.GID,
|
||||
OwnedBy = string.IsNullOrEmpty(self.Alias) ? self.UID : self.Alias,
|
||||
IsDeleted = false,
|
||||
IsPaused = false,
|
||||
InvitesEnabled = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeGroupCreate, gid);
|
||||
|
||||
return new GroupCreatedDto()
|
||||
{
|
||||
GID = newGroup.GID,
|
||||
Password = passwd
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeGroupGetGroups)]
|
||||
public async Task<List<GroupDto>> GetGroups()
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeGroupGetGroups);
|
||||
|
||||
var groups = await _dbContext.GroupPairs.Include(g => g.Group).Include(g => g.Group.Owner).Where(g => g.GroupUserUID == AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
return groups.Select(g => new GroupDto()
|
||||
{
|
||||
GID = g.GroupGID,
|
||||
Alias = g.Group.Alias,
|
||||
InvitesEnabled = g.Group.InvitesEnabled,
|
||||
OwnedBy = string.IsNullOrEmpty(g.Group.Owner.Alias) ? g.Group.Owner.UID : g.Group.Owner.Alias,
|
||||
IsPaused = g.IsPaused
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeGroupGetUsersInGroup)]
|
||||
public async Task<List<GroupPairDto>> GetUsersInGroup(string gid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeGroupGetUsersInGroup, gid);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
var existingPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
if (group == null || existingPair == null) return new List<GroupPairDto>();
|
||||
|
||||
var allPairs = await _dbContext.GroupPairs.Include(g => g.GroupUser).Where(g => g.GroupGID == gid && g.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
|
||||
return allPairs.Select(p => new GroupPairDto()
|
||||
{
|
||||
GroupGID = gid,
|
||||
IsPaused = p.IsPaused,
|
||||
IsRemoved = false,
|
||||
UserUID = p.GroupUser.UID,
|
||||
UserAlias = p.GroupUser.Alias,
|
||||
IsPinned = p.IsPinned
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupChangeInviteState)]
|
||||
public async Task GroupChangeInviteState(string gid, bool enabled)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupChangeInviteState, gid, enabled.ToString());
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
|
||||
group.InvitesEnabled = enabled;
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendGroupChangeInviteState, gid, enabled.ToString(), "Success");
|
||||
|
||||
var groupPairs = _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToList();
|
||||
await Clients.Users(groupPairs).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = gid,
|
||||
InvitesEnabled = enabled,
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupDelete)]
|
||||
public async Task GroupDelete(string gid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupDelete, gid);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
|
||||
_logger.LogCallInfo(Api.SendGroupDelete, gid, "Success");
|
||||
|
||||
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).ToListAsync().ConfigureAwait(false);
|
||||
_dbContext.RemoveRange(groupPairs);
|
||||
_dbContext.Remove(group);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = group.GID,
|
||||
IsDeleted = true,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
|
||||
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeGroupJoin)]
|
||||
public async Task<bool> GroupJoin(string gid, string password)
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeGroupJoin, gid);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid || g.Alias == gid).ConfigureAwait(false);
|
||||
var existingPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
var hashedPw = StringUtils.Sha256String(password);
|
||||
var existingUserCount = await _dbContext.GroupPairs.CountAsync(g => g.GroupGID == gid).ConfigureAwait(false);
|
||||
var joinedGroups = await _dbContext.GroupPairs.CountAsync(g => g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
if (group == null
|
||||
|| !string.Equals(group.HashedPassword, hashedPw, StringComparison.Ordinal)
|
||||
|| existingPair != null
|
||||
|| existingUserCount >= _maxGroupUserCount
|
||||
|| !group.InvitesEnabled
|
||||
|| joinedGroups >= _maxJoinedGroupsByUser)
|
||||
return false;
|
||||
|
||||
GroupPair newPair = new()
|
||||
{
|
||||
GroupGID = group.GID,
|
||||
GroupUserUID = AuthenticatedUserId
|
||||
};
|
||||
|
||||
await _dbContext.GroupPairs.AddAsync(newPair).ConfigureAwait(false);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeGroupJoin, gid, "Success");
|
||||
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = group.GID,
|
||||
OwnedBy = group.OwnerUID,
|
||||
IsDeleted = false,
|
||||
IsPaused = false,
|
||||
Alias = group.Alias,
|
||||
InvitesEnabled = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var self = _dbContext.Users.Single(u => u.UID == AuthenticatedUserId);
|
||||
|
||||
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID && p.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
|
||||
{
|
||||
GroupGID = group.GID,
|
||||
IsPaused = false,
|
||||
IsRemoved = false,
|
||||
UserUID = AuthenticatedUserId,
|
||||
UserAlias = self.Alias,
|
||||
IsPinned = false
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
|
||||
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).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(gid) is PauseInfo.Unpaused) continue;
|
||||
if (userPair.IsPausedPerGroup is PauseInfo.Paused) continue;
|
||||
|
||||
var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID).SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupLeave)]
|
||||
public async Task GroupLeave(string gid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupLeave, gid);
|
||||
|
||||
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
if (groupPair == null) return;
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == 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, AuthenticatedUserId, StringComparison.Ordinal)).ToList();
|
||||
|
||||
_dbContext.GroupPairs.Remove(groupPair);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = group.GID,
|
||||
IsDeleted = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
bool ownerHasLeft = string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal);
|
||||
if (ownerHasLeft)
|
||||
{
|
||||
if (!groupPairsWithoutSelf.Any())
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Deleted");
|
||||
|
||||
_dbContext.Remove(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
var groupHasMigrated = await SharedDbFunctions.MigrateOrDeleteGroup(_dbContext, group, groupPairsWithoutSelf, _maxExistingGroupsByUser).ConfigureAwait(false);
|
||||
|
||||
if (groupHasMigrated.Item1)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Migrated", groupHasMigrated.Item2);
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = group.GID,
|
||||
OwnedBy = groupHasMigrated.Item2,
|
||||
Alias = null
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Deleted");
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = group.GID,
|
||||
IsDeleted = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await SendGroupDeletedToAll(groupPairs).ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendGroupLeave, gid, "Success");
|
||||
|
||||
await Clients.Users(groupPairsWithoutSelf.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
|
||||
{
|
||||
GroupGID = group.GID,
|
||||
IsRemoved = true,
|
||||
UserUID = AuthenticatedUserId,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
|
||||
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
foreach (var groupUserPair in groupPairsWithoutSelf)
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, allUserPairs, userIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupPause)]
|
||||
public async Task GroupChangePauseState(string gid, bool isPaused)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupPause, gid, isPaused);
|
||||
|
||||
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
if (groupPair == null) return;
|
||||
|
||||
groupPair.IsPaused = isPaused;
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendGroupPause, gid, isPaused, "Success");
|
||||
|
||||
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid && p.GroupUserUID != AuthenticatedUserId).ToListAsync().ConfigureAwait(false);
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
|
||||
{
|
||||
GroupGID = gid,
|
||||
IsPaused = isPaused,
|
||||
UserUID = AuthenticatedUserId,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnGroupChange, new GroupDto
|
||||
{
|
||||
GID = gid,
|
||||
IsPaused = isPaused
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
|
||||
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
foreach (var groupUserPair in groupPairs)
|
||||
{
|
||||
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(gid) is PauseInfo.Unpaused) continue;
|
||||
}
|
||||
|
||||
var groupUserIdent = await _clientIdentService.GetCharacterIdentForUid(groupUserPair.GroupUserUID).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(groupUserIdent))
|
||||
{
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(isPaused ? Api.OnUserRemoveOnlinePairedPlayer : Api.OnUserAddOnlinePairedPlayer, groupUserIdent).ConfigureAwait(false);
|
||||
await Clients.User(groupUserPair.GroupUserUID).SendAsync(isPaused ? Api.OnUserRemoveOnlinePairedPlayer : Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupRemoveUser)]
|
||||
public async Task GroupRemoveUser(string gid, string uid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupRemoveUser, gid, uid);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false);
|
||||
if (groupPair == null) return;
|
||||
|
||||
_logger.LogCallInfo(Api.SendGroupRemoveUser, gid, uid, "Success");
|
||||
|
||||
_dbContext.GroupPairs.Remove(groupPair);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
var groupPairs = _dbContext.GroupPairs.Where(p => p.GroupGID == group.GID).ToList();
|
||||
await Clients.Users(groupPairs.Select(p => p.GroupUserUID)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
|
||||
{
|
||||
GroupGID = group.GID,
|
||||
IsRemoved = true,
|
||||
UserUID = uid,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var userIdent = await _clientIdentService.GetCharacterIdentForUid(uid).ConfigureAwait(false);
|
||||
if (userIdent == null) return;
|
||||
|
||||
await Clients.User(uid).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = gid,
|
||||
IsDeleted = true,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState(uid).ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupPairs)
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, allUserPairs, userIdent, uid).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupChangeOwner)]
|
||||
public async Task ChangeOwnership(string gid, string uid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupChangeOwner, gid, uid);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
var groupPair = await _dbContext.GroupPairs.Include(g => g.GroupUser).SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false);
|
||||
if (groupPair == null) return;
|
||||
var ownedShells = await _dbContext.Groups.CountAsync(g => g.OwnerUID == uid).ConfigureAwait(false);
|
||||
if (ownedShells >= _maxExistingGroupsByUser) return;
|
||||
|
||||
var prevOwner = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
prevOwner.IsPinned = false;
|
||||
group.Owner = groupPair.GroupUser;
|
||||
group.Alias = null;
|
||||
groupPair.IsPinned = true;
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendGroupChangeOwner, gid, uid, "Success");
|
||||
|
||||
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = gid,
|
||||
OwnedBy = string.IsNullOrEmpty(group.Owner.Alias) ? group.Owner.UID : group.Owner.Alias,
|
||||
Alias = null
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Where(p => !string.Equals(p, uid, StringComparison.Ordinal))).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
|
||||
{
|
||||
GroupGID = gid,
|
||||
UserUID = uid,
|
||||
IsPinned = true
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeGroupChangePassword)]
|
||||
public async Task<bool> ChangeGroupPassword(string gid, string password)
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeGroupChangePassword, gid);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return false;
|
||||
|
||||
if (password.Length < 10) return false;
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeGroupChangePassword, gid, "Success");
|
||||
|
||||
group.HashedPassword = StringUtils.Sha256String(password);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupChangePinned)]
|
||||
public async Task ChangePinned(string gid, string uid, bool isPinned)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupChangePinned, gid, uid, isPinned);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
var groupPair = await _dbContext.GroupPairs.SingleOrDefaultAsync(g => g.GroupGID == gid && g.GroupUserUID == uid).ConfigureAwait(false);
|
||||
if (groupPair == null) return;
|
||||
|
||||
groupPair.IsPinned = isPinned;
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeGroupChangePassword, gid, uid, isPinned, "Success");
|
||||
|
||||
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).Select(p => p.GroupUserUID).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Where(p => !string.Equals(p, uid, StringComparison.Ordinal))).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
|
||||
{
|
||||
GroupGID = gid,
|
||||
UserUID = uid,
|
||||
IsPinned = isPinned
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendGroupClear)]
|
||||
public async Task ClearGroup(string gid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendGroupClear, gid);
|
||||
|
||||
var group = await _dbContext.Groups.SingleOrDefaultAsync(g => g.GID == gid).ConfigureAwait(false);
|
||||
if (group == null || !string.Equals(group.OwnerUID, AuthenticatedUserId, StringComparison.Ordinal)) return;
|
||||
|
||||
var groupPairs = await _dbContext.GroupPairs.Where(p => p.GroupGID == gid).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
await Clients.Users(groupPairs.Where(p => !p.IsPinned).Select(g => g.GroupUserUID)).SendAsync(Api.OnGroupChange, new GroupDto()
|
||||
{
|
||||
GID = group.GID,
|
||||
IsDeleted = true,
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendGroupClear, gid, "Success");
|
||||
|
||||
var notPinned = groupPairs.Where(g => !g.IsPinned).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)).SendAsync(Api.OnGroupUserChange, new GroupPairDto()
|
||||
{
|
||||
GroupGID = pair.GroupGID,
|
||||
IsRemoved = true,
|
||||
UserUID = pair.GroupUserUID
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var pairIdent = await _clientIdentService.GetCharacterIdentForUid(pair.GroupUserUID).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(pairIdent)) continue;
|
||||
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState(pair.GroupUserUID).ConfigureAwait(false);
|
||||
|
||||
foreach (var groupUserPair in groupPairs.Where(p => !string.Equals(p.GroupUserUID, pair.GroupUserUID, StringComparison.Ordinal)))
|
||||
{
|
||||
await UserGroupLeave(groupUserPair, allUserPairs, pairIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Authentication;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Models;
|
||||
@@ -11,315 +12,340 @@ using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosServer.Hubs
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
public partial class MareHub
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserDeleteAccount)]
|
||||
public async Task DeleteAccount()
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserDeleteAccount)]
|
||||
public async Task DeleteAccount()
|
||||
_logger.LogCallInfo(Api.SendUserDeleteAccount);
|
||||
|
||||
string userid = AuthenticatedUserId;
|
||||
var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false);
|
||||
var charaIdent = await _clientIdentService.GetCharacterIdentForUid(userid).ConfigureAwait(false);
|
||||
var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false);
|
||||
var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false);
|
||||
var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false);
|
||||
var groupPairs = await _dbContext.GroupPairs.Where(g => g.GroupUserUID == userid).ToListAsync().ConfigureAwait(false);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
_logger.LogInformation("User {AuthenticatedUserId} deleted their account", AuthenticatedUserId);
|
||||
|
||||
string userid = AuthenticatedUserId;
|
||||
var userEntry = await _dbContext.Users.SingleAsync(u => u.UID == userid).ConfigureAwait(false);
|
||||
var charaIdent = _clientIdentService.GetCharacterIdentForUid(userid);
|
||||
var ownPairData = await _dbContext.ClientPairs.Where(u => u.User.UID == userid).ToListAsync().ConfigureAwait(false);
|
||||
var auth = await _dbContext.Auth.SingleAsync(u => u.UserUID == userid).ConfigureAwait(false);
|
||||
var lodestone = await _dbContext.LodeStoneAuth.SingleOrDefaultAsync(a => a.User.UID == userid).ConfigureAwait(false);
|
||||
|
||||
if (lodestone != null)
|
||||
{
|
||||
_dbContext.Remove(lodestone);
|
||||
}
|
||||
|
||||
while (_dbContext.Files.Any(f => f.Uploader == userEntry))
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false);
|
||||
|
||||
|
||||
_dbContext.RemoveRange(ownPairData);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
var otherPairData = await _dbContext.ClientPairs.Include(u => u.User)
|
||||
.Where(u => u.OtherUser.UID == userid).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var pair in otherPairData)
|
||||
{
|
||||
await Clients.User(pair.User.UID)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = userid,
|
||||
IsRemoved = true
|
||||
}, charaIdent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugePairs, ownPairData.Count + otherPairData.Count);
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugePairsPaused, ownPairData.Count(c => c.IsPaused));
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1);
|
||||
|
||||
_dbContext.RemoveRange(otherPairData);
|
||||
_dbContext.Remove(userEntry);
|
||||
_dbContext.Remove(auth);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
_dbContext.Remove(lodestone);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeUserGetOnlineCharacters)]
|
||||
public async Task<List<string>> GetOnlineCharacters()
|
||||
while (_dbContext.Files.Any(f => f.Uploader == userEntry))
|
||||
{
|
||||
_logger.LogInformation("User {AuthenticatedUserId} requested online characters", AuthenticatedUserId);
|
||||
|
||||
var ownUser = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false);
|
||||
|
||||
var otherUsers = await _dbContext.ClientPairs.AsNoTracking()
|
||||
.Include(u => u.User)
|
||||
.Include(u => u.OtherUser)
|
||||
.Where(w => w.User.UID == ownUser.UID && !w.IsPaused)
|
||||
//.Where(w => !string.IsNullOrEmpty(w.OtherUser.CharacterIdentification))
|
||||
.Select(e => e.OtherUser).ToListAsync().ConfigureAwait(false);
|
||||
var otherOnlineUsers =
|
||||
otherUsers.Where(u => !string.IsNullOrEmpty(_clientIdentService.GetCharacterIdentForUid(u.UID)));
|
||||
var otherEntries = await _dbContext.ClientPairs.AsNoTracking()
|
||||
.Include(u => u.User)
|
||||
.Where(u => otherOnlineUsers.Any(e => e == u.User) && u.OtherUser == ownUser && !u.IsPaused)
|
||||
.ToListAsync().ConfigureAwait(false);
|
||||
var ownIdent = _clientIdentService.GetCharacterIdentForUid(ownUser.UID);
|
||||
|
||||
await Clients.Users(otherEntries.Select(e => e.User.UID)).SendAsync(Api.OnUserAddOnlinePairedPlayer, ownIdent).ConfigureAwait(false);
|
||||
return otherEntries.Select(e => _clientIdentService.GetCharacterIdentForUid(e.User.UID)).Distinct().ToList();
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeUserGetPairedClients)]
|
||||
public async Task<List<ClientPairDto>> GetPairedClients()
|
||||
{
|
||||
string userid = AuthenticatedUserId;
|
||||
var query =
|
||||
from userToOther in _dbContext.ClientPairs
|
||||
join otherToUser in _dbContext.ClientPairs
|
||||
on new
|
||||
{
|
||||
user = userToOther.UserUID,
|
||||
other = userToOther.OtherUserUID
|
||||
await _authServiceClient.RemoveAuthAsync(new RemoveAuthRequest() { Uid = userid }).ConfigureAwait(false);
|
||||
|
||||
} equals new
|
||||
{
|
||||
user = otherToUser.OtherUserUID,
|
||||
other = otherToUser.UserUID
|
||||
} into leftJoin
|
||||
from otherEntry in leftJoin.DefaultIfEmpty()
|
||||
where
|
||||
userToOther.UserUID == userid
|
||||
select new
|
||||
_dbContext.RemoveRange(ownPairData);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
var otherPairData = await _dbContext.ClientPairs.Include(u => u.User)
|
||||
.Where(u => u.OtherUser.UID == userid).ToListAsync().ConfigureAwait(false);
|
||||
foreach (var pair in otherPairData)
|
||||
{
|
||||
await Clients.User(pair.User.UID)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
userToOther.OtherUser.Alias,
|
||||
userToOther.IsPaused,
|
||||
OtherIsPaused = otherEntry != null && otherEntry.IsPaused,
|
||||
userToOther.OtherUserUID,
|
||||
IsSynced = otherEntry != null
|
||||
};
|
||||
|
||||
return (await query.ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto()
|
||||
{
|
||||
VanityUID = f.Alias,
|
||||
IsPaused = f.IsPaused,
|
||||
OtherUID = f.OtherUserUID,
|
||||
IsSynced = f.IsSynced,
|
||||
IsPausedFromOthers = f.OtherIsPaused
|
||||
}).ToList();
|
||||
OtherUID = userid,
|
||||
IsRemoved = true
|
||||
}, charaIdent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)]
|
||||
public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List<string> visibleCharacterIds)
|
||||
foreach (var pair in groupPairs)
|
||||
{
|
||||
_logger.LogInformation("User {AuthenticatedUserId} pushing character data to {visibleCharacterIds} visible clients", AuthenticatedUserId, visibleCharacterIds.Count);
|
||||
|
||||
var user = await GetAuthenticatedUserUntrackedAsync().ConfigureAwait(false);
|
||||
|
||||
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
|
||||
}
|
||||
where
|
||||
userToOther.UserUID == user.UID
|
||||
&& !userToOther.IsPaused
|
||||
&& !otherToUser.IsPaused
|
||||
select otherToUser.UserUID;
|
||||
|
||||
var otherEntries = await query.ToListAsync().ConfigureAwait(false);
|
||||
otherEntries =
|
||||
otherEntries.Where(c => visibleCharacterIds.Select(c => c.ToLowerInvariant()).Contains(_clientIdentService.GetCharacterIdentForUid(c)?.ToLowerInvariant() ?? "")).ToList();
|
||||
var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
|
||||
|
||||
await Clients.Users(otherEntries).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false);
|
||||
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushData);
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, otherEntries.Count);
|
||||
await GroupLeave(pair.GroupGID).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserPairedClientAddition)]
|
||||
public async Task SendPairedClientAddition(string uid)
|
||||
{
|
||||
if (uid == AuthenticatedUserId || string.IsNullOrWhiteSpace(uid)) return;
|
||||
uid = uid.Trim();
|
||||
var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUsersRegisteredDeleted, 1);
|
||||
|
||||
var otherUser = await _dbContext.Users
|
||||
.SingleOrDefaultAsync(u => u.UID == uid || u.Alias == uid).ConfigureAwait(false);
|
||||
var existingEntry =
|
||||
await _dbContext.ClientPairs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.User.UID == AuthenticatedUserId && p.OtherUser.UID == otherUser.UID).ConfigureAwait(false);
|
||||
if (otherUser == null || existingEntry != null) return;
|
||||
_logger.LogInformation("User {AuthenticatedUserId} adding {uid} to whitelist", AuthenticatedUserId, uid);
|
||||
ClientPair wl = new ClientPair()
|
||||
_dbContext.RemoveRange(otherPairData);
|
||||
_dbContext.Remove(userEntry);
|
||||
_dbContext.Remove(auth);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeUserGetOnlineCharacters)]
|
||||
public async Task<List<string>> GetOnlineCharacters()
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeUserGetOnlineCharacters);
|
||||
|
||||
var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
|
||||
var usersToSendOnlineTo = await SendDataToAllPairedUsers(Api.OnUserAddOnlinePairedPlayer, ownIdent).ConfigureAwait(false);
|
||||
return usersToSendOnlineTo.Select(async e => await _clientIdentService.GetCharacterIdentForUid(e).ConfigureAwait(false)).Select(t => t.Result).Where(t => !string.IsNullOrEmpty(t)).Distinct(System.StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeUserGetPairedClients)]
|
||||
public async Task<List<ClientPairDto>> GetPairedClients()
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeUserGetPairedClients);
|
||||
|
||||
string userid = AuthenticatedUserId;
|
||||
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 == userid
|
||||
select new
|
||||
{
|
||||
IsPaused = false,
|
||||
OtherUser = otherUser,
|
||||
User = user
|
||||
userToOther.OtherUser.Alias,
|
||||
userToOther.IsPaused,
|
||||
OtherIsPaused = otherEntry != null && otherEntry.IsPaused,
|
||||
userToOther.OtherUserUID,
|
||||
IsSynced = otherEntry != null
|
||||
};
|
||||
await _dbContext.ClientPairs.AddAsync(wl).ConfigureAwait(false);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
var otherEntry = OppositeEntry(otherUser.UID);
|
||||
await Clients.User(user.UID)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
VanityUID = otherUser.Alias,
|
||||
OtherUID = otherUser.UID,
|
||||
IsPaused = false,
|
||||
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
|
||||
IsSynced = otherEntry != null
|
||||
}, string.Empty).ConfigureAwait(false);
|
||||
if (otherEntry != null)
|
||||
{
|
||||
var userIdent = _clientIdentService.GetCharacterIdentForUid(user.UID);
|
||||
await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs,
|
||||
new ClientPairDto()
|
||||
{
|
||||
VanityUID = user.Alias,
|
||||
OtherUID = user.UID,
|
||||
IsPaused = otherEntry.IsPaused,
|
||||
IsPausedFromOthers = false,
|
||||
IsSynced = true
|
||||
}, userIdent).ConfigureAwait(false);
|
||||
|
||||
var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID);
|
||||
if (!string.IsNullOrEmpty(otherIdent))
|
||||
{
|
||||
await Clients.User(user.UID)
|
||||
.SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUser.UID)
|
||||
.SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_mareMetrics.IncGauge(MetricsAPI.GaugePairs);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserPairedClientPauseChange)]
|
||||
public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused)
|
||||
return (await query.ToListAsync().ConfigureAwait(false)).Select(f => new ClientPairDto()
|
||||
{
|
||||
if (otherUserUid == AuthenticatedUserId) return;
|
||||
ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false);
|
||||
if (pair == null) return;
|
||||
VanityUID = f.Alias,
|
||||
IsPaused = f.IsPaused,
|
||||
OtherUID = f.OtherUserUID,
|
||||
IsSynced = f.IsSynced,
|
||||
IsPausedFromOthers = f.OtherIsPaused
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {AuthenticatedUserId} changed pause status with {otherUserUid} to {isPaused}", AuthenticatedUserId, otherUserUid, isPaused);
|
||||
pair.IsPaused = isPaused;
|
||||
_dbContext.Update(pair);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
var selfCharaIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
|
||||
var otherCharaIdent = _clientIdentService.GetCharacterIdentForUid(pair.OtherUserUID);
|
||||
var otherEntry = OppositeEntry(otherUserUid);
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.InvokeUserPushCharacterDataToVisibleClients)]
|
||||
public async Task PushCharacterDataToVisibleClients(CharacterCacheDto characterCache, List<string> visibleCharacterIds)
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count);
|
||||
|
||||
await Clients.User(AuthenticatedUserId)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = otherUserUid,
|
||||
IsPaused = isPaused,
|
||||
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
|
||||
IsSynced = otherEntry != null
|
||||
}, otherCharaIdent).ConfigureAwait(false);
|
||||
if (otherEntry != null)
|
||||
var allPairedUsers = await GetAllPairedUnpausedUsers().ConfigureAwait(false);
|
||||
|
||||
var allPairedUsersDict = allPairedUsers.ToDictionary(f => f, async f => await _clientIdentService.GetCharacterIdentForUid(f).ConfigureAwait(false), System.StringComparer.Ordinal)
|
||||
.Where(f => visibleCharacterIds.Contains(f.Value.Result, System.StringComparer.Ordinal));
|
||||
|
||||
var ownIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeUserPushCharacterDataToVisibleClients, visibleCharacterIds.Count, allPairedUsersDict.Count());
|
||||
|
||||
await Clients.Users(allPairedUsersDict.Select(f => f.Key)).SendAsync(Api.OnUserReceiveCharacterData, characterCache, ownIdent).ConfigureAwait(false);
|
||||
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushData);
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterUserPushDataTo, allPairedUsersDict.Count());
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserPairedClientAddition)]
|
||||
public async Task SendPairedClientAddition(string uid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendUserPairedClientAddition, uid);
|
||||
|
||||
// don't allow adding yourself or nothing
|
||||
uid = uid.Trim();
|
||||
if (string.Equals(uid, AuthenticatedUserId, System.StringComparison.Ordinal) || string.IsNullOrWhiteSpace(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);
|
||||
var existingEntry =
|
||||
await _dbContext.ClientPairs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p =>
|
||||
p.User.UID == AuthenticatedUserId && p.OtherUserUID == uid).ConfigureAwait(false);
|
||||
if (otherUser == null || existingEntry != null) return;
|
||||
|
||||
// grab self create new client pair and save
|
||||
var user = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendUserPairedClientAddition, uid, "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);
|
||||
await Clients.User(user.UID)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = AuthenticatedUserId,
|
||||
IsPaused = otherEntry.IsPaused,
|
||||
IsPausedFromOthers = isPaused,
|
||||
IsSynced = true
|
||||
}, selfCharaIdent).ConfigureAwait(false);
|
||||
}
|
||||
VanityUID = otherUser.Alias,
|
||||
OtherUID = otherUser.UID,
|
||||
IsPaused = false,
|
||||
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
|
||||
IsSynced = otherEntry != null
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
// if there's no opposite entry do nothing
|
||||
if (otherEntry == null) return;
|
||||
|
||||
// check if other user is online
|
||||
var otherIdent = await _clientIdentService.GetCharacterIdentForUid(otherUser.UID).ConfigureAwait(false);
|
||||
if (otherIdent == null) return;
|
||||
|
||||
// send push with update to other user if other user is online
|
||||
await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs,
|
||||
new ClientPairDto()
|
||||
{
|
||||
VanityUID = user.Alias,
|
||||
OtherUID = user.UID,
|
||||
IsPaused = otherEntry.IsPaused,
|
||||
IsPausedFromOthers = false,
|
||||
IsSynced = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
// get own ident and all pairs
|
||||
var userIdent = await _clientIdentService.GetCharacterIdentForUid(user.UID).ConfigureAwait(false);
|
||||
var allUserPairs = await GetAllPairedClientsWithPauseState().ConfigureAwait(false);
|
||||
|
||||
// if the other user has paused the main user and there was no previous group connection don't send anything
|
||||
if (!otherEntry.IsPaused && allUserPairs.Any(p => string.Equals(p.UID, uid, System.StringComparison.Ordinal) && p.IsPausedPerGroup is PauseInfo.Paused or PauseInfo.NoConnection))
|
||||
{
|
||||
await Clients.User(user.UID)
|
||||
.SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUser.UID)
|
||||
.SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserPairedClientPauseChange)]
|
||||
public async Task SendPairedClientPauseChange(string otherUserUid, bool isPaused)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendUserPairedClientPauseChange, otherUserUid, isPaused);
|
||||
|
||||
if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return;
|
||||
ClientPair pair = await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false);
|
||||
if (pair == null) return;
|
||||
|
||||
pair.IsPaused = isPaused;
|
||||
_dbContext.Update(pair);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendUserPairedClientPauseChange, otherUserUid, isPaused, "Success");
|
||||
|
||||
var otherEntry = OppositeEntry(otherUserUid);
|
||||
|
||||
await Clients.User(AuthenticatedUserId)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = otherUserUid,
|
||||
IsPaused = isPaused,
|
||||
IsPausedFromOthers = otherEntry?.IsPaused ?? false,
|
||||
IsSynced = otherEntry != null
|
||||
}).ConfigureAwait(false);
|
||||
if (otherEntry != null)
|
||||
{
|
||||
await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = AuthenticatedUserId,
|
||||
IsPaused = otherEntry.IsPaused,
|
||||
IsPausedFromOthers = isPaused,
|
||||
IsSynced = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var selfCharaIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
var otherCharaIdent = await _clientIdentService.GetCharacterIdentForUid(pair.OtherUserUID).ConfigureAwait(false);
|
||||
|
||||
if (selfCharaIdent == null || otherCharaIdent == null || otherEntry.IsPaused) return;
|
||||
|
||||
if (isPaused)
|
||||
{
|
||||
_mareMetrics.IncGauge(MetricsAPI.GaugePairsPaused);
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherCharaIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUserUid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, selfCharaIdent).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugePairsPaused);
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, otherCharaIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUserUid).SendAsync(Api.OnUserAddOnlinePairedPlayer, selfCharaIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserPairedClientRemoval)]
|
||||
public async Task SendPairedClientRemoval(string uid)
|
||||
{
|
||||
if (uid == AuthenticatedUserId) return;
|
||||
|
||||
var sender = await _dbContext.Users.SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
var otherUser = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == uid).ConfigureAwait(false);
|
||||
if (otherUser == null) return;
|
||||
_logger.LogInformation("User {AuthenticatedUserId} removed {uid} from whitelist", AuthenticatedUserId, uid);
|
||||
ClientPair wl =
|
||||
await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.User == sender && w.OtherUser == otherUser).ConfigureAwait(false);
|
||||
if (wl == null) return;
|
||||
_dbContext.ClientPairs.Remove(wl);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
var otherEntry = OppositeEntry(uid);
|
||||
var otherIdent = _clientIdentService.GetCharacterIdentForUid(otherUser.UID);
|
||||
await Clients.User(sender.UID)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = otherUser.UID,
|
||||
IsRemoved = true
|
||||
}, otherIdent).ConfigureAwait(false);
|
||||
if (otherEntry != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(otherIdent))
|
||||
{
|
||||
var ownIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
|
||||
await Clients.User(sender.UID)
|
||||
.SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUser.UID)
|
||||
.SendAsync(Api.OnUserRemoveOnlinePairedPlayer, ownIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUser.UID).SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = sender.UID,
|
||||
IsPaused = otherEntry.IsPaused,
|
||||
IsPausedFromOthers = false,
|
||||
IsSynced = false
|
||||
}, ownIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugePairs);
|
||||
}
|
||||
|
||||
private ClientPair OppositeEntry(string otherUID) =>
|
||||
_dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == AuthenticatedUserId);
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
[HubMethodName(Api.SendUserPairedClientRemoval)]
|
||||
public async Task SendPairedClientRemoval(string otherUserUid)
|
||||
{
|
||||
_logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid);
|
||||
|
||||
if (string.Equals(otherUserUid, AuthenticatedUserId, System.StringComparison.Ordinal)) return;
|
||||
|
||||
// check if client pair even exists
|
||||
ClientPair callerPair =
|
||||
await _dbContext.ClientPairs.SingleOrDefaultAsync(w => w.UserUID == AuthenticatedUserId && w.OtherUserUID == otherUserUid).ConfigureAwait(false);
|
||||
bool callerHadPaused = callerPair.IsPaused;
|
||||
if (callerPair == null) return;
|
||||
|
||||
// delete from database, send update info to users pair list
|
||||
_dbContext.ClientPairs.Remove(callerPair);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.SendUserPairedClientRemoval, otherUserUid, "Success");
|
||||
|
||||
await Clients.User(AuthenticatedUserId)
|
||||
.SendAsync(Api.OnUserUpdateClientPairs, new ClientPairDto()
|
||||
{
|
||||
OtherUID = otherUserUid,
|
||||
IsRemoved = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
// check if opposite entry exists
|
||||
var oppositeClientPair = OppositeEntry(otherUserUid);
|
||||
if (oppositeClientPair == null) return;
|
||||
|
||||
// check if other user is online, if no then there is no need to do anything further
|
||||
var otherIdent = await _clientIdentService.GetCharacterIdentForUid(otherUserUid).ConfigureAwait(false);
|
||||
if (otherIdent == null) return;
|
||||
|
||||
// get own ident and
|
||||
await Clients.User(otherUserUid).SendAsync(Api.OnUserUpdateClientPairs,
|
||||
new ClientPairDto()
|
||||
{
|
||||
OtherUID = AuthenticatedUserId,
|
||||
IsPausedFromOthers = false,
|
||||
IsSynced = false
|
||||
}).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, otherUserUid, System.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)
|
||||
{
|
||||
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUserUid).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userIdent).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)
|
||||
{
|
||||
var userIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
await Clients.User(AuthenticatedUserId).SendAsync(Api.OnUserAddOnlinePairedPlayer, otherIdent).ConfigureAwait(false);
|
||||
await Clients.User(otherUserUid).SendAsync(Api.OnUserAddOnlinePairedPlayer, userIdent).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private ClientPair OppositeEntry(string otherUID) =>
|
||||
_dbContext.ClientPairs.AsNoTracking().SingleOrDefault(w => w.User.UID == otherUID && w.OtherUser.UID == AuthenticatedUserId);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosServer.Services;
|
||||
using MareSynchronosServer.Utils;
|
||||
using MareSynchronosShared.Authentication;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
@@ -27,9 +28,14 @@ public partial class MareHub : Hub
|
||||
private readonly SystemInfoService _systemInfoService;
|
||||
private readonly IHttpContextAccessor _contextAccessor;
|
||||
private readonly IClientIdentificationService _clientIdentService;
|
||||
private readonly ILogger<MareHub> _logger;
|
||||
private readonly MareHubLogger _logger;
|
||||
private readonly MareDbContext _dbContext;
|
||||
private readonly Uri _cdnFullUri;
|
||||
private readonly string _shardName;
|
||||
private readonly int _maxExistingGroupsByUser;
|
||||
private readonly int _maxJoinedGroupsByUser;
|
||||
private readonly int _maxGroupUserCount;
|
||||
|
||||
public MareHub(MareMetrics mareMetrics, AuthService.AuthServiceClient authServiceClient, FileService.FileServiceClient fileServiceClient,
|
||||
MareDbContext mareDbContext, ILogger<MareHub> logger, SystemInfoService systemInfoService, IConfiguration configuration, IHttpContextAccessor contextAccessor,
|
||||
IClientIdentificationService clientIdentService)
|
||||
@@ -38,10 +44,15 @@ public partial class MareHub : Hub
|
||||
_authServiceClient = authServiceClient;
|
||||
_fileServiceClient = fileServiceClient;
|
||||
_systemInfoService = systemInfoService;
|
||||
_cdnFullUri = new Uri(configuration.GetRequiredSection("MareSynchronos").GetValue<string>("CdnFullUrl"));
|
||||
var config = configuration.GetRequiredSection("MareSynchronos");
|
||||
_cdnFullUri = new Uri(config.GetValue<string>("CdnFullUrl"));
|
||||
_shardName = config.GetValue("ShardName", "Main");
|
||||
_maxExistingGroupsByUser = config.GetValue<int>("MaxExistingGroupsByUser", 3);
|
||||
_maxJoinedGroupsByUser = config.GetValue<int>("MaxJoinedGroupsByUser", 6);
|
||||
_maxGroupUserCount = config.GetValue<int>("MaxGroupUserCount", 100);
|
||||
_contextAccessor = contextAccessor;
|
||||
_clientIdentService = clientIdentService;
|
||||
_logger = logger;
|
||||
_logger = new MareHubLogger(this, logger);
|
||||
_dbContext = mareDbContext;
|
||||
}
|
||||
|
||||
@@ -51,9 +62,9 @@ public partial class MareHub : Hub
|
||||
{
|
||||
_mareMetrics.IncCounter(MetricsAPI.CounterInitializedConnections);
|
||||
|
||||
var userId = Context.User!.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
|
||||
var userId = Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value;
|
||||
|
||||
_logger.LogInformation("Connection from {userId}, CI: {characterIdentification}", userId, characterIdentification);
|
||||
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification);
|
||||
|
||||
await Clients.Caller.SendAsync(Api.OnUpdateSystemInfo, _systemInfoService.SystemInfoDto).ConfigureAwait(false);
|
||||
|
||||
@@ -62,9 +73,11 @@ public partial class MareHub : Hub
|
||||
if (!string.IsNullOrEmpty(userId) && !isBanned && !string.IsNullOrEmpty(characterIdentification))
|
||||
{
|
||||
var user = (await _dbContext.Users.SingleAsync(u => u.UID == userId).ConfigureAwait(false));
|
||||
var existingIdent = _clientIdentService.GetCharacterIdentForUid(userId);
|
||||
if (!string.IsNullOrEmpty(existingIdent) && characterIdentification != existingIdent)
|
||||
var existingIdent = await _clientIdentService.GetCharacterIdentForUid(userId).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(existingIdent) && !string.Equals(characterIdentification, existingIdent, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Failure", "LoggedIn");
|
||||
|
||||
return new ConnectionDto()
|
||||
{
|
||||
ServerVersion = Api.Version
|
||||
@@ -72,27 +85,50 @@ public partial class MareHub : Hub
|
||||
}
|
||||
|
||||
user.LastLoggedIn = DateTime.UtcNow;
|
||||
_clientIdentService.MarkUserOnline(user.UID, characterIdentification);
|
||||
await _clientIdentService.MarkUserOnline(user.UID, characterIdentification).ConfigureAwait(false);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Success");
|
||||
|
||||
return new ConnectionDto
|
||||
{
|
||||
ServerVersion = Api.Version,
|
||||
UID = string.IsNullOrEmpty(user.Alias) ? user.UID : user.Alias,
|
||||
IsModerator = user.IsModerator,
|
||||
IsAdmin = user.IsAdmin
|
||||
IsAdmin = user.IsAdmin,
|
||||
ServerInfo = new ServerInfoDto()
|
||||
{
|
||||
MaxGroupsCreatedByUser = _maxExistingGroupsByUser,
|
||||
ShardName = _shardName,
|
||||
MaxGroupsJoinedByUser = _maxJoinedGroupsByUser,
|
||||
MaxGroupUserCount = _maxGroupUserCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogCallInfo(Api.InvokeHeartbeat, characterIdentification, "Failure");
|
||||
|
||||
return new ConnectionDto()
|
||||
{
|
||||
ServerVersion = Api.Version
|
||||
};
|
||||
}
|
||||
|
||||
[HubMethodName(Api.InvokeCheckClientHealth)]
|
||||
[Authorize(AuthenticationSchemes = SecretKeyGrpcAuthenticationHandler.AuthScheme)]
|
||||
public async Task<bool> CheckClientHealth()
|
||||
{
|
||||
var needsReconnect = string.IsNullOrEmpty(await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false));
|
||||
if (needsReconnect)
|
||||
{
|
||||
_logger.LogCallWarning(Api.InvokeCheckClientHealth, needsReconnect);
|
||||
}
|
||||
return needsReconnect;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("Connection from {ip}", _contextAccessor.GetIpAddress());
|
||||
_logger.LogCallInfo("Connect", _contextAccessor.GetIpAddress());
|
||||
_mareMetrics.IncGauge(MetricsAPI.GaugeConnections);
|
||||
await base.OnConnectedAsync().ConfigureAwait(false);
|
||||
}
|
||||
@@ -101,50 +137,22 @@ public partial class MareHub : Hub
|
||||
{
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugeConnections);
|
||||
|
||||
var userCharaIdent = _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId);
|
||||
var userCharaIdent = await _clientIdentService.GetCharacterIdentForUid(AuthenticatedUserId).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(userCharaIdent))
|
||||
{
|
||||
var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UID == AuthenticatedUserId)!.ConfigureAwait(false);
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugeAuthorizedConnections);
|
||||
|
||||
_logger.LogInformation("Disconnect from {id}", AuthenticatedUserId);
|
||||
_logger.LogCallInfo("Disconnect");
|
||||
|
||||
var query =
|
||||
from userToOther in _dbContext.ClientPairs
|
||||
join otherToUser in _dbContext.ClientPairs
|
||||
on new
|
||||
{
|
||||
user = userToOther.UserUID,
|
||||
other = userToOther.OtherUserUID
|
||||
await SendDataToAllPairedUsers(Api.OnUserRemoveOnlinePairedPlayer, userCharaIdent).ConfigureAwait(false);
|
||||
|
||||
} equals new
|
||||
{
|
||||
user = otherToUser.OtherUserUID,
|
||||
other = otherToUser.UserUID
|
||||
}
|
||||
where
|
||||
userToOther.UserUID == user.UID
|
||||
&& !userToOther.IsPaused
|
||||
&& !otherToUser.IsPaused
|
||||
select otherToUser.UserUID;
|
||||
var otherEntries = await query.ToListAsync().ConfigureAwait(false);
|
||||
_dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == AuthenticatedUserId));
|
||||
|
||||
await Clients.Users(otherEntries).SendAsync(Api.OnUserRemoveOnlinePairedPlayer, userCharaIdent).ConfigureAwait(false);
|
||||
|
||||
_dbContext.RemoveRange(_dbContext.Files.Where(f => !f.Uploaded && f.UploaderUID == user.UID));
|
||||
|
||||
_clientIdentService.MarkUserOffline(user.UID);
|
||||
await _clientIdentService.MarkUserOffline(AuthenticatedUserId).ConfigureAwait(false);
|
||||
await _dbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected string AuthenticatedUserId => Context.User?.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown";
|
||||
|
||||
protected async Task<User> GetAuthenticatedUserUntrackedAsync()
|
||||
{
|
||||
return await _dbContext.Users.AsNoTrackingWithIdentityResolution().SingleAsync(u => u.UID == AuthenticatedUserId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.47.0" />
|
||||
<PackageReference Include="Karambolo.Extensions.Logging.File" Version="3.3.1" />
|
||||
<PackageReference Include="lz4net" Version="1.0.15.93" />
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="1.0.733">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.8" />
|
||||
|
||||
@@ -9,58 +9,57 @@ using Microsoft.Extensions.Logging;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
|
||||
namespace MareSynchronosServer
|
||||
namespace MareSynchronosServer;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public class Program
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
var hostBuilder = CreateHostBuilder(args);
|
||||
var host = hostBuilder.Build();
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
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 secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
|
||||
if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal))
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
using var context = services.GetRequiredService<MareDbContext>();
|
||||
context.Database.Migrate();
|
||||
context.SaveChanges();
|
||||
|
||||
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
|
||||
if (string.IsNullOrEmpty(secondaryServer) || secondaryServer == "0")
|
||||
{
|
||||
context.Database.Migrate();
|
||||
context.SaveChanges();
|
||||
|
||||
// clean up residuals
|
||||
var looseFiles = context.Files.Where(f => f.Uploaded == false);
|
||||
var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null);
|
||||
context.RemoveRange(unfinishedRegistrations);
|
||||
context.RemoveRange(looseFiles);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
var metrics = services.GetRequiredService<MareMetrics>();
|
||||
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.Count());
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused));
|
||||
// clean up residuals
|
||||
var looseFiles = context.Files.Where(f => f.Uploaded == false);
|
||||
var unfinishedRegistrations = context.LodeStoneAuth.Where(c => c.StartedAt != null);
|
||||
context.RemoveRange(unfinishedRegistrations);
|
||||
context.RemoveRange(looseFiles);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
if (args.Length == 0 || args[0] != "dry")
|
||||
{
|
||||
host.Run();
|
||||
}
|
||||
var metrics = services.GetRequiredService<MareMetrics>();
|
||||
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairs, context.ClientPairs.Count());
|
||||
metrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, context.ClientPairs.Count(p => p.IsPaused));
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.UseConsoleLifetime()
|
||||
.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<Startup>();
|
||||
});
|
||||
if (args.Length == 0 || !string.Equals(args[0], "dry", StringComparison.Ordinal))
|
||||
{
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.UseSystemd()
|
||||
.UseConsoleLifetime()
|
||||
.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<Startup>();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API;
|
||||
@@ -7,6 +8,7 @@ using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -16,16 +18,19 @@ namespace MareSynchronosServer.Services;
|
||||
public class SystemInfoService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly MareMetrics _mareMetrics;
|
||||
private readonly IClientIdentificationService clientIdentService;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly IClientIdentificationService _clientIdentService;
|
||||
private readonly ILogger<SystemInfoService> _logger;
|
||||
private readonly IHubContext<MareHub> _hubContext;
|
||||
private Timer _timer;
|
||||
private string _shardName;
|
||||
public SystemInfoDto SystemInfoDto { get; private set; } = new();
|
||||
|
||||
public SystemInfoService(MareMetrics mareMetrics, IClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub> hubContext)
|
||||
public SystemInfoService(MareMetrics mareMetrics, IConfiguration configuration, IServiceProvider services, IClientIdentificationService clientIdentService, ILogger<SystemInfoService> logger, IHubContext<MareHub> hubContext)
|
||||
{
|
||||
_mareMetrics = mareMetrics;
|
||||
this.clientIdentService = clientIdentService;
|
||||
_services = services;
|
||||
_clientIdentService = clientIdentService;
|
||||
_logger = logger;
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
@@ -42,27 +47,28 @@ public class SystemInfoService : IHostedService, IDisposable
|
||||
private void PushSystemInfo(object state)
|
||||
{
|
||||
ThreadPool.GetAvailableThreads(out int workerThreads, out int ioThreads);
|
||||
_logger.LogInformation("ThreadPool: {workerThreads} workers available, {ioThreads} IO workers available", workerThreads, ioThreads);
|
||||
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableWorkerThreads, workerThreads);
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeAvailableIOWorkerThreads, ioThreads);
|
||||
|
||||
|
||||
var secondaryServer = Environment.GetEnvironmentVariable("SECONDARY_SERVER");
|
||||
if (string.IsNullOrEmpty(secondaryServer) || secondaryServer == "0")
|
||||
if (string.IsNullOrEmpty(secondaryServer) || string.Equals(secondaryServer, "0", StringComparison.Ordinal))
|
||||
{
|
||||
SystemInfoDto = new SystemInfoDto()
|
||||
{
|
||||
CacheUsage = 0,
|
||||
CpuUsage = 0,
|
||||
RAMUsage = 0,
|
||||
NetworkIn = 0,
|
||||
NetworkOut = 0,
|
||||
OnlineUsers = clientIdentService.GetOnlineUsers(),
|
||||
UploadedFiles = 0
|
||||
OnlineUsers = _clientIdentService.GetOnlineUsers().Result,
|
||||
};
|
||||
|
||||
_hubContext.Clients.All.SendAsync(Api.OnUpdateSystemInfo, SystemInfoDto);
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
using var db = scope.ServiceProvider.GetService<MareDbContext>()!;
|
||||
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugePairs, db.ClientPairs.Count());
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugePairsPaused, db.ClientPairs.Count(p => p.IsPaused));
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroups, db.Groups.Count());
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairs, db.GroupPairs.Count());
|
||||
_mareMetrics.SetGaugeTo(MetricsAPI.GaugeGroupPairsPaused, db.GroupPairs.Count(p => p.IsPaused));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,169 +22,170 @@ using System.Collections.Generic;
|
||||
using MareSynchronosServer.Services;
|
||||
using MareSynchronosShared.Services;
|
||||
using System.Net.Http;
|
||||
using MareSynchronosServer.Utils;
|
||||
|
||||
namespace MareSynchronosServer
|
||||
namespace MareSynchronosServer;
|
||||
|
||||
public class Startup
|
||||
{
|
||||
public class Startup
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
|
||||
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
|
||||
|
||||
services.AddMemoryCache();
|
||||
services.AddInMemoryRateLimiting();
|
||||
|
||||
services.AddSingleton<SystemInfoService, SystemInfoService>();
|
||||
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
||||
services.AddTransient(_ => Configuration);
|
||||
|
||||
var mareConfig = Configuration.GetRequiredSection("MareSynchronos");
|
||||
|
||||
var defaultMethodConfig = new MethodConfig
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
|
||||
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
|
||||
|
||||
services.AddMemoryCache();
|
||||
services.AddInMemoryRateLimiting();
|
||||
|
||||
services.AddSingleton<SystemInfoService, SystemInfoService>();
|
||||
services.AddSingleton<IUserIdProvider, IdBasedUserIdProvider>();
|
||||
services.AddTransient(_ => Configuration);
|
||||
|
||||
var mareConfig = Configuration.GetRequiredSection("MareSynchronos");
|
||||
|
||||
var defaultMethodConfig = new MethodConfig
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = new RetryPolicy
|
||||
{
|
||||
Names = { MethodName.Default },
|
||||
RetryPolicy = new RetryPolicy
|
||||
{
|
||||
MaxAttempts = 100,
|
||||
InitialBackoff = TimeSpan.FromSeconds(1),
|
||||
MaxBackoff = TimeSpan.FromSeconds(5),
|
||||
BackoffMultiplier = 1.5,
|
||||
RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable }
|
||||
}
|
||||
MaxAttempts = 100,
|
||||
InitialBackoff = TimeSpan.FromSeconds(1),
|
||||
MaxBackoff = TimeSpan.FromSeconds(5),
|
||||
BackoffMultiplier = 1.5,
|
||||
RetryableStatusCodes = { Grpc.Core.StatusCode.Unavailable }
|
||||
}
|
||||
};
|
||||
|
||||
services.AddSingleton(new MareMetrics(new List<string>
|
||||
{
|
||||
MetricsAPI.CounterInitializedConnections,
|
||||
MetricsAPI.CounterUserPushData,
|
||||
MetricsAPI.CounterUserPushDataTo,
|
||||
MetricsAPI.CounterUsersRegisteredDeleted,
|
||||
}, new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeAuthorizedConnections,
|
||||
MetricsAPI.GaugeConnections,
|
||||
MetricsAPI.GaugePairs,
|
||||
MetricsAPI.GaugePairsPaused,
|
||||
MetricsAPI.GaugeAvailableIOWorkerThreads,
|
||||
MetricsAPI.GaugeAvailableWorkerThreads
|
||||
}));
|
||||
|
||||
services.AddGrpcClient<AuthService.AuthServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>("ServiceAddress"));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
|
||||
services.AddSingleton(new MareMetrics(new List<string>
|
||||
{
|
||||
MetricsAPI.CounterInitializedConnections,
|
||||
MetricsAPI.CounterUserPushData,
|
||||
MetricsAPI.CounterUserPushDataTo,
|
||||
MetricsAPI.CounterUsersRegisteredDeleted,
|
||||
}, new List<string>
|
||||
{
|
||||
MetricsAPI.GaugeAuthorizedConnections,
|
||||
MetricsAPI.GaugeConnections,
|
||||
MetricsAPI.GaugePairs,
|
||||
MetricsAPI.GaugePairsPaused,
|
||||
MetricsAPI.GaugeAvailableIOWorkerThreads,
|
||||
MetricsAPI.GaugeAvailableWorkerThreads
|
||||
}));
|
||||
|
||||
services.AddGrpcClient<AuthService.AuthServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>("ServiceAddress"));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
|
||||
c.HttpHandler = new SocketsHttpHandler()
|
||||
{
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
});
|
||||
services.AddGrpcClient<FileService.FileServiceClient>(c =>
|
||||
{
|
||||
c.Address = new Uri(mareConfig.GetValue<string>("StaticFileServiceAddress"));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
|
||||
});
|
||||
|
||||
services.AddDbContextPool<MareDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("MareSynchronosShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, mareConfig.GetValue("DbContextPoolSize", 1024));
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme;
|
||||
}).AddScheme<AuthenticationSchemeOptions, SecretKeyGrpcAuthenticationHandler>(SecretKeyGrpcAuthenticationHandler.AuthScheme, _ => { });
|
||||
services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
|
||||
|
||||
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
|
||||
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
|
||||
{
|
||||
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
|
||||
hubOptions.EnableDetailedErrors = true;
|
||||
hubOptions.MaximumParallelInvocationsPerClient = 10;
|
||||
hubOptions.StreamBufferCapacity = 200;
|
||||
hubOptions.AddFilter<SignalRLimitFilter>();
|
||||
});
|
||||
|
||||
// add redis related options
|
||||
var redis = mareConfig.GetValue("RedisConnectionString", string.Empty);
|
||||
if (!string.IsNullOrEmpty(redis))
|
||||
{
|
||||
signalRServiceBuilder.AddStackExchangeRedis(redis, options =>
|
||||
{
|
||||
options.Configuration.ChannelPrefix = "MareSynchronos";
|
||||
});
|
||||
|
||||
services.AddStackExchangeRedisCache(opt =>
|
||||
{
|
||||
opt.Configuration = redis;
|
||||
opt.InstanceName = "MareSynchronosCache:";
|
||||
});
|
||||
services.AddSingleton<IClientIdentificationService, DistributedClientIdentificationService>();
|
||||
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IClientIdentificationService, LocalClientIdentificationService>();
|
||||
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
|
||||
}
|
||||
|
||||
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
});
|
||||
services.AddGrpcClient<FileService.FileServiceClient>(c =>
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
c.Address = new Uri(mareConfig.GetValue<string>("StaticFileServiceAddress"));
|
||||
}).ConfigureChannel(c =>
|
||||
{
|
||||
c.ServiceConfig = new ServiceConfig { MethodConfigs = { defaultMethodConfig } };
|
||||
});
|
||||
|
||||
services.AddDbContextPool<MareDbContext>(options =>
|
||||
{
|
||||
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder =>
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseMigrationsEndPoint();
|
||||
}
|
||||
else
|
||||
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
|
||||
builder.MigrationsAssembly("MareSynchronosShared");
|
||||
}).UseSnakeCaseNamingConvention();
|
||||
options.EnableThreadSafetyChecks(false);
|
||||
}, mareConfig.GetValue("DbContextPoolSize", 1024));
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = SecretKeyGrpcAuthenticationHandler.AuthScheme;
|
||||
}).AddScheme<AuthenticationSchemeOptions, SecretKeyGrpcAuthenticationHandler>(SecretKeyGrpcAuthenticationHandler.AuthScheme, _ => { });
|
||||
services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
|
||||
|
||||
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
|
||||
|
||||
var signalRServiceBuilder = services.AddSignalR(hubOptions =>
|
||||
{
|
||||
hubOptions.MaximumReceiveMessageSize = long.MaxValue;
|
||||
hubOptions.EnableDetailedErrors = true;
|
||||
hubOptions.MaximumParallelInvocationsPerClient = 10;
|
||||
hubOptions.StreamBufferCapacity = 200;
|
||||
|
||||
hubOptions.AddFilter<SignalRLimitFilter>();
|
||||
});
|
||||
|
||||
// add redis related options
|
||||
var redis = mareConfig.GetValue("RedisConnectionString", string.Empty);
|
||||
if (!string.IsNullOrEmpty(redis))
|
||||
{
|
||||
signalRServiceBuilder.AddStackExchangeRedis(redis, options =>
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseWebSockets();
|
||||
|
||||
var metricServer = new KestrelMetricServer(4980);
|
||||
metricServer.Start();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapHub<MareHub>(Api.Path, options =>
|
||||
{
|
||||
options.ApplicationMaxBufferSize = 5242880;
|
||||
options.TransportMaxBufferSize = 5242880;
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
|
||||
});
|
||||
options.Configuration.ChannelPrefix = "MareSynchronos";
|
||||
});
|
||||
|
||||
services.AddStackExchangeRedisCache(opt =>
|
||||
{
|
||||
opt.Configuration = redis;
|
||||
opt.InstanceName = "MareSynchronosCache:";
|
||||
});
|
||||
services.AddSingleton<IClientIdentificationService, DistributedClientIdentificationService>();
|
||||
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IClientIdentificationService, LocalClientIdentificationService>();
|
||||
services.AddHostedService(p => p.GetService<IClientIdentificationService>());
|
||||
}
|
||||
|
||||
services.AddHostedService(provider => provider.GetService<SystemInfoService>());
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseMigrationsEndPoint();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseIpRateLimiting();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseWebSockets();
|
||||
|
||||
var metricServer = new KestrelMetricServer(4980);
|
||||
metricServer.Start();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapHub<MareHub>(Api.Path, options =>
|
||||
{
|
||||
options.ApplicationMaxBufferSize = 5242880;
|
||||
options.TransportMaxBufferSize = 5242880;
|
||||
options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public class IdBasedUserIdProvider : IUserIdProvider
|
||||
{
|
||||
public string GetUserId(HubConnectionContext context)
|
||||
{
|
||||
return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, System.StringComparison.Ordinal))?.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using MareSynchronosServer.Hubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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 void LogCallInfo(string methodName, params object[] args)
|
||||
{
|
||||
string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogInformation("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs);
|
||||
}
|
||||
|
||||
public void LogCallWarning(string methodName, params object[] args)
|
||||
{
|
||||
string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogWarning("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public enum PauseInfo
|
||||
{
|
||||
NoConnection,
|
||||
Paused,
|
||||
Unpaused
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public record PauseState
|
||||
{
|
||||
public string GID { get; set; }
|
||||
public bool IsPaused { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
namespace MareSynchronosServer.Utils;
|
||||
public class SignalRLimitFilter : IHubFilter
|
||||
{
|
||||
private readonly IRateLimitProcessor _processor;
|
||||
@@ -42,7 +42,7 @@ public class SignalRLimitFilter : IHubFilter
|
||||
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
|
||||
if (counter.Count > rule.Limit)
|
||||
{
|
||||
var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? "Unknown";
|
||||
var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, 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}");
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user