Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signal clients on friend presence state changes #252

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions osu.Server.Spectator.Tests/AnonymousClientProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace osu.Server.Spectator.Tests
{
/// <summary>
/// Proxies objects of type <typeparamref name="T"/> as an anonymous <see cref="IClientProxy"/> object.
/// Useful in testing where <see cref="IHubContext{THub}"/> is used.
/// </summary>
/// <param name="clients">The typed clients object.</param>
/// <typeparam name="T">The type of clients being proxied.</typeparam>
public class AnonymousClientProxy<T>(IHubClients<T> clients) : IHubClients
{
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => new ClientProxy(clients.AllExcept(excludedConnectionIds));
public IClientProxy Client(string connectionId) => new ClientProxy(clients.Client(connectionId));
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => new ClientProxy(clients.Clients(connectionIds));
public IClientProxy Group(string groupName) => new ClientProxy(clients.Group(groupName));
public IClientProxy Groups(IReadOnlyList<string> groupNames) => new ClientProxy(clients.Groups(groupNames));
public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => new ClientProxy(clients.GroupExcept(groupName, excludedConnectionIds));
public IClientProxy User(string userId) => new ClientProxy(clients.User(userId));
public IClientProxy Users(IReadOnlyList<string> userIds) => new ClientProxy(clients.Users(userIds));
public IClientProxy All => new ClientProxy(clients.All);

private class ClientProxy(T? target) : IClientProxy
{
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = new CancellationToken())
{
return target == null
? Task.CompletedTask
: (Task)typeof(T).GetMethod(method, BindingFlags.Instance | BindingFlags.Public)!.Invoke(target, args)!;
}
}
}
}
50 changes: 30 additions & 20 deletions osu.Server.Spectator.Tests/MetadataHubTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,43 +31,53 @@ public class MetadataHubTest
public MetadataHubTest()
{
userStates = new EntityStore<MetadataClientState>();
mockGroupManager = new Mock<IGroupManager>();
mockWatchersGroup = new Mock<IMetadataClient>();
mockCaller = new Mock<IMetadataClient>();

var mockDatabase = new Mock<IDatabaseAccess>();

var databaseFactory = new Mock<IDatabaseFactory>();
databaseFactory.Setup(factory => factory.GetInstance()).Returns(mockDatabase.Object);
databaseFactory.Setup(factory => factory.GetInstance())
.Returns(mockDatabase.Object);

var loggerFactoryMock = new Mock<ILoggerFactory>();
loggerFactoryMock.Setup(factory => factory.CreateLogger(It.IsAny<string>()))
.Returns(new Mock<ILogger>().Object);

hub = new MetadataHub(
loggerFactoryMock.Object,
new MemoryCache(new MemoryCacheOptions()),
userStates,
databaseFactory.Object,
new Mock<IDailyChallengeUpdater>().Object,
new Mock<IScoreProcessedSubscriber>().Object);

var mockContext = new Mock<HubCallerContext>();
mockContext.Setup(ctx => ctx.UserIdentifier).Returns(user_id.ToString());

mockWatchersGroup = new Mock<IMetadataClient>();
mockCaller = new Mock<IMetadataClient>();

var mockClients = new Mock<IHubCallerClients<IMetadataClient>>();
mockClients.Setup(clients => clients.Group(MetadataHub.ONLINE_PRESENCE_WATCHERS_GROUP))
.Returns(mockWatchersGroup.Object);
mockClients.Setup(clients => clients.Caller)
.Returns(mockCaller.Object);

mockGroupManager = new Mock<IGroupManager>();
var mockHubContext = new Mock<IHubContext<MetadataHub>>();
mockHubContext.Setup(ctx => ctx.Groups)
.Returns(mockGroupManager.Object);
mockHubContext.Setup(ctx => ctx.Clients)
.Returns(new AnonymousClientProxy<IMetadataClient>(mockClients.Object));

var mockContext = new Mock<HubCallerContext>();
mockContext.Setup(ctx => ctx.UserIdentifier)
.Returns(user_id.ToString());
// this is to ensure that the `Context.GetHttpContext()` call in `MetadataHub.OnConnectedAsync()` doesn't nullref
// (the method in question is an extension, and it accesses `Features`; mocking further is not required).
mockContext.Setup(ctx => ctx.Features).Returns(new Mock<IFeatureCollection>().Object);
mockContext.Setup(ctx => ctx.Features)
.Returns(new Mock<IFeatureCollection>().Object);

hub.Context = mockContext.Object;
hub.Clients = mockClients.Object;
hub.Groups = mockGroupManager.Object;
hub = new MetadataHub(
loggerFactoryMock.Object,
new MemoryCache(new MemoryCacheOptions()),
userStates,
databaseFactory.Object,
new Mock<IDailyChallengeUpdater>().Object,
new Mock<IScoreProcessedSubscriber>().Object,
mockHubContext.Object)
{
Context = mockContext.Object,
Clients = mockClients.Object,
Groups = mockGroupManager.Object
};
}

