Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
34 changes: 26 additions & 8 deletions Desktop/Backend/BackendHubManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,34 @@ private Task OnShockerLogHandler(LogEventArgs logEventArgs)
/// <returns></returns>
public Task Control(IEnumerable<Control> shocks, string? customName = null)
{
var enabledShockers = _configManager.Config.OpenShock.Shockers
.Where(y => y.Value.Enabled &&
_openShockApi.Hubs.Value.Any(x=>
x.Shockers.Any(z => z.Id == y.Key && !z.IsPaused)))
.Select(x => x.Key)
.ToHashSet();

var shocksToSend = shocks.Where(x => enabledShockers.Contains(x.Id));
var shocksToSend = shocks.Where(x => CanControl(x.Id, x.Type));
return _openShockHubClient.Control(shocksToSend, customName);
}

/// <summary>
/// Whether the given shocker may be controlled with the given control type: it must be enabled in config, exist and
/// not be paused, and for shared shockers we must hold the permission matching the control type. Owned shockers are
/// not present in the shared permission map and are always allowed.
/// </summary>
private bool CanControl(Guid shockerId, ControlType type)
{
if (!_configManager.Config.OpenShock.Shockers.TryGetValue(shockerId, out var conf) || !conf.Enabled)
return false;

var shocker = _openShockApi.AllHubs.SelectMany(x => x.Shockers).FirstOrDefault(x => x.Id == shockerId);
if (shocker is null || shocker.IsPaused) return false;

if (!_openShockApi.SharedShockerPermissions.TryGetValue(shockerId, out var permissions)) return true;

return type switch
{
ControlType.Stop => true,
ControlType.Shock => permissions.Shock,
ControlType.Vibrate => permissions.Vibrate,
ControlType.Sound => permissions.Sound,
_ => false
};
}
}

public readonly struct ShockerLogEventArgs
Expand Down
112 changes: 90 additions & 22 deletions Desktop/Backend/OpenShockApi.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using OpenShock.Desktop.Config;
using OpenShock.Desktop.Models;
using OpenShock.Desktop.Models.BaseImpl;
using OpenShock.Desktop.ModuleBase.StableInterfaces;
using OpenShock.Desktop.Utils;
using OpenShock.SDK.CSharp;
using OpenShock.SDK.CSharp.Models;

namespace OpenShock.Desktop.Backend;

Expand Down Expand Up @@ -36,8 +38,33 @@ public void SetupApiClient()
}

public ObservableVariable<IReadOnlyList<IOpenShockHub>> Hubs { get; } = new(ImmutableArray<OpenShockHub>.Empty);

/// <summary>
/// Hubs owned by other users that have shared one or more shockers with us. Kept separate from <see cref="Hubs"/>.
/// Flattened across all owners; see <see cref="SharedOwners"/> for the owner-grouped view used by the UI.
/// </summary>
public ObservableVariable<IReadOnlyList<IOpenShockHub>> SharedHubs { get; } = new(ImmutableArray<OpenShockHub>.Empty);

/// <summary>
/// Shared hubs grouped by the owner that shared them, so the UI can attribute hubs to the person that owns them.
/// </summary>
public ObservableVariable<IReadOnlyList<SharedHubOwner>> SharedOwners { get; } =
new(ImmutableArray<SharedHubOwner>.Empty);

/// <summary>
/// Permissions granted to us per shared shocker. Owned shockers are not present here (they have all permissions).
/// </summary>
public IReadOnlyDictionary<Guid, ShockerPermissions> SharedShockerPermissions => _sharedShockerPermissions;
private volatile IReadOnlyDictionary<Guid, ShockerPermissions> _sharedShockerPermissions =
new Dictionary<Guid, ShockerPermissions>();

/// <summary>
/// All hubs we can control, both owned and shared.
/// </summary>
public IEnumerable<IOpenShockHub> AllHubs => Hubs.Value.Concat(SharedHubs.Value);

public ConcurrentDictionary<Guid, HubStatus> HubStates { get; } = new();

public async Task RefreshHubs()
{
if (Client == null)
Expand All @@ -46,29 +73,40 @@ public async Task RefreshHubs()
throw new Exception("Client is not initialized!");
}
var response = await Client.GetOwnShockers();

response.Switch(success =>
{
Hubs.Value = [..success.Value.Select(x => x.ToSdkHub(this))];

// re-populate config with previous data if present, this also deletes any shockers that are no longer present
var shockerList = new Dictionary<Guid, OpenShockConf.ShockerConf>();
foreach (var shocker in success.Value.SelectMany(x => x.Shockers))
{
var enabled = true;

if (_configManager.Config.OpenShock.Shockers.TryGetValue(shocker.Id, out var confShocker))
{
enabled = confShocker.Enabled;
}

shockerList.Add(shocker.Id, new OpenShockConf.ShockerConf
{
Enabled = enabled
});
}
_configManager.Config.OpenShock.Shockers = shockerList;
_configManager.Save();
SyncShockerConfig();
},
error =>
{
_logger.LogError("We are not authenticated with the OpenShock API!");
// TODO: handle unauthenticated error
});
}

