diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 871b0d17..0fd03535 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -14,21 +14,16 @@ using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json.Linq; using Xunit; -using FirebaseAdmin.Messaging; using Google.Apis.Json; +using Newtonsoft.Json; namespace FirebaseAdmin.Messaging.Tests { public class MessageTest { - [Fact] - public void MessageWithoutTarget() - { - Assert.Throws(() => new Message().CopyAndValidate()); - } - [Fact] public void EmptyMessage() { @@ -43,48 +38,10 @@ public void EmptyMessage() } [Fact] - public void MultipleTargets() - { - var message = new Message() - { - Token = "test-token", - Topic = "test-topic", - }; - Assert.Throws(() => message.CopyAndValidate()); - - message = new Message() - { - Token = "test-token", - Condition = "test-condition", - }; - Assert.Throws(() => message.CopyAndValidate()); - - message = new Message() - { - Condition = "test-condition", - Topic = "test-topic", - }; - Assert.Throws(() => message.CopyAndValidate()); - - message = new Message() - { - Token = "test-token", - Topic = "test-topic", - Condition = "test-condition", - }; - Assert.Throws(() => message.CopyAndValidate()); - } - - [Fact] - public void MessageDeserialization() + public void PrefixedTopicName() { - var original = new Message() - { - Topic = "test-topic", - }; - var json = NewtonsoftJsonSerializer.Instance.Serialize(original); - var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); - Assert.Equal(original.Topic, copy.Topic); + var message = new Message(){Topic = "/topics/test-topic"}; + AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); } [Fact] @@ -106,27 +63,6 @@ public void DataMessage() }, message); } - [Fact] - public void InvalidTopicNames() - { - var topics = new List() - { - "/topics/", "/foo/bar", "foo bar", - }; - foreach (var topic in topics) - { - var message = new Message(){Topic = topic}; - Assert.Throws(() => message.CopyAndValidate()); - } - } - - [Fact] - public void PrefixedTopicName() - { - var message = new Message(){Topic = "/topics/test-topic"}; - AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); - } - [Fact] public void Notification() { @@ -153,6 +89,110 @@ public void Notification() AssertJsonEquals(expected, message); } + [Fact] + public void MessageDeserialization() + { + var original = new Message() + { + Topic = "test-topic", + Data = new Dictionary(){{ "key", "value" }}, + Notification = new Notification() + { + Title = "title", + Body = "body", + }, + Android = new AndroidConfig() + { + RestrictedPackageName = "test-pkg-name", + }, + Webpush = new WebpushConfig() + { + Data = new Dictionary(){{ "key", "value" }}, + }, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Topic, copy.Topic); + Assert.Equal(original.Data, copy.Data); + Assert.Equal(original.Notification.Title, copy.Notification.Title); + Assert.Equal(original.Notification.Body, copy.Notification.Body); + Assert.Equal( + original.Android.RestrictedPackageName, copy.Android.RestrictedPackageName); + Assert.Equal(original.Webpush.Data, copy.Webpush.Data); + } + + [Fact] + public void MessageCopy() + { + var original = new Message() + { + Topic = "test-topic", + Data = new Dictionary(), + Notification = new Notification(), + Android = new AndroidConfig(), + Webpush = new WebpushConfig(), + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.NotSame(original.Data, copy.Data); + Assert.NotSame(original.Notification, copy.Notification); + Assert.NotSame(original.Android, copy.Android); + Assert.NotSame(original.Webpush, copy.Webpush); + } + + [Fact] + public void MessageWithoutTarget() + { + Assert.Throws(() => new Message().CopyAndValidate()); + } + + [Fact] + public void MultipleTargets() + { + var message = new Message() + { + Token = "test-token", + Topic = "test-topic", + }; + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() + { + Token = "test-token", + Condition = "test-condition", + }; + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() + { + Condition = "test-condition", + Topic = "test-topic", + }; + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() + { + Token = "test-token", + Topic = "test-topic", + Condition = "test-condition", + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void InvalidTopicNames() + { + var topics = new List() + { + "/topics/", "/foo/bar", "foo bar", + }; + foreach (var topic in topics) + { + var message = new Message(){Topic = topic}; + Assert.Throws(() => message.CopyAndValidate()); + } + } + [Fact] public void AndroidConfig() { @@ -227,20 +267,12 @@ public void AndroidConfigMinimal() var message = new Message() { Topic = "test-topic", - Android = new AndroidConfig() - { - RestrictedPackageName = "test-pkg-name", - }, + Android = new AndroidConfig(), }; var expected = new JObject() { {"topic", "test-topic"}, - { - "android", new JObject() - { - { "restricted_package_name", "test-pkg-name" }, - } - }, + {"android", new JObject()}, }; AssertJsonEquals(expected, message); } @@ -270,41 +302,110 @@ public void AndroidConfigFullSecondsTTL() } [Fact] - public void AndroidConfigInvalidTTL() + public void AndroidConfigDeserialization() { - var message = new Message() + var original = new AndroidConfig() { - Topic = "test-topic", - Android = new AndroidConfig() + CollapseKey = "collapse-key", + RestrictedPackageName = "test-pkg-name", + TimeToLive = TimeSpan.FromSeconds(10.5), + Priority = Priority.High, + Data = new Dictionary() { - TimeToLive = TimeSpan.FromHours(-1), + { "key", "value" }, }, - }; - var expected = new JObject() - { - {"topic", "test-topic"}, + Notification = new AndroidNotification() { - "android", new JObject() - { - { "ttl", "3600s" }, - } + Title = "title", }, }; - Assert.Throws(() => message.CopyAndValidate()); + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.CollapseKey, copy.CollapseKey); + Assert.Equal(original.RestrictedPackageName, copy.RestrictedPackageName); + Assert.Equal(original.Priority, copy.Priority); + Assert.Equal(original.TimeToLive, copy.TimeToLive); + Assert.Equal(original.Data, copy.Data); + Assert.Equal(original.Notification.Title, copy.Notification.Title); } [Fact] - public void AndroidConfigDeserialization() + public void AndroidConfigCopy() { var original = new AndroidConfig() { - TimeToLive = TimeSpan.FromSeconds(10.5), - Priority = Priority.High, + Data = new Dictionary(), + Notification = new AndroidNotification(), + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.NotSame(original.Data, copy.Data); + Assert.NotSame(original.Notification, copy.Notification); + } + + [Fact] + public void AndroidNotificationDeserialization() + { + var original = new AndroidNotification() + { + Title = "title", + Body = "body", + Icon = "icon", + Color = "#112233", + Sound = "sound", + Tag = "tag", + ClickAction = "click-action", + TitleLocKey = "title-loc-key", + TitleLocArgs = new List(){ "arg1", "arg2" }, + BodyLocKey = "body-loc-key", + BodyLocArgs = new List(){ "arg3", "arg4" }, + ChannelId = "channel-id", }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); - var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); - Assert.Equal(original.Priority, copy.Priority); - Assert.Equal(original.TimeToLive, copy.TimeToLive); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Title, copy.Title); + Assert.Equal(original.Body, copy.Body); + Assert.Equal(original.Icon, copy.Icon); + Assert.Equal(original.Color, copy.Color); + Assert.Equal(original.Sound, copy.Sound); + Assert.Equal(original.Tag, copy.Tag); + Assert.Equal(original.ClickAction, copy.ClickAction); + Assert.Equal(original.TitleLocKey, copy.TitleLocKey); + Assert.Equal(original.TitleLocArgs, copy.TitleLocArgs); + Assert.Equal(original.BodyLocKey, copy.BodyLocKey); + Assert.Equal(original.BodyLocArgs, copy.BodyLocArgs); + Assert.Equal(original.ChannelId, copy.ChannelId); + } + + [Fact] + public void AndroidNotificationCopy() + { + var original = new AndroidNotification() + { + TitleLocKey = "title-loc-key", + TitleLocArgs = new List(){ "arg1", "arg2" }, + BodyLocKey = "body-loc-key", + BodyLocArgs = new List(){ "arg3", "arg4" }, + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.NotSame(original.TitleLocArgs, copy.TitleLocArgs); + Assert.NotSame(original.BodyLocArgs, copy.BodyLocArgs); + } + + + [Fact] + public void AndroidConfigInvalidTTL() + { + var message = new Message() + { + Topic = "test-topic", + Android = new AndroidConfig() + { + TimeToLive = TimeSpan.FromHours(-1), + }, + }; + Assert.Throws(() => message.CopyAndValidate()); } [Fact] @@ -358,6 +459,346 @@ public void AndroidNotificationInvalidBodyLocArgs() Assert.Throws(() => message.CopyAndValidate()); } + [Fact] + public void WebpushConfig() + { + var message = new Message() + { + Topic = "test-topic", + Webpush = new WebpushConfig() + { + Headers = new Dictionary() + { + {"header1", "header-value1"}, + {"header2", "header-value2"}, + }, + Data = new Dictionary() + { + {"key1", "value1"}, + {"key2", "value2"}, + }, + Notification = new WebpushNotification() + { + Title = "title", + Body = "body", + Icon = "icon", + Badge = "badge", + Data = new Dictionary() + { + {"some", "data"}, + }, + Direction = Direction.LeftToRight, + Image = "image", + Language = "language", + Tag = "tag", + Silent = true, + RequireInteraction = true, + Renotify = true, + TimestampMillis = 100, + Vibrate = new int[]{10, 5, 10}, + Actions = new List() + { + new Action() + { + ActionName = "Accept", + Title = "Ok", + Icon = "ok-button", + }, + new Action() + { + ActionName = "Reject", + Title = "Cancel", + Icon = "cancel-button", + }, + }, + CustomData = new Dictionary() + { + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + }, + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "webpush", new JObject() + { + { + "headers", new JObject() + { + {"header1", "header-value1"}, + {"header2", "header-value2"}, + } + }, + { + "data", new JObject() + { + {"key1", "value1"}, + {"key2", "value2"}, + } + }, + { + "notification", new JObject() + { + {"title", "title"}, + {"body", "body"}, + {"icon", "icon"}, + {"badge", "badge"}, + { + "data", new JObject() + { + {"some", "data"}, + } + }, + {"dir", "ltr"}, + {"image", "image"}, + {"lang", "language"}, + {"renotify", true}, + {"requireInteraction", true}, + {"silent", true}, + {"tag", "tag"}, + {"timestamp", 100}, + {"vibrate", new JArray(){10, 5, 10}}, + { + "actions", new JArray() + { + new JObject() + { + {"action", "Accept"}, + {"title", "Ok"}, + {"icon", "ok-button"}, + }, + new JObject() + { + {"action", "Reject"}, + {"title", "Cancel"}, + {"icon", "cancel-button"}, + }, + } + }, + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void WebpushConfigMinimal() + { + var message = new Message() + { + Topic = "test-topic", + Webpush = new WebpushConfig(), + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + {"webpush", new JObject()}, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void WebpushConfigMinimalNotification() + { + var message = new Message() + { + Topic = "test-topic", + Webpush = new WebpushConfig() + { + Notification = new WebpushNotification() + { + Title = "title", + Body = "body", + Icon = "icon", + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "webpush", new JObject() + { + { + "notification", new JObject() + { + {"title", "title"}, + {"body", "body"}, + {"icon", "icon"}, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void WebpushConfigDeserialization() + { + var original = new WebpushConfig() + { + Headers = new Dictionary() + { + {"header1", "header-value1"}, + {"header2", "header-value2"}, + }, + Data = new Dictionary() + { + {"key1", "value1"}, + {"key2", "value2"}, + }, + Notification = new WebpushNotification() + { + Title = "title", + }, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Headers, copy.Headers); + Assert.Equal(original.Data, copy.Data); + Assert.Equal(original.Notification.Title, copy.Notification.Title); + } + + [Fact] + public void WebpushConfigCopy() + { + var original = new WebpushConfig() + { + Headers = new Dictionary(), + Data = new Dictionary(), + Notification = new WebpushNotification(), + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.NotSame(original.Headers, copy.Headers); + Assert.NotSame(original.Data, copy.Data); + Assert.NotSame(original.Notification, copy.Notification); + } + + [Fact] + public void WebpushNotificationDeserialization() + { + var original = new WebpushNotification() + { + Title = "title", + Body = "body", + Icon = "icon", + Badge = "badge", + Data = new Dictionary() + { + {"some", "data"}, + }, + Direction = Direction.LeftToRight, + Image = "image", + Language = "language", + Tag = "tag", + Silent = true, + RequireInteraction = true, + Renotify = true, + TimestampMillis = 100, + Vibrate = new int[]{10, 5, 10}, + Actions = new List() + { + new Action() + { + ActionName = "Accept", + Title = "Ok", + Icon = "ok-button", + }, + new Action() + { + ActionName = "Reject", + Title = "Cancel", + Icon = "cancel-button", + }, + }, + CustomData = new Dictionary() + { + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + }, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Title, copy.Title); + Assert.Equal(original.Body, copy.Body); + Assert.Equal(original.Icon, copy.Icon); + Assert.Equal(original.Badge, copy.Badge); + Assert.Equal(new JObject(){{"some", "data"}}, copy.Data); + Assert.Equal(original.Direction, copy.Direction); + Assert.Equal(original.Image, copy.Image); + Assert.Equal(original.Language, copy.Language); + Assert.Equal(original.Tag, copy.Tag); + Assert.Equal(original.Silent, copy.Silent); + Assert.Equal(original.RequireInteraction, copy.RequireInteraction); + Assert.Equal(original.Renotify, copy.Renotify); + Assert.Equal(original.TimestampMillis, copy.TimestampMillis); + Assert.Equal(original.Vibrate, copy.Vibrate); + var originalActions = original.Actions.ToList(); + var copyActions = original.Actions.ToList(); + Assert.Equal(originalActions.Count, copyActions.Count); + for (int i = 0; i < originalActions.Count; i++) + { + Assert.Equal(originalActions[i].ActionName, copyActions[i].ActionName); + Assert.Equal(originalActions[i].Title, copyActions[i].Title); + Assert.Equal(originalActions[i].Icon, copyActions[i].Icon); + } + Assert.Equal(original.CustomData, copy.CustomData); + } + + [Fact] + public void WebpushNotificationCopy() + { + var original = new WebpushNotification() + { + Actions = new List() + { + new Action() + { + ActionName = "Accept", + Title = "Ok", + Icon = "ok-button", + }, + }, + CustomData = new Dictionary() + { + {"custom-key1", "custom-data"}, + }, + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.NotSame(original.Actions, copy.Actions); + Assert.NotSame(original.Actions.First(), copy.Actions.First()); + Assert.NotSame(original.CustomData, copy.CustomData); + Assert.Equal(original.CustomData, copy.CustomData); + } + + [Fact] + public void WebpushNotificationDuplicateKeys() + { + var message = new Message() + { + Topic = "test-topic", + Webpush = new WebpushConfig() + { + Notification = new WebpushNotification() + { + Title = "title", + CustomData = new Dictionary(){{"title", "other"}}, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index fb1ad2e7..0c71f7f1 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -100,13 +100,5 @@ public static IReadOnlyDictionary Copy( } return copy; } - - /// - /// Creates a shallow copy of a collection of items. - /// - public static IEnumerable Copy(this IEnumerable source) - { - return new List(source); - } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 49c68553..37b79cb3 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -42,7 +42,7 @@ public sealed class AndroidConfig /// service. /// [JsonProperty("priority")] - internal string PriorityString + private string PriorityString { get { @@ -86,7 +86,7 @@ internal string PriorityString /// by the number of seconds, with nanoseconds expressed as fractional seconds. /// [JsonProperty("ttl")] - internal string TtlString + private string TtlString { get { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 5d520ba2..417efe70 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -128,9 +128,9 @@ internal AndroidNotification CopyAndValidate() Tag = this.Tag, ClickAction = this.ClickAction, TitleLocKey = this.TitleLocKey, - TitleLocArgs = this.TitleLocArgs?.Copy(), + TitleLocArgs = this.TitleLocArgs?.ToList(), BodyLocKey = this.BodyLocKey, - BodyLocArgs = this.BodyLocArgs?.Copy(), + BodyLocArgs = this.BodyLocArgs?.ToList(), ChannelId = this.ChannelId, }; if (copy.Color != null && !Regex.Match(copy.Color, "^#[0-9a-fA-F]{6}$").Success) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index baab5924..6635761e 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -47,7 +47,7 @@ public sealed class Message /// prefix if present. This is what's ultimately sent to the FCM service. /// [JsonProperty("topic")] - internal string UnprefixedTopic + private string UnprefixedTopic { get { @@ -89,6 +89,12 @@ internal string UnprefixedTopic [JsonProperty("android")] public AndroidConfig Android { get; set; } + /// + /// The Webpush-specific information to be included in the message. + /// + [JsonProperty("webpush")] + public WebpushConfig Webpush { get; set; } + /// /// Copies this message, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. Each property is copied @@ -124,6 +130,7 @@ internal Message CopyAndValidate() // Copy and validate the child properties copy.Notification = this.Notification?.CopyAndValidate(); copy.Android = this.Android?.CopyAndValidate(); + copy.Webpush = this.Webpush?.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs new file mode 100644 index 00000000..4900b1b4 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs @@ -0,0 +1,60 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the Webpush protocol options that can be included in a . + /// + public sealed class WebpushConfig + { + /// + /// Webpush HTTP headers. Refer + /// Webpush specification for supported headers. + /// + [JsonProperty("headers")] + public IReadOnlyDictionary Headers { get; set; } + + /// + /// Webpush data fields. When set, overrides any data fields set via + /// . + /// + [JsonProperty("data")] + public IReadOnlyDictionary Data { get; set; } + + /// + /// The Webpush notification to be included in the message. + /// + [JsonProperty("notification")] + public WebpushNotification Notification { get; set; } + + /// + /// Copies this Webpush config, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. + /// + internal WebpushConfig CopyAndValidate() + { + return new WebpushConfig() + { + Headers = this.Headers?.Copy(), + Data = this.Data?.Copy(), + Notification = this.Notification?.CopyAndValidate(), + }; + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs new file mode 100644 index 00000000..10dd7fb3 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -0,0 +1,276 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Google.Apis.Json; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the Webpush-specific notification options that can be included in a + /// . Supports most standard options defined in the + /// + /// Web Notification specification + /// + public sealed class WebpushNotification + { + /// + /// Title text of the notification. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Body text of the notification. + /// + [JsonProperty("body")] + public string Body { get; set; } + + /// + /// The URL to the icon of the notification. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + + /// + /// The URL of the image used to represent the notification when there is not enough space + /// to display the notification itself. + /// + [JsonProperty("badge")] + public string Badge { get; set; } + + /// + /// Any arbitrary data that should be associated with the notification. + /// + [JsonProperty("data")] + public object Data { get; set; } + + /// + /// The direction in which to display the notification. + /// + [JsonIgnore] + public Direction? Direction { get; set; } + + /// + /// Converts the property into a string value that can be included + /// in the json output. + /// + [JsonProperty("dir")] + private string DirectionString + { + get + { + switch (Direction) + { + case Messaging.Direction.Auto: + return "auto"; + case Messaging.Direction.LeftToRight: + return "ltr"; + case Messaging.Direction.RightToLeft: + return "rtl"; + default: + return null; + } + } + set + { + switch (value) + { + case "auto": + Direction = Messaging.Direction.Auto; + return; + case "ltr": + Direction = Messaging.Direction.LeftToRight; + return; + case "rtl": + Direction = Messaging.Direction.RightToLeft; + return; + default: + throw new FirebaseException($"Invalid direction value: {value}. Only " + + "'auto', 'rtl' and 'ltr' are allowed."); + } + } + } + + /// + /// The URL of an image to be displayed in the notification. + /// + [JsonProperty("image")] + public string Image { get; set; } + + /// + /// The language of the notification. + /// + [JsonProperty("lang")] + public string Language { get; set; } + + /// + /// Whether the user should be notified after a new notification replaces an old one. + /// + [JsonProperty("renotify")] + public bool? Renotify { get; set; } + + /// + /// Whether a notification should remain active until the user clicks or dismisses it, + /// rather than closing automatically. + /// + [JsonProperty("requireInteraction")] + public bool? RequireInteraction { get; set; } + + /// + /// Whether the notification should be silent. + /// + [JsonProperty("silent")] + public bool? Silent { get; set; } + + /// + /// An identifying tag for the notification. + /// + [JsonProperty("tag")] + public string Tag { get; set; } + + /// + /// A timestamp value in milliseconds on the notification. + /// + [JsonProperty("timestamp")] + public long? TimestampMillis { get; set; } + + /// + /// A vibration pattern for the receiving device's vibration hardware to emit when the + /// notification fires. + /// + [JsonProperty("vibrate")] + public int[] Vibrate { get; set; } + + /// + /// A collection of notification actions to be associated with the notification. + /// + [JsonProperty("actions")] + public IEnumerable Actions; + + /// + /// A collection of arbitrary key-value data to be included in the notification. This is + /// exposed as an to support correct + /// deserialization of custom properties. + /// + [JsonExtensionData] + public IDictionary CustomData { get; set; } + + /// + /// Copies this Webpush notification, and validates the content of it to ensure that it can + /// be serialized into the JSON format expected by the FCM service. + /// + internal WebpushNotification CopyAndValidate() + { + var copy = new WebpushNotification() + { + Title = this.Title, + Body = this.Body, + Icon = this.Icon, + Image = this.Image, + Language = this.Language, + Tag = this.Tag, + Direction = this.Direction, + Badge = this.Badge, + Renotify = this.Renotify, + RequireInteraction = this.RequireInteraction, + Silent = this.Silent, + Actions = this.Actions?.Select((item, _) => new Action(item)).ToList(), + Vibrate = this.Vibrate, + TimestampMillis = this.TimestampMillis, + Data = this.Data, + }; + + var customData = this.CustomData; + if (customData?.Count > 0) + { + var serializer = NewtonsoftJsonSerializer.Instance; + // Serialize the notification without CustomData for validation. + var json = serializer.Serialize(copy); + var dict = serializer.Deserialize>(json); + customData = new Dictionary(customData); + foreach (var entry in customData) + { + if (dict.ContainsKey(entry.Key)) + { + throw new ArgumentException( + $"Multiple specifications for WebpushNotification key: {entry.Key}"); + } + } + copy.CustomData = customData; + } + return copy; + } + } + + /// + /// Represents an action available to users when the notification is presented. + /// + public sealed class Action + { + /// + /// Action name. + /// + [JsonProperty("action")] + public string ActionName { get; set; } + + /// + /// Title text. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Icon URL. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + + /// + /// Creates a new Action instance. + /// + public Action() { } + + internal Action(Action action) + { + ActionName = action.ActionName; + Title = action.Title; + Icon = action.Icon; + } + } + + /// + /// Different directions a notification can be displayed in. + /// + public enum Direction + { + /// + /// Direction automatically determined. + /// + Auto, + + /// + /// Left to right. + /// + LeftToRight, + + /// + /// Right to left. + /// + RightToLeft, + } +}