Skip to content

Commit 0e8f35e

Browse files
committed
Created Nativ Websocket support
this is an test implementation for the dev build with help of codex from chatgpt
1 parent 2399d3b commit 0e8f35e

3 files changed

Lines changed: 283 additions & 7 deletions

File tree

Models/DeviceModel.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,31 @@ public async Task<bool> Delete()
4040
/// <param name="clientToken"></param>
4141
public async Task SendNotifications(GotifyMessage iGotifyMessage, WebsocketClient webSock)
4242
{
43+
await SendNotifications(iGotifyMessage, webSock.Url.ToString(), webSock.Name ?? "");
44+
}
45+
46+
/// <summary>
47+
/// Send the passed notification from a native websocket context
48+
/// </summary>
49+
public async Task SendNotifications(GotifyMessage iGotifyMessage, string wsUrl, string clientToken)
50+
{
51+
if (string.IsNullOrWhiteSpace(clientToken))
52+
{
53+
Console.WriteLine("ClientToken for sending notification is empty.");
54+
return;
55+
}
56+
4357
var title = iGotifyMessage.title;
4458
var msg = iGotifyMessage.message;
4559

46-
var protocol = webSock.Url.ToString().Contains("ws://") ? "http://" : "https://";
47-
var gotifyServerUrl = webSock.Url.ToString().Replace("ws://", "").Replace("wss://", "").Replace("\"", "")
60+
var protocol = wsUrl.Contains("ws://") ? "http://" : "https://";
61+
var gotifyServerUrl = wsUrl.Replace("ws://", "").Replace("wss://", "").Replace("\"", "")
4862
.Split("/stream");
4963
var imageUrl = gotifyServerUrl.Length > 0
50-
? $"{protocol}{gotifyServerUrl[0]}$$${iGotifyMessage.appid}$$${webSock.Name}"
64+
? $"{protocol}{gotifyServerUrl[0]}$$${iGotifyMessage.appid}$$${clientToken}"
5165
: "";
5266

53-
var usr = await DatabaseService.GetUser(webSock.Name!);
67+
var usr = await DatabaseService.GetUser(clientToken);
5468

5569
if (usr.Uid == 0)
5670
{
@@ -62,4 +76,4 @@ public async Task SendNotifications(GotifyMessage iGotifyMessage, WebsocketClien
6276
iGotifyMessage.priority);
6377
Console.WriteLine(response != null ? JsonConvert.SerializeObject(response) : "Notification response is null");
6478
}
65-
}
79+
}

Services/GotifySocketService.cs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Net.Sockets;
22
using System.Net.WebSockets;
3+
using System.Collections.Concurrent;
34
using iGotify_Notification_Assist.Models;
45
using SecNtfyNuGet;
56

@@ -13,6 +14,14 @@ public class GotifySocketService
1314

1415
// Data structure for tracking threads and WebSocket connections
1516
private static List<ThreadSocket>? _threadSockets;
17+
private static readonly ConcurrentDictionary<string, NativeSocketRuntime> _nativeSockets = new();
18+
19+
private sealed class NativeSocketRuntime
20+
{
21+
public required CancellationTokenSource Cts { get; init; }
22+
public required Task RunnerTask { get; init; }
23+
public required WebSockClientNative Client { get; init; }
24+
}
1625

1726
public static GotifySocketService getInstance()
1827
{
@@ -62,6 +71,8 @@ public static void KillWsThread(string clientToken)
6271
_threadSockets.Remove(threadSocket);
6372
}
6473
}
74+
75+
StopNativeSocket(clientToken);
6576
}
6677

6778
public static void KillAllWsThread()
@@ -94,6 +105,11 @@ public static void KillAllWsThread()
94105

95106
_threadSockets.Clear();
96107
}
108+
109+
foreach (var clientToken in _nativeSockets.Keys)
110+
{
111+
StopNativeSocket(clientToken);
112+
}
97113
}
98114

99115
public static void StartWsThread(string gotifyServerUrl, string clientToken)
@@ -134,6 +150,30 @@ public static void StartWsThread(Users user)
134150
Console.WriteLine($"Client: {user.ClientToken} already connected! Skipping...");
135151
}
136152

