Skip to content

Commit 18846bb

Browse files
committed
feat: min/max settings, multiple shockers per share code, and ps.pishock.com API endpoints
- Add ShareCodeMapping class with MinIntensity, MaxIntensity, MinDuration, MaxDuration fields - Support multiple shocker IDs per share code mapping (List<Guid>) - Update DoWebApiController to use per-mapping limits and send controls to all mapped shockers - Add PsWebApiController handling GetUserDevices, GetShareCodesByOwner, GetShockersByShareIds - Add ps.pishock.com as SAN in server certificate - Add ps.pishock.com to hosts file redirect - Update Blazor UI with multi-select shockers and min/max configuration fields
1 parent ca60cee commit 18846bb

8 files changed

Lines changed: 329 additions & 57 deletions

File tree

Interception/Certificates/CertificateManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ private static async Task<X509Certificate2> LoadOrCreateServerCert(string path,
107107

108108
var sanBuilder = new SubjectAlternativeNameBuilder();
109109
sanBuilder.AddDnsName("do.pishock.com");
110+
sanBuilder.AddDnsName("ps.pishock.com");
110111
sanBuilder.AddIpAddress(IPAddress.Loopback);
111112
req.CertificateExtensions.Add(sanBuilder.Build());
112113

Interception/HostsFile/HostsFileManager.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ namespace OpenShock.Desktop.Modules.Interception.HostsFile;
66
public sealed class HostsFileManager
77
{
88
private const string HostsPath = @"C:\Windows\System32\drivers\etc\hosts";
9-
private const string HostEntry = "127.0.0.1 do.pishock.com";
109
private const string Marker = "# OpenShock Interception";
1110

11+
private static readonly string[] HostEntries =
12+
[
13+
$"127.0.0.1 do.pishock.com {Marker}",
14+
$"127.0.0.1 ps.pishock.com {Marker}"
15+
];
16+
1217
public bool IsEnabled { get; private set; }
1318

1419
public async Task EnableAsync()
1520
{
1621
if (IsEnabled) return;
17-
var line = $"{HostEntry} {Marker}";
18-
await RunElevatedHostsCommand($"add \"{line}\"");
22+
await RunElevatedHostsCommand("add");
1923
IsEnabled = true;
2024
await FlushDns();
2125
}
@@ -44,15 +48,16 @@ public async Task DetectCurrentState()
4448
private static async Task RunElevatedHostsCommand(string action)
4549
{
4650
string script;
47-
if (action.StartsWith("add"))
51+
if (action == "add")
4852
{
49-
var line = action.Substring(4).Trim();
53+
var linesArray = string.Join(",", HostEntries.Select(e => $"'{e}'"));
5054
script = string.Join("\n",
51-
$"$line = {line};",
55+
$"$lines = @({linesArray});",
5256
$"$hostsPath = '{HostsPath}';",
5357
"$content = Get-Content $hostsPath -Raw -ErrorAction SilentlyContinue;",
5458
"if ($content -notmatch 'OpenShock Interception') {",
55-
" Add-Content -Path $hostsPath -Value \"`n$line\" -NoNewline:$false",
59+
" $toAdd = \"`n\" + ($lines -join \"`n\");",
60+
" Add-Content -Path $hostsPath -Value $toAdd -NoNewline:$false",
5661
"}");
5762
}
5863
else
@@ -104,4 +109,4 @@ private static async Task FlushDns()
104109
// Best-effort DNS flush
105110
}
106111
}
107-
}
112+
}

Interception/InterceptionConfig.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using OpenShock.Desktop.Modules.Interception.Server;
2+
13
namespace OpenShock.Desktop.Modules.Interception;
24

35
public sealed class InterceptionConfig
46
{
57
public ushort Port { get; set; } = 443;
68
public bool AutoStart { get; set; } = true;
7-
public Dictionary<string, Guid> ShareCodeMappings { get; set; } = new();
8-
}
9+
public Dictionary<string, ShareCodeMapping> ShareCodeMappings { get; set; } = new();
10+
}

Interception/InterceptionService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ public async Task StartAsync()
3535
var cert = certManager.ServerCertificate;
3636

3737
var operateController = ActivatorUtilities.CreateInstance<DoWebApiController>(serviceProvider);
38+
var psController = ActivatorUtilities.CreateInstance<PsWebApiController>(serviceProvider);
3839

3940
_server = new WebServer(o => o
4041
.WithUrlPrefix($"https://*:{port}/")
4142
.WithMode(HttpListenerMode.EmbedIO)
4243
.WithCertificate(cert))
4344
.WithWebApi("/api", m => m.WithController(() => operateController))
45+
.WithWebApi("/PiShock", m => m.WithController(() => psController))
4446
;
4547

4648
_ = _server.RunAsync();
@@ -60,4 +62,4 @@ public async Task UpdateConfig(Action<InterceptionConfig> update)
6062
update(Config);
6163
await moduleConfig.Save();
6264
}
63-
}
65+
}

Interception/Server/DoWebApiController.cs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public async Task Operate()
5656
return;
5757
}
5858

