Initial commit

This commit is contained in:
Stone_Red
2024-03-02 19:10:14 +01:00
commit e9927528e9
38 changed files with 1225 additions and 0 deletions
@@ -0,0 +1,63 @@
using StoneRed.NetificationApi.Client;
using StoneRed.NetificationApi.Client.Models;
using StoneRed.NetificationApi.Server;
using StoneRed.NetificationApi.Server.IdentifyUser;
using StoneRed.NetificationApi.Server.Send;
const string userId = "<UserId>";
const string notificationId = "<NotifcationId>";
const string clientId = "<ClientId>";
const string clientSecret = "<ClientSecret>";
Console.WriteLine("Start client");
NotificationApiClient notificationApiClient = new NotificationApiClient("test", clientId, clientSecret, false);
await notificationApiClient.Start();
Console.WriteLine("Client started");
notificationApiClient.RequestNotifications();
notificationApiClient.RequestedNotificationsReceived += (sender, args) =>
{
Console.WriteLine("Requested notifications received");
foreach (NotificationReceivedData notificationReceiveData in args.Notifications)
{
Console.WriteLine($"Notification received: {notificationReceiveData.Id}");
}
};
notificationApiClient.NewNotificationsReceived += (sender, args) =>
{
Console.WriteLine("New notifications received");
foreach (NotificationReceivedData notificationReceiveData in args.Notifications)
{
Console.WriteLine($"New Notification received: {notificationReceiveData.Id}");
}
};
NotificationApiServer notificationApiServer = new NotificationApiServer(clientId, clientSecret, false);
Console.WriteLine("Identify user");
await notificationApiServer.Identify(new IdentifyUserData
{
UserId = userId,
Email = "[email protected]"
});
Console.WriteLine("User identified");
Console.WriteLine("Send notification");
SendNotificationData sendNotificationData = new SendNotificationData
{
NotificationId = notificationId,
User = new NotificationUser
{
Id = userId
}
};
await notificationApiServer.Send(sendNotificationData);
Console.WriteLine("Notification sent");
Console.ReadLine();
Console.WriteLine("Stop client");
await notificationApiClient.Stop();
Console.WriteLine("Client stopped");
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StoneRed.NetificationApi\StoneRed.NetificationApi.csproj" />
</ItemGroup>
</Project>
+31
View File
@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34616.47
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoneRed.NetificationApi", "StoneRed.NetificationApi\StoneRed.NetificationApi.csproj", "{DED64841-1673-47C9-99E4-BD0DC01C1F2E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StoneRed.NetificationApi.Example", "StoneRed.NetificationApi.Example\StoneRed.NetificationApi.Example.csproj", "{8A7CD1F7-78E4-4265-A10A-E116D6639078}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DED64841-1673-47C9-99E4-BD0DC01C1F2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DED64841-1673-47C9-99E4-BD0DC01C1F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DED64841-1673-47C9-99E4-BD0DC01C1F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DED64841-1673-47C9-99E4-BD0DC01C1F2E}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7CD1F7-78E4-4265-A10A-E116D6639078}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7CD1F7-78E4-4265-A10A-E116D6639078}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7CD1F7-78E4-4265-A10A-E116D6639078}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7CD1F7-78E4-4265-A10A-E116D6639078}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {35AE5460-29CB-4E09-A2AA-9055E6FBEF12}
EndGlobalSection
EndGlobal
@@ -0,0 +1,11 @@
namespace StoneRed.NetificationApi.Client.Models;
public class NotificationReceivedData
{
public required string Id { get; set; }
public required bool Seen { get; set; }
public required string Title { get; set; }
public required string RedirectURL { get; set; }
public required string ImageURL { get; set; }
public required DateTime Date { get; set; }
}
@@ -0,0 +1,6 @@
namespace StoneRed.NetificationApi.Client.Models;
public class NotificationsReceivedEventArgs(List<NotificationReceivedData> notifications) : EventArgs
{
public List<NotificationReceivedData> Notifications { get; set; } = notifications;
}
@@ -0,0 +1,97 @@
using StoneRed.NetificationApi.Client.Models;
using StoneRed.NetificationApi.Client.Payloads;
using StoneRed.NetificationApi.Utilities;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Reactive.Linq;
using System.Text.Json;
using System.Web;
using Websocket.Client;
namespace StoneRed.NetificationApi.Client;
public class NotificationApiClient
{
public event EventHandler<NotificationsReceivedEventArgs>? RequestedNotificationsReceived;
public event EventHandler<NotificationsReceivedEventArgs>? NewNotificationsReceived;
private readonly WebsocketClient client;
public NotificationApiClient(string userId, string clientId, string clientSecret, bool secureMode, string baseAddress = "wss://ws.notificationapi.com")
{
UriBuilder uriBuilder = new UriBuilder(baseAddress);
NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query);
query["envId"] = clientId;
query["userId"] = userId;
if (secureMode)
{
query["userIdHash"] = UserIdHasher.Hash(userId, clientSecret);
}
uriBuilder.Query = query.ToString();
client = new WebsocketClient(uriBuilder.Uri);
_ = client.MessageReceived.Subscribe(msg =>
{
Debug.WriteLine("Message:" + msg.Text);
});
_ = client.MessageReceived
.Where(msg => msg.Text is not null)
.Where(msg => WebsocketMessageComparer.Compare(msg.Text, "inapp_web/notifications"))
.Select(msg => WebsocketMessageConverter.ConvertWebsocketMessage<NotificationsReceivedPayload>(msg.Text!))
.Subscribe(msg =>
{
if (msg.Payload is null)
{
return;
}
RequestedNotificationsReceived?.Invoke(this, new NotificationsReceivedEventArgs(msg.Payload.Notifications));
});
_ = client.MessageReceived
.Where(msg => msg.Text is not null)
.Where(msg => WebsocketMessageComparer.Compare(msg.Text, "inapp_web/new_notifications"))
.Select(msg => WebsocketMessageConverter.ConvertWebsocketMessage<NotificationsReceivedPayload>(msg.Text!))
.Subscribe(msg =>
{
if (msg.Payload is null)
{
return;
}
NewNotificationsReceived?.Invoke(this, new NotificationsReceivedEventArgs(msg.Payload.Notifications));
});
}
public Task Start()
{
return client.StartOrFail();
}
public bool RequestNotifications()
{
WebsocketMessage<object> message = new("inapp_web/notifications")
{
Payload = new
{
count = 50
}
};
return client.Send(JsonSerializer.Serialize(message, Configuration.JsonSerializerOptions));
}
public Task Stop()
{
return client.StopOrFail(WebSocketCloseStatus.NormalClosure, "Normal closure");
}
}
@@ -0,0 +1,8 @@
using StoneRed.NetificationApi.Client.Models;
namespace StoneRed.NetificationApi.Client.Payloads;
internal class NotificationsReceivedPayload
{
public required List<NotificationReceivedData> Notifications { get; set; }
}
@@ -0,0 +1,39 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Client;
internal class WebsocketMessage : IWebsocketMessage
{
public required string Route { get; set; }
[SetsRequiredMembers]
public WebsocketMessage(string route)
{
Route = route;
}
public WebsocketMessage()
{
}
}
internal class WebsocketMessage<T> : IWebsocketMessage
{
public required string Route { get; set; }
public T? Payload { get; set; }
[SetsRequiredMembers]
public WebsocketMessage(string route)
{
Route = route;
}
public WebsocketMessage()
{
}
}
internal interface IWebsocketMessage
{
string Route { get; set; }
}
@@ -0,0 +1,28 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace StoneRed.NetificationApi.Server.IdentifyUser;
public class IdentifyUserData
{
[JsonIgnore]
public required string UserId { get; set; }
public string? Email { get; set; }
[JsonPropertyName("number")]
public string? TelephoneNumber { get; set; }
public List<NotificationPushToken>? PushTokens { get; set; }
public List<NotificationWebPushToken>? WebPushTokens { get; set; }
[SetsRequiredMembers]
public IdentifyUserData(string userId)
{
UserId = userId;
}
public IdentifyUserData()
{
}
}
@@ -0,0 +1,7 @@
namespace StoneRed.NetificationApi.Server.IdentifyUser;
public enum NotificationPushProviders
{
FCM,
APM
}
@@ -0,0 +1,22 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.IdentifyUser;
public class NotificationPushToken
{
public required NotificationPushProviders Type { get; set; }
public required string Token { get; set; }
public required NotificationPushTokenDevice Device { get; set; }
[SetsRequiredMembers]
public NotificationPushToken(NotificationPushProviders type, string token, NotificationPushTokenDevice device)
{
Type = type;
Token = token;
Device = device;
}
public NotificationPushToken()
{
}
}
@@ -0,0 +1,30 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace StoneRed.NetificationApi.Server.IdentifyUser;
public class NotificationPushTokenDevice
{
[JsonPropertyName("app_id")]
public string? AppId { get; set; }
[JsonPropertyName("ad_id")]
public string? AdId { get; set; }
[JsonPropertyName("device_id")]
public required string DeviceId { get; set; }
public string? Platform { get; set; }
public string? Manufacturer { get; set; }
public string? Model { get; set; }
[SetsRequiredMembers]
public NotificationPushTokenDevice(string deviceId)
{
DeviceId = deviceId;
}
public NotificationPushTokenDevice()
{
}
}
@@ -0,0 +1,18 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.IdentifyUser;
public class NotificationWebPushToken
{
public required NotificationWebPushTokenSub Sub { get; set; }
[SetsRequiredMembers]
public NotificationWebPushToken(NotificationWebPushTokenSub sub)
{
Sub = sub;
}
public NotificationWebPushToken()
{
}
}
@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.IdentifyUser;
public class NotificationWebPushTokenKeys
{
public required string P256dh { get; set; }
public required string Auth { get; set; }
[SetsRequiredMembers]
public NotificationWebPushTokenKeys(string p256dh, string auth)
{
P256dh = p256dh;
Auth = auth;
}
public NotificationWebPushTokenKeys()
{
}
}
@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.IdentifyUser;
public class NotificationWebPushTokenSub
{
public required string Endpoint { get; set; }
public required NotificationWebPushTokenKeys Keys { get; set; }
[SetsRequiredMembers]
public NotificationWebPushTokenSub(string endpoint, NotificationWebPushTokenKeys keys)
{
Endpoint = endpoint;
Keys = keys;
}
public NotificationWebPushTokenSub()
{
}
}
@@ -0,0 +1,82 @@
using StoneRed.NetificationApi.Server.IdentifyUser;
using StoneRed.NetificationApi.Server.Retract;
using StoneRed.NetificationApi.Server.Send;
using StoneRed.NetificationApi.Server.SetUserPreferences;
using StoneRed.NetificationApi.Utilities;
using System.Net.Http.Json;
using System.Text;
namespace StoneRed.NetificationApi.Server;
public class NotificationApiServer
{
private readonly string clientId;
private readonly string clientSecret;
private readonly bool secureMode;
private readonly HttpClient httpClient;
public NotificationApiServer(string clientId, string clientSecret, bool secureMode, string baseAddress = "https://api.notificationapi.com") : this(new HttpClient(), clientId, clientSecret, secureMode, baseAddress)
{
this.clientId = clientId;
this.clientSecret = clientSecret;
}
public NotificationApiServer(HttpClient httpClient, string clientId, string clientSecret, bool secureMode, string baseAddress = "https://api.notificationapi.com")
{
string authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
httpClient.BaseAddress = new Uri(new Uri(baseAddress), $"{clientId}/");
httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {authToken}");
this.clientId = clientId;
this.clientSecret = clientSecret;
this.secureMode = secureMode;
this.httpClient = httpClient;
}
public async Task<HttpResponseMessage> Send(SendNotificationData sendNotificationData)
{
return await httpClient.PostAsJsonAsync("sender", sendNotificationData, Configuration.JsonSerializerOptions);
}
public async Task<HttpResponseMessage> Retract(RetractNotificationData retractNotificationData)
{
return await httpClient.PostAsJsonAsync("sender/retract", retractNotificationData, Configuration.JsonSerializerOptions);
}
public async Task<HttpResponseMessage> Identify(IdentifyUserData identifyUserData)
{
string authToken;
if (secureMode)
{
string hashedUserId = UserIdHasher.Hash(identifyUserData.UserId, clientSecret);
authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientId}:{identifyUserData.UserId}:{hashedUserId}"));
}
else
{
authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientId}:{identifyUserData.UserId}"));
}
var requestData = new
{
email = identifyUserData.Email,
number = identifyUserData.TelephoneNumber,
};
HttpRequestMessage request = new(HttpMethod.Post, $"users/{identifyUserData.UserId}")
{
Content = JsonContent.Create(requestData, options: Configuration.JsonSerializerOptions),
};
request.Headers.Add("Authorization", $"Basic {authToken}");
return await httpClient.SendAsync(request);
}
public async Task<HttpResponseMessage> SetUserPreferences(SetUserPreferencesData setUserPreferencesData)
{
return await httpClient.PostAsJsonAsync($"user_preferences/{setUserPreferencesData.UserId}", setUserPreferencesData, Configuration.JsonSerializerOptions);
}
}
@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.Retract;
public class RetractNotificationData
{
public required string UserId { get; set; }
public required string NotificationId { get; set; }
public string? SecondaryId { get; set; }
[SetsRequiredMembers]
public RetractNotificationData(string userId, string notificationId)
{
UserId = userId;
NotificationId = notificationId;
}
public RetractNotificationData()
{
}
}
@@ -0,0 +1,18 @@
namespace StoneRed.NetificationApi.Server.Send;
public class NotificationApnOptions
{
public int? Expiry { get; set; }
public int? Priority { get; set; }
public string? CollapseId { get; set; }
public string? ThreadId { get; set; }
public int? Badge { get; set; }
public string? Sound { get; set; }
public bool? ContentAvailable { get; set; }
}
@@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.Send;
internal class NotificationEmailAttachments
{
public required string FileName { get; set; }
public required string Url { get; set; }
[SetsRequiredMembers]
public NotificationEmailAttachments(string fileName, string url)
{
FileName = fileName;
Url = url;
}
public NotificationEmailAttachments()
{
}
}
@@ -0,0 +1,12 @@
namespace StoneRed.NetificationApi.Server.Send;
public class NotificationEmailOptions
{
public string[]? ReplyToAddresses { get; set; }
public string[]? CcAddresses { get; set; }
public string[]? BccAddresses { get; set; }
public string[]? Attachments { get; set; }
}
@@ -0,0 +1,10 @@
namespace StoneRed.NetificationApi.Server.Send;
internal class NotificationFcmAndroidOptions
{
public string? CollapseKey { get; set; }
public string? Priority { get; set; }
public string? Ttl { get; set; }
}
@@ -0,0 +1,6 @@
namespace StoneRed.NetificationApi.Server.Send;
internal class NotificationFcmOptions
{
public NotificationFcmAndroidOptions? Android { get; set; }
}
@@ -0,0 +1,8 @@
namespace StoneRed.NetificationApi.Server.Send;
public class NotificationOptions
{
public NotificationEmailOptions? Email { get; set; }
public NotificationApnOptions? Apn { get; set; }
}
@@ -0,0 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace StoneRed.NetificationApi.Server.Send;
public class NotificationUser
{
public required string Id { get; set; }
public string? Email { get; set; }
[JsonPropertyName("number")]
public string? TelephoneNumber { get; set; }
public NotificationUser()
{
}
[SetsRequiredMembers]
public NotificationUser(string id)
{
Id = id;
}
}
@@ -0,0 +1,30 @@
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.Send;
public class SendNotificationData
{
public required string NotificationId { get; set; }
public string? SubNotificationId { get; set; }
public string? TemplateId { get; set; }
public required NotificationUser User { get; set; }
public Dictionary<string, object>? MergeTags { get; set; }
public Dictionary<string, string>? Replace { get; set; }
public NotificationOptions? Options { get; set; }
[SetsRequiredMembers]
public SendNotificationData(string notificationId, NotificationUser user)
{
NotificationId = notificationId;
User = user;
}
public SendNotificationData()
{
}
}
@@ -0,0 +1,26 @@
using StoneRed.NetificationApi.Shared;
using System.Diagnostics.CodeAnalysis;
namespace StoneRed.NetificationApi.Server.SetUserPreferences;
public class NotificationPreference
{
public required string NotificationId { get; set; }
public required NotificationChannel Channel { get; set; }
public required bool State { get; set; }
[SetsRequiredMembers]
public NotificationPreference(string notificationId, NotificationChannel channel, bool state)
{
NotificationId = notificationId;
Channel = channel;
State = state;
}
public NotificationPreference()
{
}
}
@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace StoneRed.NetificationApi.Server.SetUserPreferences;
public class SetUserPreferencesData
{
[JsonIgnore]
public required string UserId { get; set; }
public required List<NotificationPreference> Preferences { get; set; }
[SetsRequiredMembers]
public SetUserPreferencesData(string userId, List<NotificationPreference> preferences)
{
UserId = userId;
Preferences = preferences;
}
public SetUserPreferencesData()
{
}
}
@@ -0,0 +1,11 @@
namespace StoneRed.NetificationApi.Shared;
public enum NotificationChannel
{
EMAIL,
INAPP_WEB,
WEB_PUSH,
SMS,
PUSH,
CALL
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Websocket.Client" Version="5.1.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,18 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StoneRed.NetificationApi.Utilities;
internal static class Configuration
{
public static JsonSerializerOptions JsonSerializerOptions { get; } = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters =
{
new JsonStringEnumConverter(),
new DateTimeConverter(),
}
};
}
@@ -0,0 +1,17 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StoneRed.NetificationApi.Utilities;
internal class DateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetDateTime().ToUniversalTime();
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToUniversalTime());
}
}
@@ -0,0 +1,13 @@
using System.Security.Cryptography;
using System.Text;
namespace StoneRed.NetificationApi.Utilities;
internal static class UserIdHasher
{
public static string Hash(string userId, string clientSecret)
{
using HMACSHA256 hmac = new HMACSHA256(Encoding.ASCII.GetBytes(clientSecret));
return Convert.ToBase64String(hmac.ComputeHash(Encoding.ASCII.GetBytes(userId)));
}
}
@@ -0,0 +1,20 @@
using StoneRed.NetificationApi.Client;
using System.Text.Json;
namespace StoneRed.NetificationApi.Utilities;
internal static class WebsocketMessageComparer
{
public static bool Compare(string? message, string route)
{
if (message is null)
{
return false;
}
WebsocketMessage? websocketMessage = JsonSerializer.Deserialize<WebsocketMessage>(message, Configuration.JsonSerializerOptions);
return websocketMessage?.Route == route;
}
}
@@ -0,0 +1,13 @@
using StoneRed.NetificationApi.Client;
using System.Text.Json;
namespace StoneRed.NetificationApi.Utilities;
internal static class WebsocketMessageConverter
{
public static WebsocketMessage<T> ConvertWebsocketMessage<T>(string message)
{
return JsonSerializer.Deserialize<WebsocketMessage<T>>(message, Configuration.JsonSerializerOptions) ?? throw new InvalidOperationException("Failed to deserialize websocket message.");
}
}