using System; using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Models.NotificationViewModels; using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Controllers { [BitpayAPIConstraint(false)] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewNotificationsForUser)] [Route("notifications/{action:lowercase=Index}")] public class UINotificationsController : Controller { private readonly BTCPayServerEnvironment _env; private readonly NotificationSender _notificationSender; private readonly UserManager _userManager; private readonly NotificationManager _notificationManager; private readonly EventAggregator _eventAggregator; public UINotificationsController(BTCPayServerEnvironment env, NotificationSender notificationSender, UserManager userManager, NotificationManager notificationManager, EventAggregator eventAggregator) { _env = env; _notificationSender = notificationSender; _userManager = userManager; _notificationManager = notificationManager; _eventAggregator = eventAggregator; } [HttpGet] public IActionResult GetNotificationDropdownUI(string returnUrl) { return ViewComponent("Notifications", new { appearance = "Dropdown", returnUrl }); } [HttpGet] public async Task SubscribeUpdates(CancellationToken cancellationToken) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return BadRequest(); } var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); var userId = _userManager.GetUserId(User); var websocketHelper = new WebSocketHelper(websocket); IEventAggregatorSubscription subscription = null; try { subscription = _eventAggregator.SubscribeAsync(async evt => { if (evt.UserId == userId) { await websocketHelper.Send("update"); } }); await websocketHelper.NextMessageAsync(cancellationToken); } catch (OperationCanceledException) { // ignored } catch (WebSocketException) { } finally { subscription?.Dispose(); await websocketHelper.DisposeAsync(CancellationToken.None); } return new EmptyResult(); } #if DEBUG [HttpGet] public async Task GenerateJunk(int x = 100, bool admin = true) { for (int i = 0; i < x; i++) { await _notificationSender.SendNotification( admin ? (NotificationScope)new AdminScope() : new UserScope(_userManager.GetUserId(User)), new JunkNotification()); } return RedirectToAction("Index"); } #endif [HttpGet] public async Task Index(int skip = 0, int count = 50, int timezoneOffset = 0) { if (!ValidUserClaim(out var userId)) return RedirectToAction("Index", "UIHome"); var res = await _notificationManager.GetNotifications(new NotificationsQuery() { Skip = skip, Take = count, UserId = userId }); var model = new IndexViewModel() { Skip = skip, Count = count, Items = res.Items, Total = res.Count }; return View(model); } [HttpPost] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)] public async Task FlipRead(string id) { if (ValidUserClaim(out var userId)) { await _notificationManager.ToggleSeen(new NotificationsQuery() { Ids = new[] { id }, UserId = userId }, null); return RedirectToAction(nameof(Index)); } return BadRequest(); } [HttpGet] public async Task NotificationPassThrough(string id) { if (ValidUserClaim(out var userId)) { var items = await _notificationManager.ToggleSeen(new NotificationsQuery() { Ids = new[] { id }, UserId = userId }, true); var link = items.FirstOrDefault()?.ActionLink ?? ""; if (string.IsNullOrEmpty(link)) { return RedirectToAction(nameof(Index)); } return Redirect(link); } return NotFound(); } [HttpPost] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)] public async Task MassAction(string command, string[] selectedItems) { if (!ValidUserClaim(out var userId)) { return NotFound(); } if (command.StartsWith("flip-individual", StringComparison.InvariantCulture)) { var id = command.Split(":")[1]; return await FlipRead(id); } if (selectedItems != null) { switch (command) { case "delete": await _notificationManager.Remove(new NotificationsQuery() { UserId = userId, Ids = selectedItems }); break; case "mark-seen": await _notificationManager.ToggleSeen(new NotificationsQuery() { UserId = userId, Ids = selectedItems, Seen = false }, true); break; case "mark-unseen": await _notificationManager.ToggleSeen(new NotificationsQuery() { UserId = userId, Ids = selectedItems, Seen = true }, false); break; } return RedirectToAction(nameof(Index)); } return RedirectToAction(nameof(Index)); } [HttpPost] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanManageNotificationsForUser)] public async Task MarkAllAsSeen(string returnUrl) { if (!ValidUserClaim(out var userId)) { return NotFound(); } await _notificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true); return LocalRedirect(returnUrl); } private bool ValidUserClaim(out string userId) { userId = _userManager.GetUserId(User); return userId != null; } } }