59-
if (!_service.Config.ShareCodeMappings.TryGetValue(request.Code, out var shockerId))
59+
if (!_service.Config.ShareCodeMappings.TryGetValue(request.Code, out var mapping))
6060
{
6161
_logger.LogError("Share code not mapped to any shocker: {Code}", request.Code);
6262
HttpContext.Response.StatusCode = 404;
@@ -65,6 +65,15 @@ await HttpContext.SendStringAsync("Share code not mapped to any shocker", "text/
6565
return;
6666
}
6767

68+
if (mapping.ShockerIds.Count == 0)
69+
{
70+
_logger.LogError("Share code has no shockers configured: {Code}", request.Code);
71+
HttpContext.Response.StatusCode = 404;
72+
await HttpContext.SendStringAsync("Share code has no shockers configured", "text/plain",
73+
Encoding.UTF8);
74+
return;
75+
}
76+
6877
var controlType = request.Op switch
6978
{
7079
0 => ControlType.Shock,
@@ -73,30 +82,27 @@ await HttpContext.SendStringAsync("Share code not mapped to any shocker", "text/
7382
_ => ControlType.Vibrate
7483
};
7584

76-
var durationMs = (ushort)Math.Clamp(request.Duration * 1000, 300, 30000);
77-
var intensity = (byte)Math.Clamp(request.Intensity, 1, 100);
85+
var durationMs = (ushort)Math.Clamp(request.Duration * 1000, mapping.MinDuration * 1000, mapping.MaxDuration * 1000);
86+
var intensity = (byte)Math.Clamp(request.Intensity, mapping.MinIntensity, mapping.MaxIntensity);
7887

7988
if (request.Intensity <= 0) controlType = ControlType.Stop;
8089

81-
var controls = new[]
90+
var controls = mapping.ShockerIds.Select(id => new ShockerControl
8291
{
83-
new ShockerControl
84-
{
85-
Id = shockerId,
86-
Type = controlType,
87-
Intensity = intensity,
88-
Duration = durationMs
89-
}
90-
};
92+
Id = id,
93+
Type = controlType,
94+
Intensity = intensity,
95+
Duration = durationMs
96+
}).ToArray();
9197

9298
var customName = request.Name ?? request.Username ?? "PiShock Interception";
9399

94100
try
95101
{
96102
await _openShockControl.Control(controls, customName);
97103
_logger.LogInformation(
98-
"PiShock Do API: control command: {ControlType} {Intensity}% for {Duration}s on shocker {ShockerId} by {Name}",
99-
controlType, intensity, durationMs / 1000.0, shockerId, customName);
104+
"PiShock Do API: control command: {ControlType} {Intensity}% for {Duration}s on {ShockerCount} shocker(s) by {Name}",
105+
controlType, intensity, durationMs / 1000.0, controls.Length, customName);
100106
await HttpContext.SendStringAsync(
101107
JsonSerializer.Serialize(new { success = true, message = "Operation Succeeded." }),
102108
"application/json", Encoding.UTF8);
@@ -137,7 +143,7 @@ public async Task GetShockerInfo()
137143
return;
138144
}
139145