[Fact]
Expand Down
18 changes: 9 additions & 9 deletions osu.Server.Spectator.Tests/Multiplayer/MultiplayerInviteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public async Task UserCanInviteFriends()
SetUserContext(ContextUser);
await Hub.JoinRoom(ROOM_ID);

Database.Setup(d => d.GetUserRelation(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync(new phpbb_zebra { friend = true });
Database.Setup(d => d.GetUserRelationAsync(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync(new phpbb_zebra { friend = true });

SetUserContext(ContextUser);
await Hub.InvitePlayer(USER_ID_2);
Expand All @@ -35,8 +35,8 @@ public async Task UserCantInviteUserTheyBlocked()
SetUserContext(ContextUser);
await Hub.JoinRoom(ROOM_ID);

Database.Setup(d => d.GetUserRelation(USER_ID, USER_ID_2)).ReturnsAsync(new phpbb_zebra { foe = true });
Database.Setup(d => d.GetUserRelation(USER_ID_2, USER_ID)).ReturnsAsync(new phpbb_zebra { friend = true });
Database.Setup(d => d.GetUserRelationAsync(USER_ID, USER_ID_2)).ReturnsAsync(new phpbb_zebra { foe = true });
Database.Setup(d => d.GetUserRelationAsync(USER_ID_2, USER_ID)).ReturnsAsync(new phpbb_zebra { friend = true });

SetUserContext(ContextUser);
await Assert.ThrowsAsync<UserBlockedException>(() => Hub.InvitePlayer(USER_ID_2));
Expand All @@ -54,8 +54,8 @@ public async Task UserCantInviteUserTheyAreBlockedBy()
SetUserContext(ContextUser);
await Hub.JoinRoom(ROOM_ID);

Database.Setup(d => d.GetUserRelation(USER_ID, USER_ID_2)).ReturnsAsync(new phpbb_zebra { friend = true });
Database.Setup(d => d.GetUserRelation(USER_ID_2, USER_ID)).ReturnsAsync(new phpbb_zebra { foe = true });
Database.Setup(d => d.GetUserRelationAsync(USER_ID, USER_ID_2)).ReturnsAsync(new phpbb_zebra { friend = true });
Database.Setup(d => d.GetUserRelationAsync(USER_ID_2, USER_ID)).ReturnsAsync(new phpbb_zebra { foe = true });

SetUserContext(ContextUser);
await Assert.ThrowsAsync<UserBlockedException>(() => Hub.InvitePlayer(USER_ID_2));
Expand All @@ -73,7 +73,7 @@ public async Task UserCantInviteUserWithDisabledPMs()
SetUserContext(ContextUser);
await Hub.JoinRoom(ROOM_ID);

Database.Setup(d => d.GetUserAllowsPMs(USER_ID_2)).ReturnsAsync(false);
Database.Setup(d => d.GetUserAllowsPMsAsync(USER_ID_2)).ReturnsAsync(false);

SetUserContext(ContextUser);
await Assert.ThrowsAsync<UserBlocksPMsException>(() => Hub.InvitePlayer(USER_ID_2));
Expand All @@ -91,7 +91,7 @@ public async Task UserCantInviteRestrictedUser()
SetUserContext(ContextUser);
await Hub.JoinRoom(ROOM_ID);

Database.Setup(d => d.GetUserRelation(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync(new phpbb_zebra { friend = true });
Database.Setup(d => d.GetUserRelationAsync(It.IsAny<int>(), It.IsAny<int>())).ReturnsAsync(new phpbb_zebra { friend = true });
Database.Setup(d => d.IsUserRestrictedAsync(It.IsAny<int>())).ReturnsAsync(true);

SetUserContext(ContextUser);
Expand All @@ -110,7 +110,7 @@ public async Task UserCanInviteUserWithEnabledPMs()
SetUserContext(ContextUser);
await Hub.JoinRoom(ROOM_ID);

Database.Setup(d => d.GetUserAllowsPMs(USER_ID_2)).ReturnsAsync(true);
Database.Setup(d => d.GetUserAllowsPMsAsync(USER_ID_2)).ReturnsAsync(true);

SetUserContext(ContextUser);
await Hub.InvitePlayer(USER_ID_2);
Expand Down Expand Up @@ -138,7 +138,7 @@ public async Task UserCanInviteIntoRoomWithPassword()
SetUserContext(ContextUser);
await Hub.JoinRoomWithPassword(ROOM_ID, password);

Database.Setup(d => d.GetUserAllowsPMs(USER_ID_2)).ReturnsAsync(true);
Database.Setup(d => d.GetUserAllowsPMsAsync(USER_ID_2)).ReturnsAsync(true);

SetUserContext(ContextUser);
await Hub.InvitePlayer(USER_ID_2);
Expand Down
4 changes: 1 addition & 3 deletions osu.Server.Spectator.Tests/Multiplayer/MultiplayerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@ protected MultiplayerTest()

var hubContext = new Mock<IHubContext<MultiplayerHub>>();
hubContext.Setup(ctx => ctx.Groups).Returns(Groups.Object);
hubContext.Setup(ctx => ctx.Clients.Client(It.IsAny<string>())).Returns<string>(connectionId => (ISingleClientProxy)Clients.Object.Client(connectionId));
hubContext.Setup(ctx => ctx.Clients.Group(It.IsAny<string>())).Returns<string>(groupName => (ISingleClientProxy)Clients.Object.Group(groupName));
hubContext.Setup(ctx => ctx.Clients.All).Returns((ISingleClientProxy)Clients.Object.All);
hubContext.Setup(ctx => ctx.Clients).Returns(new AnonymousClientProxy<IMultiplayerClient>(Clients.Object));

Groups.Setup(g => g.AddToGroupAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Callback<string, string, CancellationToken>((connectionId, groupId, _) =>
Expand Down
20 changes: 15 additions & 5 deletions osu.Server.Spectator/Database/DatabaseAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public async Task<multiplayer_playlist_item[]> GetAllPlaylistItemsAsync(long roo
return (await connection.QueryAsync<multiplayer_playlist_item>("SELECT * FROM multiplayer_playlist_items WHERE room_id = @RoomId", new { RoomId = roomId })).ToArray();
}

public async Task<BeatmapUpdates> GetUpdatedBeatmapSets(int? lastQueueId, int limit = 50)
public async Task<BeatmapUpdates> GetUpdatedBeatmapSetsAsync(int? lastQueueId, int limit = 50)
{
var connection = await getConnectionAsync();

Expand All @@ -306,7 +306,7 @@ public async Task<BeatmapUpdates> GetUpdatedBeatmapSets(int? lastQueueId, int li
return new BeatmapUpdates(Array.Empty<int>(), lastEntry?.queue_id ?? 0);
}

public async Task MarkScoreHasReplay(Score score)
public async Task MarkScoreHasReplayAsync(Score score)
{
var connection = await getConnectionAsync();

Expand Down Expand Up @@ -347,7 +347,7 @@ public async Task<bool> IsScoreProcessedAsync(long scoreId)
});
}

public async Task<phpbb_zebra?> GetUserRelation(int userId, int zebraId)
public async Task<phpbb_zebra?> GetUserRelationAsync(int userId, int zebraId)
{
var connection = await getConnectionAsync();

Expand All @@ -358,7 +358,17 @@ public async Task<bool> IsScoreProcessedAsync(long scoreId)
});
}

public async Task<bool> GetUserAllowsPMs(int userId)
public async Task<IEnumerable<phpbb_zebra>> GetUserFriendsAsync(int userId)
{
var connection = await getConnectionAsync();

return await connection.QueryAsync<phpbb_zebra>("SELECT * FROM `phpbb_zebra` WHERE `user_id` = @UserId AND `friend` = 1", new
{
UserId = userId
});
}

public async Task<bool> GetUserAllowsPMsAsync(int userId)
{
var connection = await getConnectionAsync();

Expand Down Expand Up @@ -427,7 +437,7 @@ public async Task<IEnumerable<multiplayer_room>> GetActiveDailyChallengeRoomsAsy
new { scoreId = scoreId });
}

public async Task<IEnumerable<SoloScore>> GetPassingScoresForPlaylistItem(long playlistItemId, ulong afterScoreId = 0)
public async Task<IEnumerable<SoloScore>> GetPassingScoresForPlaylistItemAsync(long playlistItemId, ulong afterScoreId = 0)
{
var connection = await getConnectionAsync();

Expand Down
14 changes: 8 additions & 6 deletions osu.Server.Spectator/Database/IDatabaseAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,13 @@ public interface IDatabaseAccess : IDisposable
/// <param name="lastQueueId">A queue ID to fetch updated items since</param>
/// <param name="limit">Maximum number of entries to return. Defaults to 50.</param>
/// <returns>Update metadata.</returns>
Task<BeatmapUpdates> GetUpdatedBeatmapSets(int? lastQueueId, int limit = 50);
Task<BeatmapUpdates> GetUpdatedBeatmapSetsAsync(int? lastQueueId, int limit = 50);

/// <summary>
/// Mark a score as having a replay available.
/// </summary>
/// <param name="score">The score to mark.</param>
Task MarkScoreHasReplay(Score score);
Task MarkScoreHasReplayAsync(Score score);

/// <summary>
/// Retrieves the <see cref="SoloScore"/> for a given score token. Will return null while the score has not yet been submitted.
Expand All @@ -152,20 +152,22 @@ public interface IDatabaseAccess : IDisposable
/// <summary>
/// Returns information about if the user with the supplied <paramref name="zebraId"/> has been added as a friend or blocked by the user with the supplied <paramref name="userId"/>.
/// </summary>
Task<phpbb_zebra?> GetUserRelation(int userId, int zebraId);
Task<phpbb_zebra?> GetUserRelationAsync(int userId, int zebraId);

Task<IEnumerable<phpbb_zebra>> GetUserFriendsAsync(int userId);

/// <summary>
/// Returns <see langword="true"/> if the user with the supplied <paramref name="userId"/> allows private messages from people not on their friends list.
/// </summary>
Task<bool> GetUserAllowsPMs(int userId);
Task<bool> GetUserAllowsPMsAsync(int userId);

/// <summary>
/// Returns all available main builds from the lazer release stream.
/// </summary>
Task<IEnumerable<osu_build>> GetAllMainLazerBuildsAsync();

/// <summary>
/// Returns all known platform-specifc lazer builds.
/// Returns all known platform-specific lazer builds.
/// </summary>
Task<IEnumerable<osu_build>> GetAllPlatformSpecificLazerBuildsAsync();

Expand Down Expand Up @@ -196,7 +198,7 @@ public interface IDatabaseAccess : IDisposable
/// <param name="playlistItemId">The playlist item.</param>
/// <param name="afterScoreId">An optional score ID to only fetch newer scores.</param>
/// <returns></returns>
Task<IEnumerable<SoloScore>> GetPassingScoresForPlaylistItem(long playlistItemId, ulong afterScoreId = 0);
Task<IEnumerable<SoloScore>> GetPassingScoresForPlaylistItemAsync(long playlistItemId, ulong afterScoreId = 0);

/// <summary>
/// Returns the best score of user with <paramref name="userId"/> on the playlist item with <paramref name="playlistItemId"/>.
Expand Down
52 changes: 52 additions & 0 deletions osu.Server.Spectator/Hubs/Friends/MetadataHubFriendsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using osu.Game.Online.Friends;

Check failure on line 6 in osu.Server.Spectator/Hubs/Friends/MetadataHubFriendsContext.cs

View workflow job for this annotation

GitHub Actions / Unit testing

The type or namespace name 'Friends' does not exist in the namespace 'osu.Game.Online' (are you missing an assembly reference?)
using osu.Server.Spectator.Database;

namespace osu.Server.Spectator.Hubs.Friends
{
public class MetadataHubFriendsContext<THub, T>
where THub : Hub<T>
where T : class, IFriendsClient

Check failure on line 13 in osu.Server.Spectator/Hubs/Friends/MetadataHubFriendsContext.cs

View workflow job for this annotation

GitHub Actions / Unit testing

The type or namespace name 'IFriendsClient' could not be found (are you missing a using directive or an assembly reference?)
{
private readonly IDatabaseFactory databaseFactory;

public MetadataHubFriendsContext(IHubContext<THub> context, IDatabaseFactory databaseFactory)
{
this.databaseFactory = databaseFactory;

Clients = context.Clients;
Groups = context.Groups;
}

public async Task OnConnectedAsync(ClientState state)
{
using (var db = databaseFactory.GetInstance())
{
foreach (var friend in await db.GetUserFriendsAsync(state.UserId))
await Groups.AddToGroupAsync(state.ConnectionId, friend_presence_watchers(friend.zebra_id));
}

await Clients.Group(friend_presence_watchers(state.UserId)).SendAsync(nameof(IFriendsClient.FriendConnected), state.UserId);
}

public async Task OnDisconnectedAsync(ClientState state)
{
using (var db = databaseFactory.GetInstance())
{
foreach (var friend in await db.GetUserFriendsAsync(state.UserId))
await Groups.RemoveFromGroupAsync(state.ConnectionId, friend_presence_watchers(friend.zebra_id));
}

await Clients.Group(friend_presence_watchers(state.UserId)).SendAsync(nameof(IFriendsClient.FriendDisconnected), state.UserId);
}

private static string friend_presence_watchers(int userId) => $"friends:online-presence-watchers:{userId}";

public IHubClients Clients { get; }
public IGroupManager Groups { get; }
}
}
2 changes: 1 addition & 1 deletion osu.Server.Spectator/Hubs/Metadata/MetadataBroadcaster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private async void pollForChanges(object? sender, ElapsedEventArgs args)
{
using (var db = databaseFactory.GetInstance())
{
var updates = await db.GetUpdatedBeatmapSets(lastQueueId);
var updates = await db.GetUpdatedBeatmapSetsAsync(lastQueueId);

lastQueueId = updates.LastProcessedQueueID;
logger.LogInformation("Polled beatmap changes up to last queue id {lastProcessedQueueID}", updates.LastProcessedQueueID);
Expand Down
Loading
Loading