clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

wake the requester for job results and failing feedback

Dami committed d2f0451
commit
d2f0451b3282236f6c81a4c748f39a8687318c3f
parent
51a4c905ad75f7693542b26289ca654ac8926bcc
author
Dami <dami.etoile@gmail.com>
2 files changed, +73 -0
Minternal/daemon/jobs_test.go32+++++++++++++++++++++++
Minternal/daemon/webpush_triggers.go41++++++++++++++++++++++++++++++
M internal/daemon/jobs_test.go +32 -0
@@ -169,6 +169,38 @@ func TestMCPJobToolsBuildValidateAndPublish(t *testing.T) {
169169 }170170 }171171 172+func TestJobAnswersWakeTheRequesterAsMentions(t *testing.T) {173+ _, tenant, member, agent := jobTenant(t)174+ ctx := context.Background()175+ now := time.Now().Unix()176+ request := wikiPublish(t, tenant, testMemberSecret, 5001, now-100, "", []string{"i", "hello", "text"})177+ result := event.Event{Kind: 6001, PubKey: agent, CreatedAt: now, Tags: [][]string{{"e", request.ID}, {"p", member}}, Content: "hola"}178+ notices := tenant.pushNotices(ctx, result)179+ if len(notices) != 1 || notices[0].category != pushMentions || notices[0].recipient != member || notices[0].body != "job 5001 result" || !strings.HasSuffix(notices[0].url, "/e/"+result.ID) || len(notices[0].actions) != 0 {180+ t.Fatalf("result notices %+v", notices)181+ }182+ for status, want := range map[string]int{"error": 1, "payment-required": 1, "processing": 0, "success": 0, "partial": 0} {183+ feedback := event.Event{Kind: 7000, PubKey: agent, CreatedAt: now, Tags: [][]string{{"status", status}, {"e", request.ID}, {"p", member}}}184+ notices := tenant.pushNotices(ctx, feedback)185+ if len(notices) != want {186+ t.Fatalf("%s feedback notices %+v", status, notices)187+ }188+ if want == 1 && (notices[0].category != pushMentions || notices[0].body != "job 5001 "+status) {189+ t.Fatalf("%s feedback notice %+v", status, notices[0])190+ }191+ }192+ // Feedback for a request the relay does not hold names its own kind.193+ elsewhere := event.Event{Kind: 7000, PubKey: agent, CreatedAt: now, Tags: [][]string{{"status", "error"}, {"e", strings.Repeat("e", 64)}, {"p", member}}}194+ if notices := tenant.pushNotices(ctx, elsewhere); len(notices) != 1 || notices[0].body != "job 7000 error" {195+ t.Fatalf("elsewhere notices %+v", notices)196+ }197+ // The requester's own feedback wakes nobody.198+ own := event.Event{Kind: 7000, PubKey: member, CreatedAt: now, Tags: [][]string{{"status", "error"}, {"e", request.ID}, {"p", member}}}199+ if notices := tenant.pushNotices(ctx, own); len(notices) != 0 {200+ t.Fatalf("own notices %+v", notices)201+ }202+}203+172204 func TestBrowseJobsListsRequestsWithStatusAndResults(t *testing.T) {173205 _, tenant, member, agent := jobTenant(t)174206 ctx := context.Background()
M internal/daemon/webpush_triggers.go +41 -0
@@ -91,6 +91,15 @@ func (t *Tenant) pushNotices(ctx context.Context, e event.Event) []pushNotice {
9191 if kind, ok := approvalRequest(e); ok {9292 return t.approvalNotices(e, kind)9393 }94+ // A job result, or feedback that needs the requester's attention,95+ // reaches the requester as a mention.96+ if status, ok := jobNotice(e); ok {97+ body := "job " + t.jobRequestKind(ctx, e) + " " + status98+ for _, recipient := range pushRecipients(e) {99+ notices = append(notices, pushNotice{recipient: recipient, category: pushMentions, body: body, url: base + "/e/" + e.ID})100+ }101+ return notices102+ }94103 switch e.Kind {95104 case event.KIND_WRAP, 4:96105 for _, recipient := range pushRecipients(e) {
@@ -146,6 +155,38 @@ func (t *Tenant) pushNotices(ctx context.Context, e event.Event) []pushNotice {
146155 return notices147156 }148157 158+// jobNotice reports whether a long task event wakes its requester and with159+// which status: every result does, and feedback does when it asks for160+// payment or reports an error. Progress and success feedback stay quiet,161+// since the result follows.162+func jobNotice(e event.Event) (string, bool) {163+ if event.IsJobResult(e.Kind) {164+ return "result", true165+ }166+ if e.Kind != event.KIND_JOB_FEEDBACK {167+ return "", false168+ }169+ status := event.Tag(e, "status")170+ return status, status == "error" || status == "payment-required"171+}172+173+// jobRequestKind names the request kind an answer belongs to: a result174+// says it by its own kind, and feedback by the request the relay holds.175+func (t *Tenant) jobRequestKind(ctx context.Context, e event.Event) string {176+ if event.IsJobResult(e.Kind) {177+ return strconv.Itoa(e.Kind - 1000)178+ }179+ id := event.Tag(e, "e")180+ if len(id) != 64 {181+ return strconv.Itoa(e.Kind)182+ }183+ rows, err := t.store.Query(ctx, event.Filter{IDs: []string{id}, Tags: map[string][]string{}}, storage.QueryOptions{Now: time.Now().Unix(), Access: storage.Access{All: true}, Limit: 1})184+ if err != nil || len(rows.Events) == 0 || !event.IsJobRequest(rows.Events[0].Kind) {185+ return strconv.Itoa(e.Kind)186+ }187+ return strconv.Itoa(rows.Events[0].Kind)188+}189+149190 // pushRecipients lists the addressed keys other than the author, bounded so150191 // a broadcast to a large list cannot fan out to every device.151192 func pushRecipients(e event.Event) []string {