Skip to content

Commit 93cce15

Browse files
committed
feat: add POST /PiShock/Operate endpoint and health checks
- Add Operate endpoint to PsWebApiController matching the ps.pishock.com non-legacy operate API (same share code based auth, same control flow) - Add HealthWebApiController with /Health/Check (200) and /Health/Server (204) endpoints to satisfy apps that health-check the PiShock API - Register health controller in InterceptionService
1 parent 18846bb commit 93cce15

3 files changed

Lines changed: 114 additions & 2 deletions

File tree

Interception/InterceptionService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ public async Task StartAsync()
3636

3737
var operateController = ActivatorUtilities.CreateInstance<DoWebApiController>(serviceProvider);
3838
var psController = ActivatorUtilities.CreateInstance<PsWebApiController>(serviceProvider);
39+
var healthController = new HealthWebApiController();
3940

4041
_server = new WebServer(o => o
4142
.WithUrlPrefix($"https://*:{port}/")
4243
.WithMode(HttpListenerMode.EmbedIO)
4344
.WithCertificate(cert))
4445
.WithWebApi("/api", m => m.WithController(() => operateController))
4546
.WithWebApi("/PiShock", m => m.WithController(() => psController))
47+
.WithWebApi("/Health", m => m.WithController(() => healthController))
4648
;
4749

4850
_ = _server.RunAsync();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Text;
2+
using EmbedIO;
3+
using EmbedIO.Routing;
4+
using EmbedIO.WebApi;
5+
6+
namespace OpenShock.Desktop.Modules.Interception.Server;
7+
8+
public sealed class HealthWebApiController : WebApiController
9+
{
10+
[Route(HttpVerbs.Get, "/Check")]
11+
public async Task Check()
12+
{
13+
HttpContext.Response.StatusCode = 200;
14+
await HttpContext.SendStringAsync("OK", "text/plain", Encoding.UTF8);
15+
}
16+
17+
[Route(HttpVerbs.Get, "/Server")]
18+
public Task Server()
19+
{
20+
HttpContext.Response.StatusCode = 204;
21+
return Task.CompletedTask;
22+
}
23+
}

Interception/Server/PsWebApiController.cs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,110 @@
55
using EmbedIO.WebApi;
66
using Microsoft.Extensions.Logging;
77
using OpenShock.Desktop.ModuleBase.Api;
8+
using OpenShock.Desktop.ModuleBase.Models;
89

910
namespace OpenShock.Desktop.Modules.Interception.Server;
1011

1112
public sealed class PsWebApiController : WebApiController
1213
{
14+
private static readonly JsonSerializerOptions JsonOptions = new()
15+
{
16+
PropertyNameCaseInsensitive = true
17+
};
18+
1319
private readonly ILogger<PsWebApiController> _logger;
20+
private readonly IOpenShockControl _openShockControl;
1421
private readonly IOpenShockData _openShockData;
1522
private readonly InterceptionService _service;
1623

17-
public PsWebApiController(InterceptionService service, IOpenShockData openShockData,
18-
ILogger<PsWebApiController> logger)
24+
public PsWebApiController(InterceptionService service, IOpenShockControl openShockControl,
25+
IOpenShockData openShockData, ILogger<PsWebApiController> logger)
1926
{
2027
_service = service;
28+
_openShockControl = openShockControl;
2129
_openShockData = openShockData;
2230
_logger = logger;
2331
}
2432

33+
[Route(HttpVerbs.Post, "/Operate")]
34+
public async Task Operate()
35+
{
36+
using var reader = new StreamReader(HttpContext.Request.InputStream);
37+
var body = await reader.ReadToEndAsync();
38+
39+
PiShockRequest? request;
40+
try
41+
{
42+
request = JsonSerializer.Deserialize<PiShockRequest>(body, JsonOptions);
43+
}
44+
catch
45+
{
46+
_logger.LogError("Error parsing JSON body: {Body}", body);
47+
HttpContext.Response.StatusCode = 400;
48+
await HttpContext.SendStringAsync("Invalid JSON", "text/plain", Encoding.UTF8);
49+
return;
50+
}
51+
52+
if (request?.Code == null)
53+
{
54+
_logger.LogError("Missing share code in request: {Body}", body);
55+
HttpContext.Response.StatusCode = 400;
56+
await HttpContext.SendStringAsync("Missing share code", "text/plain", Encoding.UTF8);
57+
return;
58+
}
59+
60+
if (!_service.Config.ShareCodeMappings.TryGetValue(request.Code, out var mapping))
61+
{
62+
_logger.LogError("Share code not mapped to any shocker: {Code}", request.Code);
63+
await HttpContext.SendStringAsync("This code doesn't exist.", "text/plain", Encoding.UTF8);
64+
return;
65+
}
66+
67+
if (mapping.ShockerIds.Count == 0)
68+
{
69+
_logger.LogError("Share code has no shockers configured: {Code}", request.Code);
70+
await HttpContext.SendStringAsync("This code doesn't exist.", "text/plain", Encoding.UTF8);
71+
return;
72+
}
73+
74+
var controlType = request.Op switch
75+
{
76+
0 => ControlType.Shock,
77+
1 => ControlType.Vibrate,
78+
2 => ControlType.Sound,
79+
_ => ControlType.Vibrate
80+
};
81+
82+
var durationMs = (ushort)Math.Clamp(request.Duration * 1000, mapping.MinDuration * 1000, mapping.MaxDuration * 1000);
83+
var intensity = (byte)Math.Clamp(request.Intensity, mapping.MinIntensity, mapping.MaxIntensity);
84+
85+
if (request.Intensity <= 0) controlType = ControlType.Stop;
86+
87+
var controls = mapping.ShockerIds.Select(id => new ShockerControl
88+
{
89+
Id = id,
90+
Type = controlType,
91+
Intensity = intensity,
92+
Duration = durationMs
93+
}).ToArray();
94+
95+
var customName = request.Name ?? request.Username ?? "PiShock Interception";
96+
97+
try
98+
{
99+
await _openShockControl.Control(controls, customName);
100+
_logger.LogInformation(
101+
"PiShock Ps API Operate: {ControlType} {Intensity}% for {Duration}s on {ShockerCount} shocker(s) by {Name}",
102+
controlType, intensity, durationMs / 1000.0, controls.Length, customName);
103+
await HttpContext.SendStringAsync("Operation Succeeded.", "text/plain", Encoding.UTF8);
104+
}
105+
catch (Exception ex)
106+
{
107+
HttpContext.Response.StatusCode = 500;
108+
await HttpContext.SendStringAsync(ex.Message, "text/plain", Encoding.UTF8);
109+
}
110+
}
111+
25112
[Route(HttpVerbs.Get, "/GetUserDevices")]
26113
public async Task GetUserDevices()
27114
{

0 commit comments

Comments
 (0)