diff --git a/BTCPayServer.Client/BTCPayServerClient.Notifications.cs b/BTCPayServer.Client/BTCPayServerClient.Notifications.cs index 2e156a41d..bdae85d30 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Notifications.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Notifications.cs @@ -33,6 +33,16 @@ public partial class BTCPayServerClient return await SendHttpRequest($"api/v1/users/me/notifications/{notificationId}", new UpdateNotification { Seen = seen }, HttpMethod.Put, token); } + public virtual async Task GetNotificationSettings(CancellationToken token = default) + { + return await SendHttpRequest("api/v1/users/me/notification-settings", null, HttpMethod.Get, token); + } + + public virtual async Task UpdateNotificationSettings(UpdateNotificationSettingsRequest request, CancellationToken token = default) + { + return await SendHttpRequest("api/v1/users/me/notification-settings", request, HttpMethod.Put, token); + } + public virtual async Task RemoveNotification(string notificationId, CancellationToken token = default) { await SendHttpRequest($"api/v1/users/me/notifications/{notificationId}", null, HttpMethod.Delete, token); diff --git a/BTCPayServer.Client/Models/NotificationSettingsData.cs b/BTCPayServer.Client/Models/NotificationSettingsData.cs new file mode 100644 index 000000000..bcfcece6f --- /dev/null +++ b/BTCPayServer.Client/Models/NotificationSettingsData.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace BTCPayServer.Client.Models; + +public class NotificationSettingsData +{ + public List Notifications { get; set; } +} + +public class NotificationSettingsItemData +{ + public string Identifier { get; set; } + public string Name { get; set; } + public bool Enabled { get; set; } +} diff --git a/BTCPayServer.Client/Models/UpdateNotificationSettingsRequest.cs b/BTCPayServer.Client/Models/UpdateNotificationSettingsRequest.cs new file mode 100644 index 000000000..4909418cd --- /dev/null +++ b/BTCPayServer.Client/Models/UpdateNotificationSettingsRequest.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace BTCPayServer.Client.Models; + +public class UpdateNotificationSettingsRequest +{ + public List Disabled { get; set; } +} diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index d07e8da40..aae3979d3 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -2851,6 +2851,30 @@ namespace BTCPayServer.Tests await client.RemoveNotification(notification.Id); Assert.Empty(await viewOnlyClient.GetNotifications(true)); Assert.Empty(await viewOnlyClient.GetNotifications(false)); + + // Settings + var settings = await client.GetNotificationSettings(); + Assert.True(settings.Notifications.Find(n => n.Identifier == "newversion").Enabled); + Assert.True(settings.Notifications.Find(n => n.Identifier == "pluginupdate").Enabled); + Assert.True(settings.Notifications.Find(n => n.Identifier == "inviteaccepted").Enabled); + + var request = new UpdateNotificationSettingsRequest { Disabled = ["newversion", "pluginupdate"] }; + settings = await client.UpdateNotificationSettings(request); + Assert.False(settings.Notifications.Find(n => n.Identifier == "newversion").Enabled); + Assert.False(settings.Notifications.Find(n => n.Identifier == "pluginupdate").Enabled); + Assert.True(settings.Notifications.Find(n => n.Identifier == "inviteaccepted").Enabled); + + request = new UpdateNotificationSettingsRequest { Disabled = ["all"] }; + settings = await client.UpdateNotificationSettings(request); + Assert.False(settings.Notifications.Find(n => n.Identifier == "newversion").Enabled); + Assert.False(settings.Notifications.Find(n => n.Identifier == "pluginupdate").Enabled); + Assert.False(settings.Notifications.Find(n => n.Identifier == "inviteaccepted").Enabled); + + request = new UpdateNotificationSettingsRequest { Disabled = [] }; + settings = await client.UpdateNotificationSettings(request); + Assert.True(settings.Notifications.Find(n => n.Identifier == "newversion").Enabled); + Assert.True(settings.Notifications.Find(n => n.Identifier == "pluginupdate").Enabled); + Assert.True(settings.Notifications.Find(n => n.Identifier == "inviteaccepted").Enabled); } [Fact(Timeout = TestTimeout)] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs index bf85fa7f0..2d86d7c22 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldNotificationsController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; @@ -23,12 +24,16 @@ namespace BTCPayServer.Controllers.Greenfield { private readonly UserManager _userManager; private readonly NotificationManager _notificationManager; + private readonly IEnumerable _notificationHandlers; - public GreenfieldNotificationsController(UserManager userManager, - NotificationManager notificationManager) + public GreenfieldNotificationsController( + UserManager userManager, + NotificationManager notificationManager, + IEnumerable notificationHandlers) { _userManager = userManager; _notificationManager = notificationManager; + _notificationHandlers = notificationHandlers; } [Authorize(Policy = Policies.CanViewNotificationsForUser, @@ -95,6 +100,37 @@ namespace BTCPayServer.Controllers.Greenfield return Ok(); } + + [Authorize(Policy = Policies.CanManageNotificationsForUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/users/me/notification-settings")] + public async Task GetNotificationSettings() + { + var user = await _userManager.GetUserAsync(User); + var model = GetNotificationSettingsData(user); + return Ok(model); + } + + [Authorize(Policy = Policies.CanManageNotificationsForUser, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPut("~/api/v1/users/me/notification-settings")] + public async Task UpdateNotificationSettings(UpdateNotificationSettingsRequest request) + { + var user = await _userManager.GetUserAsync(User); + if (request.Disabled.Contains("all")) + { + user.DisabledNotifications = "all"; + } + else + { + var disabled = _notificationHandlers + .SelectMany(handler => handler.Meta.Select(tuple => tuple.identifier)) + .Where(id => request.Disabled.Contains(id)).ToList(); + user.DisabledNotifications = disabled.Any() ? string.Join(';', disabled) + ";" : string.Empty; + } + await _userManager.UpdateAsync(user); + + var model = GetNotificationSettingsData(user); + return Ok(model); + } private NotificationData ToModel(NotificationViewModel entity) { @@ -113,5 +149,19 @@ namespace BTCPayServer.Controllers.Greenfield { return this.CreateAPIError(404, "notification-not-found", "The notification was not found"); } + + private NotificationSettingsData GetNotificationSettingsData(ApplicationUser user) + { + var disabledAll = user.DisabledNotifications == "all"; + var disabledNotifications = user.DisabledNotifications?.Split(';', StringSplitOptions.RemoveEmptyEntries).ToList() ?? []; + var notifications = _notificationHandlers.SelectMany(handler => handler.Meta.Select(tuple => + new NotificationSettingsItemData + { + Identifier = tuple.identifier, + Name = tuple.name, + Enabled = !disabledAll && !disabledNotifications.Contains(tuple.identifier, StringComparer.InvariantCultureIgnoreCase) + })).ToList(); + return new NotificationSettingsData { Notifications = notifications }; + } } } diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 86596cbba..dd682e081 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -22,7 +22,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NBitcoin; -using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json.Linq; using InvoiceData = BTCPayServer.Client.Models.InvoiceData; @@ -644,6 +643,18 @@ namespace BTCPayServer.Controllers.Greenfield HandleActionResult(await GetController().RevokeAPIKey(apikey)); } + public override async Task GetNotificationSettings(CancellationToken token = default) + { + return GetFromActionResult( + await GetController().GetNotificationSettings()); + } + + public override async Task UpdateNotificationSettings(UpdateNotificationSettingsRequest request, CancellationToken token = default) + { + return GetFromActionResult( + await GetController().UpdateNotificationSettings(request)); + } + public override async Task> GetNotifications(bool? seen = null, int? skip = null, int? take = null, CancellationToken token = default) { diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.notifications.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.notifications.json index 315b1687e..9e0f0aabc 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.notifications.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.notifications.json @@ -203,6 +203,80 @@ } ] } + }, + "/api/v1/users/me/notification-settings": { + "get": { + "tags": [ + "Notifications (Current User)" + ], + "summary": "Get notification settings", + "description": "View information about your notification settings", + "operationId": "Notifications_GetNotificationSettings", + "responses": { + "200": { + "description": "The current user's notification settings", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationSettingsData" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to view the notification settings" + } + }, + "security": [ + { + "API_Key": [ + "btcpay.user.canmanagenotificationsforuser" + ], + "Basic": [] + } + ] + }, + "put": { + "tags": [ + "Notifications (Current User)" + ], + "summary": "Update notification settings", + "description": "Updates the current user's notification settings", + "operationId": "Notifications_UpdateNotification", + "responses": { + "200": { + "description": "The current user's notification settings", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotificationSettingsData" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to update the notification settings" + } + }, + "security": [ + { + "API_Key": [ + "btcpay.user.canmanagenotificationsforuser" + ], + "Basic": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNotificationSettingsRequest" + } + } + } + } + } } }, "components": { @@ -254,6 +328,117 @@ "description": "If the notification has been seen by the user" } } + }, + "UpdateNotificationSettingsRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "disabled": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of the notification type identifiers, which should be disabled. Can also be a single item 'all'.", + "example": ["newversion", "pluginupdate"], + "nullable": false + } + } + }, + "NotificationSettingsData": { + "type": "object", + "additionalProperties": false, + "properties": { + "notifications": { + "type": "array", + "description": "The notification types", + "items": { + "$ref": "#/components/schemas/NotificationSettingsItemData" + } + } + }, + "example": [ + { + "identifier": "newversion", + "name": "New version", + "enabled": false + }, + { + "identifier": "newuserrequiresapproval", + "name": "New user requires approval", + "enabled": true + }, + { + "identifier": "inviteaccepted", + "name": "User accepted invitation", + "enabled": true + }, + { + "identifier": "pluginupdate", + "name": "Plugin update", + "enabled": false + }, + { + "identifier": "invoicestate", + "name": "All invoice updates", + "enabled": true + }, + { + "identifier": "invoicestate_invoice_paidAfterExpiration", + "name": "Invoice was paid after expiration", + "enabled": true + }, + { + "identifier": "invoicestate_invoice_expiredPaidPartial", + "name": "Invoice expired with partial payments", + "enabled": true + }, + { + "identifier": "invoicestate_invoice_failedToConfirm", + "name": "Invoice has payments that failed to confirm on time", + "enabled": true + }, + { + "identifier": "invoicestate_invoice_confirmed", + "name": "Invoice is settled", + "enabled": true + }, + { + "identifier": "payout", + "name": "Payouts", + "enabled": true + }, + { + "identifier": "external-payout-transaction", + "name": "External payout approval", + "enabled": true + } + ] + }, + "NotificationSettingsItemData": { + "type": "object", + "additionalProperties": false, + "properties": { + "identifier": { + "type": "string", + "description": "The identifier of the notification type", + "nullable": false + }, + "name": { + "type": "string", + "description": "The description of the notification type", + "nullable": false + }, + "enabled": { + "type": "boolean", + "description": "If the notification type is enabled", + "nullable": false + } + }, + "example": { + "identifier": "newversion", + "name": "New version", + "enabled": false + } } } },