153+
public static void StartNativeWsTask(Users user)
154+
{
155+
if (_nativeSockets.ContainsKey(user.ClientToken))
156+
{
157+
Console.WriteLine($"Client: {user.ClientToken} already connected (native)! Skipping...");
158+
return;
159+
}
160+
161+
var cts = new CancellationTokenSource();
162+
var nativeClient = new WebSockClientNative();
163+
var task = Task.Run(() => nativeClient.RunAsync(user, cts.Token), cts.Token);
164+
165+
if (!_nativeSockets.TryAdd(user.ClientToken, new NativeSocketRuntime
166+
{
167+
Cts = cts,
168+
RunnerTask = task,
169+
Client = nativeClient
170+
}))
171+
{
172+
cts.Cancel();
173+
cts.Dispose();
174+
}
175+
}
176+
137177
private static void StartWsConn(ThreadSocket threadSocket, Users user)
138178
{
139179
while (!threadSocket.cts!.IsCancellationRequested)
@@ -315,7 +355,8 @@ private async void StartConnection(List<Users> userList, string secntfyUrl)
315355
Console.WriteLine($"Is SecNtfy Server - Url available: {isSecNtfyAvailable}");
316356
Console.WriteLine($"Client - Token: {user.ClientToken}");
317357

318-
StartWsThread(user);
358+
// StartWsThread(user); // legacy websocket.client implementation
359+
StartNativeWsTask(user);
319360
}
320361
}
321362

@@ -325,4 +366,25 @@ private async void StartDelayedConnection(List<Users> userList, string secntfyUr
325366
Console.WriteLine("Reconnecting...");
326367
StartConnection(userList, secntfyUrl);
327368
}
328-
}
369+
370+
private static void StopNativeSocket(string clientToken)
371+
{
372+
if (!_nativeSockets.TryRemove(clientToken, out var runtime))
373+
return;
374+
375+
try
376+
{
377+
runtime.Cts.Cancel();
378+
runtime.Client.StopAsync().GetAwaiter().GetResult();
379+
runtime.RunnerTask.Wait(millisecondsTimeout: 500);
380+
}
381+
catch (Exception e)
382+
{
383+
Console.WriteLine(e);
384+
}
385+
finally
386+
{
387+
runtime.Cts.Dispose();
388+
}
389+
}
390+
}