140-
if (!_service.Config.ShareCodeMappings.TryGetValue(request.Code, out var shockerId))
146+
if (!_service.Config.ShareCodeMappings.TryGetValue(request.Code, out var mapping))
141147
{
142148
_logger.LogError("Share code not mapped to any shocker: {Code}", request.Code);
143149
HttpContext.Response.StatusCode = 404;
@@ -147,15 +153,15 @@ public async Task GetShockerInfo()
147153

148154
var info = new
149155
{
150-
clientId = shockerId,
156+
clientId = mapping.ShockerIds.FirstOrDefault(),
151157
name = $"Shocker ({request.Code})",
152-
maxIntensity = 100,
153-
maxDuration = 15,
158+
maxIntensity = (int)mapping.MaxIntensity,
159+
maxDuration = (int)mapping.MaxDuration,
154160
online = true
155161
};
156162

157163
await HttpContext.SendStringAsync(
158164
JsonSerializer.Serialize(info),
159165
"application/json", Encoding.UTF8);
160166
}
161-
}
167+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using EmbedIO;
4+
using EmbedIO.Routing;
5+
using EmbedIO.WebApi;
6+
using Microsoft.Extensions.Logging;
7+
using OpenShock.Desktop.ModuleBase.Api;
8+
9+
namespace OpenShock.Desktop.Modules.Interception.Server;
10+
11+
public sealed class PsWebApiController : WebApiController
12+
{
13+
private readonly ILogger<PsWebApiController> _logger;
14+
private readonly IOpenShockData _openShockData;
15+
private readonly InterceptionService _service;
16+
17+
public PsWebApiController(InterceptionService service, IOpenShockData openShockData,
18+
ILogger<PsWebApiController> logger)
19+
{
20+
_service = service;
21+
_openShockData = openShockData;
22+
_logger = logger;
23+
}
24+
25+
[Route(HttpVerbs.Get, "/GetUserDevices")]
26+
public async Task GetUserDevices()
27+
{
28+
_logger.LogInformation("PiShock Ps API: GetUserDevices request");
29+
30+
var shareCodeIndex = 0;
31+
var devices = new List<object>();
32+
33+
// Build a synthetic device containing all mapped shockers
34+
var shockers = new List<object>();
35+
foreach (var (shareCode, mapping) in _service.Config.ShareCodeMappings)
36+
{
37+
foreach (var shockerId in mapping.ShockerIds)
38+
{
39+
var shockerInfo = FindShockerInfo(shockerId);
40+
shockers.Add(new
41+
{
42+
shockerId = shareCodeIndex++,
43+
name = shockerInfo?.Name ?? $"Shocker ({shareCode})",
44+
shareCode,
45+
isPaused = false,
46+
maxIntensity = (int)mapping.MaxIntensity,
47+
maxDuration = (int)mapping.MaxDuration
48+
});
49+
}
50+
}
51+
52+
if (shockers.Count > 0)
53+
{
54+
devices.Add(new
55+
{
56+
deviceId = 0,
57+
name = "OpenShock Interception",
58+
shockers
59+
});
60+
}
61+
62+
await HttpContext.SendStringAsync(
63+
JsonSerializer.Serialize(devices),
64+
"application/json", Encoding.UTF8);
65+
}
66+
67+
[Route(HttpVerbs.Get, "/GetShareCodesByOwner")]
68+
public async Task GetShareCodesByOwner()
69+
{
70+
_logger.LogInformation("PiShock Ps API: GetShareCodesByOwner request");
71+
72+
var result = new List<object>();
73+
var shareId = 0;
74+
75+
foreach (var (shareCode, mapping) in _service.Config.ShareCodeMappings)
76+
{
77+
var shockerInfo = mapping.ShockerIds.Count > 0 ? FindShockerInfo(mapping.ShockerIds[0]) : null;
78+
result.Add(new
79+
{
80+
shareCodeId = shareId++,
81+
code = shareCode,
82+
shockerName = shockerInfo?.Name ?? $"Shocker ({shareCode})",
83+
isPaused = false,
84+
maxIntensity = (int)mapping.MaxIntensity,
85+
maxDuration = (int)mapping.MaxDuration,
86+
permissions = new
87+
{
88+
shock = true,
89+
vibrate = true,
90+
sound = true
91+
}
92+
});
93+
}
94+
95+
await HttpContext.SendStringAsync(
96+
JsonSerializer.Serialize(result),
97+
"application/json", Encoding.UTF8);
98+
}
99+
100+
[Route(HttpVerbs.Get, "/GetShockersByShareIds")]
101+
public async Task GetShockersByShareIds()
102+
{
103+
_logger.LogInformation("PiShock Ps API: GetShockersByShareIds request");
104+
105+
var shareIdParams = HttpContext.GetRequestQueryData().GetValues("shareIds") ?? [];
106+
107+
var result = new List<object>();
108+
var shareId = 0;
109+
110+
foreach (var (shareCode, mapping) in _service.Config.ShareCodeMappings)
111+
{
112+
var currentId = shareId++;
113+
if (shareIdParams.Length > 0 && !shareIdParams.Contains(currentId.ToString())) continue;
114+
115+
var shockerInfo = mapping.ShockerIds.Count > 0 ? FindShockerInfo(mapping.ShockerIds[0]) : null;
116+
result.Add(new
117+
{
118+
shareCodeId = currentId,
119+
code = shareCode,
120+
shockerName = shockerInfo?.Name ?? $"Shocker ({shareCode})",
121+
isPaused = false,
122+
maxIntensity = (int)mapping.MaxIntensity,
123+
maxDuration = (int)mapping.MaxDuration,
124+
online = true,
125+
permissions = new
126+
{
127+
shock = true,
128+
vibrate = true,
129+
sound = true
130+
}
131+
});
132+
}
133+
134+
await HttpContext.SendStringAsync(
135+
JsonSerializer.Serialize(result),
136+
"application/json", Encoding.UTF8);
137+
}
138+
139+
private ShockerInfo? FindShockerInfo(Guid shockerId)
140+
{
141+
foreach (var hub in _openShockData.Hubs.Value)
142+
{
143+
foreach (var shocker in hub.Shockers)
144+
{
145+
if (shocker.Id == shockerId)
146+
return new ShockerInfo(shocker.Name, hub.Name);
147+
}
148+
}
149+
150+
return null;
151+
}
152+
153+
private sealed record ShockerInfo(string Name, string HubName);
154+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace OpenShock.Desktop.Modules.Interception.Server;
2+
3+
public sealed class ShareCodeMapping
4+
{
5+
public List<Guid> ShockerIds { get; set; } = [];
6+
public byte MinIntensity { get; set; } = 1;
7+
public byte MaxIntensity { get; set; } = 100;
8+
public ushort MinDuration { get; set; } = 1;
9+
public ushort MaxDuration { get; set; } = 15;
10+
}

0 commit comments

Comments
 (0)