| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- using Inversion;
- using Spry;
- using Spry.Authentication;
- using Statum;
- using ClankerSaddle.Services;
- namespace ClankerSaddle {
- /**
- * Stores the browser's Web Push subscription for the signed-in
- * user: `endpoint`, `p256dh` and `auth` arrive
- * from the dashboard bell's subscribe flow
- * (`pushManager.subscribe` + the VAPID key from the page state).
- * Validated per {@link PushService.validate_subscription}; the
- * endpoint is unique per browser, so a re-subscribe replaces the
- * stored keys.
- */
- public class PushSubscribeAction : StatumAction {
- private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
- private PushService push = inject<PushService>();
- private UserService users = inject<UserService>();
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- string endpoint = Forms.field(request, "endpoint");
- string p256dh = Forms.field(request, "p256dh");
- string auth = Forms.field(request, "auth");
- string problem = PushService.validate_subscription(endpoint, p256dh, auth);
- if (problem != null) {
- return directives().notify("error", @"Could not enable notifications: $((!)problem)")
- .navigate("/");
- }
- var me = SpryAuth.current(request);
- try {
- yield push.upsert_subscription(db, me.user_id, endpoint, p256dh, auth);
- return directives().notify("success",
- "Notifications enabled — you'll be told about questions and reviews")
- .navigate("/");
- } catch (Error e) {
- return directives().notify("error",
- @"Could not store the subscription: $(e.message)").navigate("/");
- }
- }
- }
- /**
- * Removes the signed-in user's subscription for an endpoint (the
- * bell's off switch; the browser already unsubscribed its side).
- */
- public class PushUnsubscribeAction : StatumAction {
- private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
- private PushService push = inject<PushService>();
- private UserService users = inject<UserService>();
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- string endpoint = Forms.field(request, "endpoint");
- try {
- var me = SpryAuth.current(request);
- yield push.remove_subscription(db, me.user_id, endpoint);
- return directives().notify("success", "Notifications turned off").navigate("/");
- } catch (Error e) {
- return directives().notify("error",
- @"Could not turn notifications off: $(e.message)").navigate("/");
- }
- }
- }
- /**
- * Flips the signed-in user's per-task "Notify me" pref: opted-in
- * users receive the task's first start, failure
- * and completion on top of the global block events every
- * push-subscribed user gets.
- */
- /**
- * The bell's "Send test notification" probe (per-browser bell):
- * queues a REAL Web Push — full RFC 8291 + VAPID path — targeted at
- * the signed-in user's own subscriptions ({@link
- * PushService.send_test}), then reports the per-endpoint outcome
- * through the notify so the UI toasts the truth. This is the
- * missing end-to-end check: a green light here proves keys,
- * crypto, the push service and the browser's subscription all
- * line up, and a failure surfaces its actual reason instead of
- * silence. Also refreshes the dashboard slot so the bell's
- * endpoint fingerprints (and a pruned dead endpoint) are current.
- */
- public class PushTestAction : StatumAction {
- private TaskService task_service = inject<TaskService>();
- private SaddleService saddle_service = inject<SaddleService>();
- private TimelineService timeline_service = inject<TimelineService>();
- private NoteService note_service = inject<NoteService>();
- private InvercargillSql.Orm.OrmSession db = inject<InvercargillSql.Orm.OrmSession>();
- private PushService push = inject<PushService>();
- private UserService users = inject<UserService>();
- private ModelConfigService model_config = inject<ModelConfigService>();
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- var me = SpryAuth.current(request);
- try {
- var report = yield push.send_test(db, me.user_id);
- string message;
- string kind;
- if (report.failed == 0 && report.pruned == 0) {
- kind = "success";
- message = report.delivered > 1
- ? @"Test notification sent to $(report.delivered) browsers"
- : "Test notification sent";
- } else if (report.delivered > 0) {
- kind = "error";
- message = @"Test notification reached $(report.delivered) browser(s)"
- + @" but $(report.failed) failed";
- if (report.first_failure.length > 0) {
- message += @" ($(report.first_failure))";
- }
- } else {
- kind = "error";
- message = "Test notification failed";
- if (report.first_failure.length > 0) {
- message += @": $(report.first_failure)";
- }
- if (report.pruned > 0) {
- message += @" — $(report.pruned) dead endpoint(s) removed";
- }
- }
- var state = yield DashboardState.build(state_service, task_service,
- saddle_service, timeline_service, db, note_service, action_registry,
- push, me.user_id, model_config);
- return directives().notify(kind, message).set(state);
- } catch (Error e) {
- return directives().notify("error",
- @"Could not send the test notification: $(e.message)");
- }
- }
- }
- public class ToggleTaskNotifyAction : SealedTaskAction {
- private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
- private PushService push = inject<PushService>();
- private UserService users = inject<UserService>();
- public override async DirectiveBuilder handle() throws GLib.Error {
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- int64 task_id = sealed_task_id();
- if (task_id <= 0) {
- return directives().navigate("/tasks");
- }
- var me = SpryAuth.current(request);
- try {
- bool enabled = !(yield push.has_notify_pref(db, me.user_id, task_id));
- yield push.set_notify_pref(db, me.user_id, task_id, enabled);
- return directives().notify("success", enabled
- ? "You'll be notified when this task starts, fails or completes"
- : "Task notifications turned off")
- .navigate(@"/tasks/$(task_id)");
- } catch (Error e) {
- return directives().notify("error",
- @"Could not toggle notifications: $(e.message)")
- .navigate(@"/tasks/$(task_id)");
- }
- }
- }
- }
|