Services/WebSockClientNative.cs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
using System.Net.WebSockets;
2+
using System.Text;
3+
using iGotify_Notification_Assist.Models;
4+
using Newtonsoft.Json;
5+
6+
namespace iGotify_Notification_Assist.Services;
7+
8+
public sealed class WebSockClientNative
9+
{
10+
private ClientWebSocket? _socket;
11+
private volatile bool _isStopped;
12+
13+
public async Task RunAsync(Users user, CancellationToken cancellationToken)
14+
{
15+
var wsUrl = BuildWsUrl(user);
16+
var reconnectDelaySeconds = 1;
17+
18+
while (!cancellationToken.IsCancellationRequested && !_isStopped)
19+
{
20+
try
21+
{
22+
using var socket = CreateSocket(user);
23+
_socket = socket;
24+
25+
Console.WriteLine($"Client connecting (native): {user.ClientToken}");
26+
await socket.ConnectAsync(new Uri(wsUrl), cancellationToken);
27+
Console.WriteLine($"Client connected (native): {user.ClientToken}");
28+
29+
reconnectDelaySeconds = 1;
30+
await ReceiveLoopAsync(socket, wsUrl, user.ClientToken, cancellationToken);
31+
}
32+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested || _isStopped)
33+
{
34+
break;
35+
}
36+
catch (WebSocketException wse)
37+
{
38+
if (wse.Message.Contains("401"))
39+
{
40+
Console.WriteLine(
41+
$"ClientToken: {user.ClientToken} is not authorized and returned a 401 Unauthorized error! Skipping reconnection...");
42+
break;
43+
}
44+
45+
Console.WriteLine(
46+
$"Unable to connect or connection aborted for clientToken: {user.ClientToken}. {wse.Message}");
47+
}
48+
catch (Exception ex)
49+
{
50+
Console.WriteLine($"Unexpected websocket error for clientToken: {user.ClientToken}. {ex.Message}");
51+
}
52+
finally
53+
{
54+
_socket = null;
55+
}
56+
57+
if (cancellationToken.IsCancellationRequested || _isStopped)
58+
break;
59+
60+
var jitterMs = Random.Shared.Next(250, 1250);
61+
var delay = TimeSpan.FromSeconds(reconnectDelaySeconds) + TimeSpan.FromMilliseconds(jitterMs);
62+
63+
Console.WriteLine(
64+
$"WebSocket reconnect for clientToken: {user.ClientToken} in {Math.Round(delay.TotalSeconds, 1)}s");
65+
66+
try
67+
{
68+
await Task.Delay(delay, cancellationToken);
69+
}
70+
catch (OperationCanceledException)
71+
{
72+
break;
73+
}
74+
75+
reconnectDelaySeconds = Math.Min(reconnectDelaySeconds * 2, 30);
76+
}
77+
78+
Console.WriteLine($"Client disconnected (native): {user.ClientToken}");
79+
}
80+
81+
public async Task StopAsync()
82+
{
83+
_isStopped = true;
84+
85+
if (_socket == null)
86+
return;
87+
88+
try
89+
{
90+
if (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.CloseReceived)
91+
{
92+
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closing",
93+
CancellationToken.None);
94+
}
95+
else
96+
{
97+
_socket.Abort();
98+
}
99+
}
100+
catch
101+
{
102+
_socket.Abort();
103+
}
104+
}
105+
106+
private static ClientWebSocket CreateSocket(Users user)
107+
{
108+
var socket = new ClientWebSocket();
109+
110+
if (string.IsNullOrWhiteSpace(user.Headers))
111+
return socket;
112+
113+
List<CustomHeaders>? customHeaders;
114+
try
115+
{
116+
customHeaders = JsonConvert.DeserializeObject<List<CustomHeaders>>(user.Headers);
117+
}
118+
catch
119+
{
120+
customHeaders = null;
121+
}
122+
123+
if (customHeaders == null)
124+
return socket;
125+
126+
foreach (var header in customHeaders)
127+
{
128+
if (string.IsNullOrWhiteSpace(header.Key) || string.IsNullOrWhiteSpace(header.Value))
129+
continue;
130+
131+
socket.Options.SetRequestHeader(header.Key, header.Value);
132+
}
133+
134+
return socket;
135+
}
136+
137+
private static string BuildWsUrl(Users user)
138+
{
139+
var socket = user.GotifyUrl.Contains("http://") ? "ws" : "wss";
140+
var gotifyServerUrl = user.GotifyUrl.Replace("http://", "").Replace("https://", "").Replace("\"", "");
141+
return $"{socket}://{gotifyServerUrl}/stream?token={user.ClientToken}";
142+
}
143+
144+
private static async Task ReceiveLoopAsync(ClientWebSocket socket, string wsUrl, string clientToken,
145+
CancellationToken cancellationToken)
146+
{
147+
var buffer = new byte[8 * 1024];
148+
149+
while (!cancellationToken.IsCancellationRequested && socket.State == WebSocketState.Open)
150+
{
151+
using var ms = new MemoryStream();
152+
WebSocketReceiveResult result;
153+
154+
do
155+
{
156+
result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
157+
158+
if (result.MessageType == WebSocketMessageType.Close)
159+
{
160+
if (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseReceived)
161+
{
162+
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server closed connection",
163+
cancellationToken);
164+
}
165+
166+
return;
167+
}
168+
169+
ms.Write(buffer, 0, result.Count);
170+
} while (!result.EndOfMessage);
171+
172+
var rawMessage = Encoding.UTF8.GetString(ms.ToArray());
173+
var message = rawMessage.Replace("client::display", "clientdisplay")
174+
.Replace("client::notification", "clientnotification")
175+
.Replace("android::action", "androidaction");
176+
177+
if (Environments.isLogEnabled)
178+
Console.WriteLine("Message converted: " + message);
179+
180+
GotifyMessage? gm;
181+
try
182+
{
183+
gm = JsonConvert.DeserializeObject<GotifyMessage>(message);
184+
}
185+
catch
186+
{
187+
gm = null;
188+
}
189+
190+
if (gm == null)
191+
{
192+
Console.WriteLine("GotifyMessage is null");
193+
continue;
194+
}
195+
196+
Console.WriteLine($"WS Instance from (native): {clientToken}");
197+
await new DeviceModel().SendNotifications(gm, wsUrl, clientToken);
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)