Wire Web Push broadcaster: lock-screen pushes ride the in-app notifications
- nuget.config with Soroush Nexus + Liara mirrors (nuget.org filtered); added WebPush 1.0.12 - PushNotifier: VAPID send to a user's subscriptions, prunes dead (404/410); config from AppSetting - NotificationService fans out a Web Push to matched users' subscribed browsers after creating in-app notifications (best-effort; no-op until admin enables push + sets VAPID) - Build verified through the mirrors; app boots with PushNotifier wired Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<!-- Soroush Nexus mirror (primary) + Liara mirror (fallback) — nuget.org is filtered. -->
|
||||
<add key="nexus" value="https://mirror.soroushasadi.com/repository/nuget-group/index.json" protocolVersion="3" />
|
||||
<add key="liara" value="https://package-mirror.liara.ir/repository/nuget/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
<config>
|
||||
<add key="http_retry_count" value="6" />
|
||||
<add key="http_retry_delay_milliseconds" value="1000" />
|
||||
</config>
|
||||
</configuration>
|
||||
@@ -12,6 +12,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="WebPush" Version="1.0.12" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -24,6 +24,7 @@ builder.Services.AddScoped<OtpService>();
|
||||
builder.Services.AddSingleton<CaptchaService>();
|
||||
builder.Services.AddScoped<SubmissionGuard>();
|
||||
builder.Services.AddScoped<NotificationService>();
|
||||
builder.Services.AddScoped<PushNotifier>();
|
||||
// Listing parser: heuristic now; swap for an LLM-backed IListingParser later.
|
||||
builder.Services.AddSingleton<IListingParser, HeuristicListingParser>();
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ namespace JobsMedical.Web.Services;
|
||||
public class NotificationService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly PushNotifier _push;
|
||||
private readonly ILogger<NotificationService> _log;
|
||||
|
||||
public NotificationService(AppDbContext db, ILogger<NotificationService> log)
|
||||
public NotificationService(AppDbContext db, PushNotifier push, ILogger<NotificationService> log)
|
||||
{
|
||||
_db = db;
|
||||
_push = push;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
@@ -74,5 +76,9 @@ public class NotificationService
|
||||
_db.Notifications.Add(new Notification { UserId = uid, Title = title, Body = body, Url = url });
|
||||
await _db.SaveChangesAsync();
|
||||
_log.LogInformation("Notified {Count} users: {Title}", userIds.Count, title);
|
||||
|
||||
// Also push to the lock screen for users who subscribed (best-effort).
|
||||
try { await _push.PushToUsersAsync(userIds, title, body, url); }
|
||||
catch (Exception ex) { _log.LogWarning(ex, "Web push fan-out failed"); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Text.Json;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services.Scraping;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WebPush;
|
||||
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Sends Web Push notifications (lock-screen) to users' subscribed browsers via VAPID. Rides the
|
||||
/// same in-app notification records — when users are notified, their subscriptions also get a push.
|
||||
/// Config (enable + VAPID keys) comes from AppSetting; dead subscriptions (404/410) are pruned.
|
||||
/// ⚠️ Chrome push goes through FCM (may be filtered in Iran); the Bazaar TWA can use native push.
|
||||
/// </summary>
|
||||
public class PushNotifier
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly SettingsService _settings;
|
||||
private readonly ILogger<PushNotifier> _log;
|
||||
|
||||
public PushNotifier(AppDbContext db, SettingsService settings, ILogger<PushNotifier> log)
|
||||
{
|
||||
_db = db;
|
||||
_settings = settings;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public async Task PushToUsersAsync(IReadOnlyCollection<int> userIds, string title, string? body, string url)
|
||||
{
|
||||
if (userIds.Count == 0) return;
|
||||
var s = await _settings.GetAsync();
|
||||
if (!s.PushEnabled || string.IsNullOrWhiteSpace(s.VapidPublicKey) || string.IsNullOrWhiteSpace(s.VapidPrivateKey))
|
||||
return;
|
||||
|
||||
var subs = await (from sub in _db.WebPushSubscriptions
|
||||
join v in _db.Visitors on sub.VisitorId equals v.Id
|
||||
where v.UserId != null && userIds.Contains(v.UserId.Value)
|
||||
select sub).ToListAsync();
|
||||
if (subs.Count == 0) return;
|
||||
|
||||
var client = new WebPushClient();
|
||||
var vapid = new VapidDetails(s.VapidSubject ?? "mailto:admin@hamkadr.ir", s.VapidPublicKey, s.VapidPrivateKey);
|
||||
var payload = JsonSerializer.Serialize(new { title, body, url });
|
||||
|
||||
var dead = new List<WebPushSubscription>();
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.SendNotificationAsync(
|
||||
new WebPush.PushSubscription(sub.Endpoint, sub.P256dh, sub.Auth), payload, vapid);
|
||||
}
|
||||
catch (WebPushException ex) when ((int)ex.StatusCode is 404 or 410)
|
||||
{
|
||||
dead.Add(sub); // subscription expired/gone
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning(ex, "Web push send failed for one subscription");
|
||||
}
|
||||
}
|
||||
if (dead.Count > 0)
|
||||
{
|
||||
_db.WebPushSubscriptions.RemoveRange(dead);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user