public async Task RefreshSharedHubs()
{
if (Client == null)
{
_logger.LogError("Client is not initialized!");
throw new Exception("Client is not initialized!");
}
var response = await Client.GetSharedShockers();

response.Switch(success =>
{
var owners = success.Value.ToSdkSharedOwners(this);
SharedOwners.Value = owners;
SharedHubs.Value = owners.SelectMany(owner => owner.Hubs).ToArray();

_sharedShockerPermissions = success.Value
.SelectMany(owner => owner.Devices)
.SelectMany(device => device.Shockers)
.ToDictionary(shocker => shocker.Id, shocker => shocker.Permissions);

SyncShockerConfig();
},
error =>
{
Expand All @@ -77,9 +115,39 @@ public async Task RefreshHubs()
});
}

/// <summary>
/// Re-populates the per-shocker config with the currently known owned and shared shockers, preserving the enabled
/// flag for shockers that were already present and dropping shockers that no longer exist. Newly discovered owned
/// shockers default to enabled; newly discovered shared shockers default to disabled so another user's device is
/// never controlled until the user explicitly opts in.
/// </summary>
private void SyncShockerConfig()
{
var shockerList = new Dictionary<Guid, OpenShockConf.ShockerConf>();
foreach (var shockerId in AllHubs.SelectMany(x => x.Shockers).Select(x => x.Id))
{
if (shockerList.ContainsKey(shockerId)) continue;

// Shared shockers are present in the permission map; owned shockers are not.
var enabled = !_sharedShockerPermissions.ContainsKey(shockerId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a way to enable newly shared shockers

When a shared shocker is first discovered, this defaults Enabled to false, but the only enable checkbox I found is in Desktop/Ui/Pages/Dash/Tabs/ShockersTab/ShockersTab.razor and it renders only OpenShockApi.Hubs.Value.SelectMany(...), i.e. owned hubs. In the normal UI flow a newly shared shocker therefore stays disabled forever, so BackendHubManager.CanControl and IsLiveControllable filter it out even when the user has permissions; include shared shockers in the opt-in UI or provide another persisted opt-in path.

Useful? React with 👍 / 👎.

if (_configManager.Config.OpenShock.Shockers.TryGetValue(shockerId, out var confShocker))
enabled = confShocker.Enabled;

shockerList.Add(shockerId, new OpenShockConf.ShockerConf
{
Enabled = enabled
});
}
_configManager.Config.OpenShock.Shockers = shockerList;
_configManager.Save();
}

public void Logout()
{
Hubs.Value = ImmutableArray<OpenShockHub>.Empty;
SharedHubs.Value = ImmutableArray<OpenShockHub>.Empty;
SharedOwners.Value = ImmutableArray<SharedHubOwner>.Empty;
_sharedShockerPermissions = new Dictionary<Guid, ShockerPermissions>();
}

}
15 changes: 15 additions & 0 deletions Desktop/Models/SharedHubOwner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using OpenShock.Desktop.ModuleBase.StableInterfaces;

namespace OpenShock.Desktop.Models;

/// <summary>
/// A user who has shared one or more of their hubs' shockers with the current user, grouped for display so the UI can
/// attribute shared hubs to the person that owns them.
/// </summary>
public sealed class SharedHubOwner
{
public required Guid Id { get; init; }
public required string Name { get; init; }
public Uri? Image { get; init; }
public required IReadOnlyList<IOpenShockHub> Hubs { get; init; }
}
1 change: 1 addition & 0 deletions Desktop/ModuleManager/Implementation/OpenShockData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ namespace OpenShock.Desktop.ModuleManager.Implementation;
public class OpenShockData : IOpenShockData
{
public required IObservableVariable<IReadOnlyList<IOpenShockHub>> Hubs { get; init; }
public required IObservableVariable<IReadOnlyList<IOpenShockHub>> SharedHubs { get; init; }
}
3 changes: 2 additions & 1 deletion Desktop/ModuleManager/Implementation/OpenShockService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public OpenShockService(IServiceProvider serviceProvider)
Control = _controlInstance = new OpenShockControl(backendHubManager, liveControlManager);
Data = new OpenShockData
{
Hubs = openShockApi.Hubs
Hubs = openShockApi.Hubs,
SharedHubs = openShockApi.SharedHubs
};
Api = new OpenShockApiWrapper(openShockApi);
Auth = new OpenShockAuth
Expand Down
1 change: 1 addition & 0 deletions Desktop/Services/AuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public async Task Authenticate()

_logger.LogInformation("Refreshing shockers");
await _apiClient.RefreshHubs();
await _apiClient.RefreshSharedHubs();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve shared-shocker config across login refreshes

On startup or re-auth, SharedHubs is still empty when RefreshHubs() runs, so its SyncShockerConfig() rewrites and saves Config.OpenShock.Shockers with only owned IDs before this newly added shared refresh happens. The subsequent RefreshSharedHubs() call no longer sees any saved Enabled=true entries for shared shockers and re-adds them as default-disabled, so any prior opt-in/manual config for shared shockers is lost on every login; defer the sync/save until both owned and shared responses are loaded or preserve existing shared entries during the owned refresh.

Useful? React with 👍 / 👎.


await _liveControlManager.RefreshConnections();

Expand Down
Loading
Loading