Skip to content

Commit

Permalink
Completed SignalR query
Browse files Browse the repository at this point in the history
  • Loading branch information
AlenGeoAlex committed Jan 20, 2024
1 parent 2478c49 commit c4b8fa4
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 24 deletions.
13 changes: 10 additions & 3 deletions Sharecode.Backend.Api/SignalR/SnippetHub.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Sharecode.Backend.Application.Client;
using Sharecode.Backend.Application.Features.Live.Snippet;
Expand All @@ -12,7 +13,8 @@ namespace Sharecode.Backend.Api.SignalR;

public class SnippetHub(ILogger logger, IGroupStateManager groupStateManager, IMediator mediator, IAppCacheClient appCacheClient) : AbstractHub<ISignalRClient>(logger, groupStateManager, mediator, appCacheClient)
{

private readonly ILogger _logger = logger;

public override async Task OnConnectedAsync()
{
var queries = Context.GetHttpContext()?.Request.Query;
Expand All @@ -31,7 +33,8 @@ public override async Task OnConnectedAsync()

var joinedSnippetEvent = new JoinedSnippetEvent()
{
SnippetId = snippetId
SnippetId = snippetId,
ConnectionId = Context.ConnectionId
};

var joinedSnippetResponse = await Mediator.Send(joinedSnippetEvent);
Expand All @@ -45,16 +48,20 @@ public override async Task OnConnectedAsync()
var added = await AddToGroupAsync(joinedSnippetResponse.SnippetId.ToString(), Context.ConnectionId,
joinedSnippetResponse.JoinedUserId.ToString() ?? joinedSnippetResponse.JoinedUserName);
if (added)
{
await Clients.Group(joinedSnippetResponse.SnippetId.ToString())
.Message(LiveEvent<object>.Of(joinedSnippetEvent));
Context.Items["NAME"] = joinedSnippetResponse.JoinedUserName;
}

}

public override async Task OnDisconnectedAsync(Exception? exception)
{
var contextConnectionId = Context.ConnectionId;
if (exception != null)
{
logger.Error(exception, "A disconnect event has been called with an error message {Message} on connection id ", exception.Message, contextConnectionId);
_logger.Error(exception, "A disconnect event has been called with an error message {Message} on connection id ", exception.Message, contextConnectionId);
}

await DisconnectAsync(Context.ConnectionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ namespace Sharecode.Backend.Application.Features.Live.Snippet;
public class JoinedSnippetEvent : IAppRequest<JoinedSnippetResponse?>
{
public Guid SnippetId { get; init; }
public string ConnectionId { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,121 @@
using MediatR;
using Sharecode.Backend.Application.Client;
using Sharecode.Backend.Application.Service;

namespace Sharecode.Backend.Application.Features.Live.Snippet;

public class JoinedSnippetLiveHandler : IRequestHandler<JoinedSnippetEvent, JoinedSnippetResponse?>
public class JoinedSnippetLiveHandler(IHttpClientContext clientContext, IGroupStateManager groupStateManager, ISnippetService snippetService, IUserService userService) : IRequestHandler<JoinedSnippetEvent, JoinedSnippetResponse?>
{
public Task<JoinedSnippetResponse?> Handle(JoinedSnippetEvent request, CancellationToken cancellationToken)
public async Task<JoinedSnippetResponse?> Handle(JoinedSnippetEvent request, CancellationToken cancellationToken)
{
throw new NotImplementedException();
var requestingUser = await clientContext.GetUserIdentifierAsync() ?? Guid.Empty;
//If the requests doesn't have an authorization token.
//Scenario - Public Snippets
bool isRequestAnonymous = requestingUser == Guid.Empty;
// Get the users access to work on snippets
//If there is no authorization token, the request would be sent with Empty Guid, which won't match the select
//statement with in the access control table.
var snippetAccess = await snippetService.GetSnippetAccess(request.SnippetId, requestingUser , false);
//If the user doesn't have any access to that particular snippet, then return null. If the snippet is public
//the fn will return read access
if (!snippetAccess.Any())
{
return null;
}

//Get all the existing members in the signalR group
var currentMembers = await groupStateManager.Members(request.SnippetId.ToString(), cancellationToken);
//The way we store it in SignalR is, if there is a validated user, he/she will be a Guid, if its a string, it will
//be an anonymous user.
var loggedInUsers = currentMembers.Values.Select(x =>
{
bool success = Guid.TryParse(x, out var parsed);
return new { success, parsed };
})
.Where(x => x.success)
.Select(x => x.parsed)
.ToHashSet();
//If the requesting user is logged in, then also add him/her too, so that we can get the logo and all of this newly
//joined user too
if (!isRequestAnonymous)
{
loggedInUsers.Add(requestingUser);
}

//Get User Profile Information
var userEnumerable = await userService.GetUsersProfileInformationAsync(loggedInUsers, cancellationToken);
var activeUsers = userEnumerable
.Select(x =>
new ActiveSnippetUsersDto()
{
Id = x.Id,
FullName = x.FullName,
ProfilePicture = x.ProfilePicture
})
.ToDictionary(x => x.Id!.Value, x => x);

List<ActiveSnippetUsersDto> responseUsers = [];
//Loop through the current members in the group
foreach (var (connectionId, userIdentifier) in currentMembers)
{
//If the current member in the group is a Guid, that means he/she was a logged in user
if (Guid.TryParse(userIdentifier, out var uniqueUserId))
{
//Check whether such a user was there in the response returned from Db
if (activeUsers.TryGetValue(uniqueUserId, out var activeUser))
{
//If there is attach connectionId and also add to the return array
activeUser.ConnectionId = connectionId;
responseUsers.Add(activeUser);
}
}
else
{
//In this else case, it means the user is a non-logged in user
//In this case, only attach the name and connection Id we have
var anonymousUser = new ActiveSnippetUsersDto()
{
ConnectionId = connectionId,
FullName = userIdentifier,
Id = null,
ProfilePicture = null
};
responseUsers.Add(anonymousUser);
}
}

var response = new JoinedSnippetResponse()
{
JoinedUserAccesses = snippetAccess.ToControlModel(),
JoinedUserId = Guid.Empty,
ActiveUsers = responseUsers
};

//If the requesting user was not logged in or if there is no data
//associated with the requested user in the dbResponse
//Set him as an anonymous User, also add him in the array of current users
if (isRequestAnonymous || !activeUsers.ContainsKey(requestingUser))
{
response.JoinedUserName = "Anonymous User";
var anonymousUser = new ActiveSnippetUsersDto()
{
ConnectionId = request.ConnectionId,
FullName = response.JoinedUserName,
Id = null,
ProfilePicture = null
};
responseUsers.Add(anonymousUser);
}
else
{
//If there is data, Set the UserName and UserId for the newly joined user
//Also add him in the current users array
var activeSnippetUsersDto = activeUsers[requestingUser];
responseUsers.Add(activeSnippetUsersDto);
response.JoinedUserName = activeSnippetUsersDto.FullName;
response.JoinedUserId = activeSnippetUsersDto.Id;
}

return response;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
namespace Sharecode.Backend.Application.Features.Live.Snippet;
using Sharecode.Backend.Domain.Dto.Snippet;

namespace Sharecode.Backend.Application.Features.Live.Snippet;

public class JoinedSnippetResponse
{
public Guid SnippetId { get; init; }
public Guid? JoinedUserId { get; init; }
public string JoinedUserName { get; init; }
public List<ActiveSnippetUsersDto> ActiveUsers { get; } = [];
public Guid? JoinedUserId { get; set; }
public string JoinedUserName { get; set; }
public SnippetAccessControlDto JoinedUserAccesses { get; set; }
public List<ActiveSnippetUsersDto> ActiveUsers { get; set; } = [];
}

public sealed record ActiveSnippetUsersDto
{
public Guid? Id { get; init; }
public string ConnectionId { get; init; }
public string ConnectionId { get; set; }
public string FullName { get; init; }
public string? ProfilePicture { get; init; }
}
8 changes: 8 additions & 0 deletions Sharecode.Backend.Application/Service/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ Task<IReadOnlyList<User>> GetUsersToTagAsync(string searchQuery, int take, int s
bool shouldEnableTagging = true, CancellationToken token = default);

/// <summary>
/// Get the basic information of provided list of users
/// </summary>
/// <param name="userIds">Collection of user ids</param>
/// <param name="token">Cancellation token</param>
/// <returns></returns>
Task<IEnumerable<User>> GetUsersProfileInformationAsync(IEnumerable<Guid> userIds,
CancellationToken token = default);
/// <summary>
/// Retrieves a list of snippets for a specified user.
/// </summary>
/// <param name="userId">The ID of the user.</param>
Expand Down
8 changes: 8 additions & 0 deletions Sharecode.Backend.Domain/Enums/SnippetAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Sharecode.Backend.Domain.Enums;

public enum SnippetAccess
{
Read,
Write,
Manage
}
43 changes: 32 additions & 11 deletions Sharecode.Backend.Domain/Helper/SnippetAccessPermission.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
namespace Sharecode.Backend.Domain.Helper;
using Sharecode.Backend.Domain.Dto.Snippet;
using Sharecode.Backend.Domain.Entity.Snippet;
using Sharecode.Backend.Domain.Enums;

public class SnippetAccessPermission(Guid snippetId, Guid accessorId, bool read, bool write, bool manage)
namespace Sharecode.Backend.Domain.Helper;

public class SnippetAccessPermission(Guid snippetId, Guid accessorId, bool read, bool write, bool manage, bool snippetPublic)
{

public static SnippetAccessPermission NoPermission(Guid snippetId, Guid accessorId) =>
new SnippetAccessPermission(snippetId, accessorId, false, false, false);
new SnippetAccessPermission(snippetId, accessorId, false, false, false, true);

public static SnippetAccessPermission Error =>
new SnippetAccessPermission(Guid.Empty, Guid.Empty, false, false, false);
new SnippetAccessPermission(Guid.Empty, Guid.Empty, false, false, false, true);

public Guid SnippetId { get; } = snippetId;
public Guid AccessorId { get; } = accessorId;
private bool Read { get; } = read || write || manage;
private bool Write { get; } = write || manage;
private bool Manage { get; } = manage;

public bool IsPublicSnippet => snippetPublic;
public bool IsPrivateSnippet => !snippetPublic;

public bool Any()
{
return Any(SnippetAccess.Read, SnippetAccess.Write, SnippetAccess.Manage);
}

public bool Any(params SnippetAccess[] accesses)
{
foreach (var access in accesses)
Expand All @@ -31,6 +43,11 @@ public bool Any(params SnippetAccess[] accesses)
return false;
}

public bool All()
{
return All(SnippetAccess.Read, SnippetAccess.Write, SnippetAccess.Manage);
}

public bool All(params SnippetAccess[] accesses)
{
foreach (var access in accesses)
Expand All @@ -46,11 +63,15 @@ public bool All(params SnippetAccess[] accesses)

return true;
}
}

public enum SnippetAccess
{
Read,
Write,
Manage
}
public SnippetAccessControlDto ToControlModel()
{
return new SnippetAccessControlDto()
{
UserId = AccessorId,
Manage = manage,
Read = read,
Write = write
};
}
}
8 changes: 8 additions & 0 deletions Sharecode.Backend.Domain/Repositories/IUserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,12 @@ Task<List<User>> GetUsersForMentionWithNotificationSettings(HashSet<Guid> userId
/// <param name="token">The cancellation token to cancel operation if needed. Default value is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="Task{T}"/> representing the asynchronous operation. The task result contains a <see cref="HashSet{T}"/> of <see cref="Permission"/> objects representing the user's permissions.</returns>
Task<HashSet<Permission>> GetUsersPermissionAsync(Guid userId, CancellationToken token = default);

/// <summary>
/// Get the basic information of provided list of users
/// </summary>
/// <param name="userIds">Collection of user ids</param>
/// <param name="token">Cancellation token</param>
/// <returns></returns>
Task<IEnumerable<User>> GetListOfUserProfileAsync(IEnumerable<Guid> userIds, CancellationToken token = default);
}
1 change: 0 additions & 1 deletion Sharecode.Backend.Infrastructure/Base/BaseRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ protected static IQueryable<TEntity> ApplySpecification(IQueryable<TEntity> quer
if (specification.Includes != null)
{
query = specification.Includes.Aggregate(query, (current, include) => current.Include(include));

}
if (specification.OrderBy != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ public Task<long> GetTotalSizeOfSnippetOnSpecification(ISpecification<Snippet> s
.AsQueryable();

var snippetSpecification = ApplySpecification(queryable, specification);
return snippetSpecification.Select(x => x.Size).SumAsync(token);
return snippetSpecification
.Select(x => x.Size )
.SumAsync(token);
}
}

23 changes: 23 additions & 0 deletions Sharecode.Backend.Infrastructure/Repositories/UserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,27 @@ public async Task<HashSet<Permission>> GetUsersPermissionAsync(Guid userId, Canc
.Select(x => x.Permissions)
.FirstOrDefaultAsync(cancellationToken: token) ?? [];
}

public async Task<IEnumerable<User>> GetListOfUserProfileAsync(IEnumerable<Guid> userIds, CancellationToken token = default)
{
return await Table
.SetTracking(false)
.Where(x => userIds.Contains(x.Id))
.Select(x =>
new User
{
Id = x.Id,
EmailAddress = x.EmailAddress,
FirstName = x.FirstName,
LastName = x.LastName,
MiddleName = x.MiddleName,
ProfilePicture = x.ProfilePicture,
Visibility = AccountVisibility.Public,
AccountSetting = new AccountSetting()
{
EnableNotificationsForMentions = x.AccountSetting.EnableNotificationsForMentions
}
})
.ToListAsync(token);
}
}
5 changes: 5 additions & 0 deletions Sharecode.Backend.Infrastructure/Service/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ public async Task<IReadOnlyList<User>> GetUsersToTagAsync(string searchQuery, in
return mentionableUsers.ToList();
}

public async Task<IEnumerable<User>> GetUsersProfileInformationAsync(IEnumerable<Guid> userIds, CancellationToken token = default)
{
return await _userRepository.GetListOfUserProfileAsync(userIds, token);
}

public async Task<List<MySnippetsDto>> ListUserSnippetsAsync(Guid userId, bool onlyOwned = false,
bool recentSnippets = true, int skip = 0, int take = 20,
string order = "ASC", string orderBy = "ModifiedAt", string searchQuery = null, CancellationToken cancellationToken = default)
Expand Down

0 comments on commit c4b8fa4

Please sign in to comment.