Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Core/Resgrid.Config/ChatConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static class ChatConfig
public static string NovuUnitFcmProviderId = "";
public static string NovuUnitApnsProviderId = "";
public static string NovuResponderFcmProviderId = "";
public static string NovuResponderApnsProviderId = "";
public static string NovuDispatchUnitWorkflowId = "unit-dispatch";
public static string NovuDispatchUserWorkflowId = "user-dispatch";
public static string NovuMessageUserWorkflowId = "user-message";
Expand Down
8 changes: 5 additions & 3 deletions Core/Resgrid.Model/Providers/IOutboundVoiceProvider.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace Resgrid.Model.Providers
using System.Threading.Tasks;

namespace Resgrid.Model.Providers
{
public interface IOutboundVoiceProvider
{
void CommunicateCall(string phoneNumber, UserProfile profile, Call call);
Task<bool> CommunicateCallAsync(string phoneNumber, UserProfile profile, Call call);
}
}
}
2 changes: 1 addition & 1 deletion Core/Resgrid.Services/CommunicationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public async Task<bool> SendCallAsync(Call call, CallDispatch dispatch, string d
{

if (!Config.SystemBehaviorConfig.DoNotBroadcast || Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId))
_outboundVoiceProvider.CommunicateCall(departmentNumber, profile, call);
await _outboundVoiceProvider.CommunicateCallAsync(departmentNumber, profile, call);
}
catch (Exception ex)
{
Expand Down
82 changes: 32 additions & 50 deletions Providers/Resgrid.Providers.Messaging/NovuProvider.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Novu.Domain.Models.Subscribers;
using RabbitMQ.Client;
using Resgrid.Config;
using Resgrid.Framework;
using Resgrid.Model;
using Resgrid.Model.Providers;
using Resgrid.Providers.Bus.Models;
using System.Text;


namespace Resgrid.Providers.Messaging
Expand Down Expand Up @@ -196,7 +197,7 @@ public async Task<bool> UpdateUserSubscriberFcm(string userId, string code, stri

public async Task<bool> UpdateUserSubscriberApns(string userId, string code, string token)
{
return await UpdateSubscriberApns($"{code}_User_{userId}", token, ChatConfig.NovuUnitApnsProviderId);
return await UpdateSubscriberApns($"{code}_User_{userId}", token, ChatConfig.NovuResponderApnsProviderId);
}

public async Task<bool> UpdateUnitSubscriberFcm(int unitId, string code, string token)
Expand All @@ -221,7 +222,7 @@ private async Task<bool> SendNotification(string title, string body, string reci
httpClient.DefaultRequestHeaders.Add("Authorization", $"ApiKey {ChatConfig.NovuSecretKey}");
httpClient.DefaultRequestHeaders.Add("idempotency-key", Guid.NewGuid().ToString());

string androidChannelName = GetAndroidChannelName(eventCode);
string channelName = GetAndroidChannelName(eventCode);
// Build request payload
var payload = new
{
Expand All @@ -230,24 +231,21 @@ private async Task<bool> SendNotification(string title, string body, string reci
{
subject = title,
body = body,
//inAppAvatar
//arrowImage

},
overrides = new
{
fcm = new
{
android = new
{
priority = androidChannelName == "calls" ? "high" : "normal",
priority = channelName == "calls" ? "high" : "normal",
notification = new
{
channelId = type,
defaultSound = true,
sticky = androidChannelName == "calls" ? true : false,
sticky = channelName == "calls" ? true : false,
//priority = androidChannelName == "calls" ? 5 : 3,
priority = androidChannelName == "calls" ? "max" : "default",
notification_priority = channelName == "calls" ? "PRIORITY_MAX" : "PRIORITY_DEFAULT",
},
data = new
{
Expand All @@ -256,29 +254,35 @@ private async Task<bool> SendNotification(string title, string body, string reci
eventCode = eventCode,
type = type
}
}//,
//data = new
//{
// title = title,
// message = body,
// eventCode = eventCode,
// type = type
//}
}
},
},
apns = new
{
badge = count,
sound = new
{
name = GetSoundFileNameFromType(Platforms.iOS, type),
critical = channelName == "calls" ? 1 : 0,
volume = 1.0f
},
type = type,
category = channelName,
eventCode = eventCode,
},
Comment on lines +259 to +271
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify APNS override shape matches Novu’s schema

The overrides.apns block sets badge/sound/category directly. Novu’s schema for APNS overrides may expect an aps payload wrapper (e.g., overrides.apns.payload.aps.{badge,sound,category}) and custom fields (eventCode/type) outside aps. If this shape is off, APNS may ignore the fields.

Would you verify against Novu’s overrides schema and example for APNS?


🌐 Web query:

What is the expected JSON schema for the "overrides.apns" payload in Novu's v1 events/trigger API? Provide an example including badge, sound with critical/volume, and category.

💡 Result:

The expected JSON schema for the overrides.apns payload in Novu's v1 events/trigger API should closely follow the ApnsConfig structure used by Firebase Cloud Messaging (FCM), as Novu internally uses this format for push overrides[1][3]. This allows you to specify detailed APNs (Apple Push Notification Service) payload options, such as badge, sound (with critical and volume), and category.

Example JSON Structure

{
  "overrides": {
    "apns": {
      "payload": {
        "aps": {
          "badge": 5,
          "sound": {
            "critical": 1,
            "name": "default",
            "volume": 0.8
          },
          "category": "MY_CATEGORY"
        }
      }
    }
  }
}

Key fields explanation:

  • badge
    Integer. Specifies the badge number for the app icon.

  • sound
    Object. Includes:

    • "name": String, the sound file name ("default" or custom).
    • "critical": Integer (1 for critical alert, 0 for non-critical).
    • "volume": Float (0.0 to 1.0).
  • category
    String. Used for grouping or to enable specific notification actions.

Schema Reference

The schema should match:

{
  "overrides": {
    "apns": {
      "payload": {
        "aps": {
          "badge": <integer>,
          "sound": {
            "critical": <integer>,   // 0 or 1
            "name": <string>,
            "volume": <float>        // 0.0 through 1.0
          },
          "category": <string>
        }
      }
    }
  }
}
  • Wrap these settings in the aps payload object as per APNs and FCM standards[1][3].
  • Additional APNs fields (e.g., alert, thread-id, etc.) can also be provided in the aps dictionary.

Notes

  • Critical sound notifications require the app to have the proper entitlement.
  • Novu processes this JSON as-is and relays it to APNs using the FCM or relevant provider integration[1][3].

For further customizations or to validate your payload, refer to the ApnsConfig documentation[3].

Citations:


Adjust APNS override to include the payload.aps wrapper

The current apns object is not nested under payload.aps as required by Novu’s schema. Badge, sound, and category must live inside payload.aps, with custom fields (type, eventCode) alongside aps.

Please update in Providers/Resgrid.Providers.Messaging/NovuProvider.cs (lines 259–271):

-   apns = new
-   {
-       badge = count,
-       sound = new
-       {
-           name = GetSoundFileNameFromType(Platforms.iOS, type),
-           critical = channelName == "calls" ? 1 : 0,
-           volume = 1.0f
-       },
-       type = type,
-       category = channelName,
-       eventCode = eventCode,
-   },
+   apns = new
+   {
+       payload = new
+       {
+           aps = new
+           {
+               badge = count,
+               sound = new
+               {
+                   name = GetSoundFileNameFromType(Platforms.iOS, type),
+                   critical = channelName == "calls" ? 1 : 0,
+                   volume = 1.0f
+               },
+               category = channelName
+           },
+           type = type,
+           eventCode = eventCode
+       }
+   },

This aligns with the expected JSON:

"overrides": {
  "apns": {
    "payload": {
      "aps": {
        "badge": 5,
        "sound": {
          "critical": 1,
          "name": "default",
          "volume": 0.8
        },
        "category": "MY_CATEGORY"
      },
      "type": "<your_type>",
      "eventCode": "<your_eventCode>"
    }
  }
}


},
to = new[]{ new
{
subscriberId = recipientId
}},
};

var payloadString = JsonConvert.SerializeObject(payload);
var content = new StringContent(
JsonConvert.SerializeObject(payload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
payloadString,
Encoding.UTF8,
"application/json");



var result = await httpClient.PostAsync("v1/events/trigger", content);

return result.IsSuccessStatusCode;
Expand Down Expand Up @@ -317,53 +321,31 @@ private string GetSoundFileNameFromType(Platforms platform, string type)
{
if (type == ((int)PushSoundTypes.CallEmergency).ToString())
{
if (platform == Platforms.iOS)
return "callemergency.caf";

return "callemergency.wav";
}
else if (type == ((int)PushSoundTypes.CallHigh).ToString())

{
if (platform == Platforms.iOS)
return "callhigh.caf";

return "callhigh.mp3";
return "callhigh.wav";
}
else if (type == ((int)PushSoundTypes.CallMedium).ToString())
{
if (platform == Platforms.iOS)
return "callmedium.caf";

return "callmedium.mp3";
return "callmedium.wav";
}
else if (type == ((int)PushSoundTypes.CallLow).ToString())
{
if (platform == Platforms.iOS)
return "calllow.caf";

return "calllow.mp3";
return "calllow.wav";
}
else if (type == ((int)PushSoundTypes.Notifiation).ToString())
{
if (platform == Platforms.iOS)
return "notification.caf";

return "notification.mp3";
return "notification.wav";
}
else if (type == ((int)PushSoundTypes.Message).ToString())
{
if (platform == Platforms.iOS)
return "message.caf";

return "message.mp3";
return "message.wav";
}
else
{
if (platform == Platforms.iOS)
return $"{type}.caf";

return $"{type}.mp3";
return $"{type}.wav";
}
}

Expand Down
15 changes: 10 additions & 5 deletions Providers/Resgrid.Providers.Number/OutboundVoiceProvider.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Resgrid.Model;
using Resgrid.Model.Providers;
using Twilio;
Expand All @@ -11,15 +12,15 @@ namespace Resgrid.Providers.NumberProvider
{
public class OutboundVoiceProvider : IOutboundVoiceProvider
{
public void CommunicateCall(string phoneNumber, UserProfile profile, Call call)
public async Task<bool> CommunicateCallAsync(string phoneNumber, UserProfile profile, Call call)
{
if (profile == null)
return;
return false;

TwilioClient.Init(Config.NumberProviderConfig.TwilioAccountSid, Config.NumberProviderConfig.TwilioAuthToken);

if (!profile.VoiceForCall)
return;
return false;

string number = phoneNumber;

Expand All @@ -38,7 +39,8 @@ public void CommunicateCall(string phoneNumber, UserProfile profile, Call call)
options.Method = "GET";
//options.IfMachine = "Continue";

var phoneCall = CallResource.Create(options);
var phoneCall = await CallResource.CreateAsync(options);
return true;
}
}

Expand All @@ -51,9 +53,12 @@ public void CommunicateCall(string phoneNumber, UserProfile profile, Call call)
options.Method = "GET";
//options.IfMachine = "Continue";

var phoneCall = CallResource.Create(options);
var phoneCall = await CallResource.CreateAsync(options);
return true;
}
}

return false;
}
}
}
Loading