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>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
|
<PackageReference Include="WebPush" Version="1.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ builder.Services.AddScoped<OtpService>();
|
|||||||
builder.Services.AddSingleton<CaptchaService>();
|
builder.Services.AddSingleton<CaptchaService>();
|
||||||
builder.Services.AddScoped<SubmissionGuard>();
|
builder.Services.AddScoped<SubmissionGuard>();
|
||||||
builder.Services.AddScoped<NotificationService>();
|
builder.Services.AddScoped<NotificationService>();
|
||||||
|
builder.Services.AddScoped<PushNotifier>();
|
||||||
// Listing parser: heuristic now; swap for an LLM-backed IListingParser later.
|
// Listing parser: heuristic now; swap for an LLM-backed IListingParser later.
|
||||||
builder.Services.AddSingleton<IListingParser, HeuristicListingParser>();
|
builder.Services.AddSingleton<IListingParser, HeuristicListingParser>();
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ namespace JobsMedical.Web.Services;
|
|||||||
public class NotificationService
|
public class NotificationService
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
private readonly PushNotifier _push;
|
||||||
private readonly ILogger<NotificationService> _log;
|
private readonly ILogger<NotificationService> _log;
|
||||||
|
|
||||||
public NotificationService(AppDbContext db, ILogger<NotificationService> log)
|
public NotificationService(AppDbContext db, PushNotifier push, ILogger<NotificationService> log)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_push = push;
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,5 +76,9 @@ public class NotificationService
|
|||||||
_db.Notifications.Add(new Notification { UserId = uid, Title = title, Body = body, Url = url });
|
_db.Notifications.Add(new Notification { UserId = uid, Title = title, Body = body, Url = url });
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
_log.LogInformation("Notified {Count} users: {Title}", userIds.Count, title);
|
_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