aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--syz-cluster/email-reporter/main.go1
-rw-r--r--syz-cluster/pkg/api/reporter.go28
-rw-r--r--syz-cluster/pkg/db/entities.go6
-rw-r--r--syz-cluster/pkg/db/migrations/1_initialize.up.sql13
-rw-r--r--syz-cluster/pkg/db/report_reply_repo.go92
-rw-r--r--syz-cluster/pkg/db/report_reply_repo_test.go61
-rw-r--r--syz-cluster/pkg/db/report_repo.go11
-rw-r--r--syz-cluster/pkg/db/report_repo_test.go22
-rw-r--r--syz-cluster/pkg/db/spanner.go7
-rw-r--r--syz-cluster/pkg/db/util_test.go8
-rw-r--r--syz-cluster/pkg/reporter/api.go36
-rw-r--r--syz-cluster/pkg/reporter/api_test.go78
-rw-r--r--syz-cluster/pkg/service/discussion.go80
-rw-r--r--syz-cluster/reporter-server/main.go3
14 files changed, 430 insertions, 16 deletions
diff --git a/syz-cluster/email-reporter/main.go b/syz-cluster/email-reporter/main.go
index 24b4adb7f..8726204ee 100644
--- a/syz-cluster/email-reporter/main.go
+++ b/syz-cluster/email-reporter/main.go
@@ -11,6 +11,7 @@ import (
"time"
"github.com/google/syzkaller/pkg/email"
+ "github.com/google/syzkaller/syz-cluster/pkg/api"
"github.com/google/syzkaller/syz-cluster/pkg/app"
)
diff --git a/syz-cluster/pkg/api/reporter.go b/syz-cluster/pkg/api/reporter.go
index 5a7b7bfb4..e73bdb668 100644
--- a/syz-cluster/pkg/api/reporter.go
+++ b/syz-cluster/pkg/api/reporter.go
@@ -7,6 +7,7 @@ import (
"context"
"net/url"
"strings"
+ "time"
)
type ReporterClient struct {
@@ -53,3 +54,30 @@ func (client ReporterClient) UpstreamReport(ctx context.Context, id string, req
_, err := postJSON[UpstreamReportReq, any](ctx, client.baseURL+"/reports/"+id+"/upstream", req)
return err
}
+
+type RecordReplyReq struct {
+ MessageID string `json:"message_id"`
+ InReplyTo string `json:"in_reply_to"`
+ Reporter string `json:"reporter"`
+ Time time.Time `json:"time"`
+}
+
+type RecordReplyResp struct {
+ New bool `json:"new"`
+ ReportID string `json:"report_id"` // or empty, if no original message was found
+}
+
+func (client ReporterClient) RecordReply(ctx context.Context, req *RecordReplyReq) (*RecordReplyResp, error) {
+ return postJSON[RecordReplyReq, RecordReplyResp](ctx, client.baseURL+"/reports/record_reply", req)
+}
+
+type LastReplyResp struct {
+ Time time.Time `json:"time"`
+}
+
+// Returns nil if no reply has ever been recorded.
+func (client ReporterClient) LastReply(ctx context.Context, reporter string) (*LastReplyResp, error) {
+ v := url.Values{}
+ v.Add("reporter", reporter)
+ return postJSON[any, LastReplyResp](ctx, client.baseURL+"/reports/last_reply?"+v.Encode(), nil)
+}
diff --git a/syz-cluster/pkg/db/entities.go b/syz-cluster/pkg/db/entities.go
index a7f08f9ab..c6c469131 100644
--- a/syz-cluster/pkg/db/entities.go
+++ b/syz-cluster/pkg/db/entities.go
@@ -141,3 +141,9 @@ type SessionReport struct {
func (s *SessionReport) SetReportedAt(t time.Time) {
s.ReportedAt = spanner.NullTime{Time: t, Valid: true}
}
+
+type ReportReply struct {
+ MessageID string `spanner:"MessageID"`
+ ReportID string `spanner:"ReportID"`
+ Time time.Time `spanner:"Time"`
+}
diff --git a/syz-cluster/pkg/db/migrations/1_initialize.up.sql b/syz-cluster/pkg/db/migrations/1_initialize.up.sql
index ca91b343a..3100c9bb2 100644
--- a/syz-cluster/pkg/db/migrations/1_initialize.up.sql
+++ b/syz-cluster/pkg/db/migrations/1_initialize.up.sql
@@ -100,14 +100,23 @@ CREATE UNIQUE INDEX NoDupFindings ON Findings(SessionID, TestName, Title);
-- Session's bug reports.
CREATE TABLE SessionReports (
- ID STRING(36) NOT NULL, -- UUID??
+ ID STRING(36) NOT NULL, -- UUID
SessionID STRING(36) NOT NULL, -- UUID
ReportedAt TIMESTAMP,
Moderation BOOL,
- MessageID STRING(256),
+ MessageID STRING(512),
Reporter STRING(256),
CONSTRAINT FK_SessionReports FOREIGN KEY (SessionID) REFERENCES Sessions (ID),
) PRIMARY KEY(ID);
CREATE UNIQUE INDEX NoDupSessionReports ON SessionReports(SessionID, Moderation);
CREATE INDEX SessionReportsByStatus ON SessionReports (Reporter, ReportedAt);
+CREATE INDEX SessionReportsByMessageID ON SessionReports(Reporter, MessageID);
+
+-- Replies on a session report.
+CREATE TABLE ReportReplies (
+ MessageID STRING(512) NOT NULL, -- Gmail sets a limit of 500 characters for Message-ID
+ ReportID STRING(36) NOT NULL, -- UUID
+ Time TIMESTAMP,
+ CONSTRAINT FK_ReplyReportID FOREIGN KEY (ReportID) REFERENCES SessionReports (ID),
+) PRIMARY KEY(MessageID, ReportID);
diff --git a/syz-cluster/pkg/db/report_reply_repo.go b/syz-cluster/pkg/db/report_reply_repo.go
new file mode 100644
index 000000000..7e21403c2
--- /dev/null
+++ b/syz-cluster/pkg/db/report_reply_repo.go
@@ -0,0 +1,92 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package db
+
+import (
+ "context"
+ "errors"
+
+ "cloud.google.com/go/spanner"
+)
+
+type ReportReplyRepository struct {
+ client *spanner.Client
+}
+
+func NewReportReplyRepository(client *spanner.Client) *ReportReplyRepository {
+ return &ReportReplyRepository{
+ client: client,
+ }
+}
+
+func (repo *ReportReplyRepository) FindParentReportID(ctx context.Context, reporter, messageID string) (string, error) {
+ stmt := spanner.Statement{
+ SQL: "SELECT `ReportReplies`.ReportID FROM `ReportReplies` " +
+ "JOIN `SessionReports` ON `SessionReports`.ID = `ReportReplies`.ReportID " +
+ "WHERE `ReportReplies`.MessageID = @messageID " +
+ "AND `SessionReports`.Reporter = @reporter LIMIT 1",
+ Params: map[string]interface{}{
+ "reporter": reporter,
+ "messageID": messageID,
+ },
+ }
+ iter := repo.client.Single().Query(ctx, stmt)
+ defer iter.Stop()
+
+ type result struct {
+ ReportID string `spanner:"ReportID"`
+ }
+ ret, err := readOne[result](iter)
+ if err != nil {
+ return "", err
+ } else if ret != nil {
+ return ret.ReportID, nil
+ }
+ return "", nil
+}
+
+var ErrReportReplyExists = errors.New("the reply has already been recorded")
+
+func (repo *ReportReplyRepository) Insert(ctx context.Context, reply *ReportReply) error {
+ _, err := repo.client.ReadWriteTransaction(ctx,
+ func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
+ stmt := spanner.Statement{
+ SQL: "SELECT * from `ReportReplies` " +
+ "WHERE `ReportID`=@reportID AND `MessageID`=@messageID",
+ Params: map[string]interface{}{
+ "reportID": reply.ReportID,
+ "messageID": reply.MessageID,
+ },
+ }
+ iter := txn.Query(ctx, stmt)
+ entity, err := readOne[ReportReply](iter)
+ iter.Stop()
+ if err != nil {
+ return err
+ } else if entity != nil {
+ return ErrReportReplyExists
+ }
+ insert, err := spanner.InsertStruct("ReportReplies", reply)
+ if err != nil {
+ return err
+ }
+ return txn.BufferWrite([]*spanner.Mutation{insert})
+ })
+ return err
+}
+
+func (repo *ReportReplyRepository) LastForReporter(ctx context.Context, reporter string) (*ReportReply, error) {
+ stmt := spanner.Statement{
+ SQL: "SELECT `ReportReplies`.* FROM `ReportReplies` " +
+ "JOIN `SessionReports` ON `SessionReports`.ID=`ReportReplies`.ReportID " +
+ "WHERE `SessionReports`.Reporter=@reporter " +
+ "ORDER BY `Time` DESC LIMIT 1",
+ Params: map[string]interface{}{
+ "reporter": reporter,
+ },
+ }
+ iter := repo.client.Single().Query(ctx, stmt)
+ defer iter.Stop()
+ return readOne[ReportReply](iter)
+}
diff --git a/syz-cluster/pkg/db/report_reply_repo_test.go b/syz-cluster/pkg/db/report_reply_repo_test.go
new file mode 100644
index 000000000..27c2849cd
--- /dev/null
+++ b/syz-cluster/pkg/db/report_reply_repo_test.go
@@ -0,0 +1,61 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package db
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestReportReplyRepository(t *testing.T) {
+ client, ctx := NewTransientDB(t)
+ dtd := &dummyTestData{t, ctx, client}
+ session := dtd.dummySession(dtd.dummySeries())
+
+ reportRepo := NewReportRepository(client)
+ report := &SessionReport{SessionID: session.ID, Reporter: dummyReporter}
+ err := reportRepo.Insert(ctx, report)
+ assert.NoError(t, err)
+
+ replyRepo := NewReportReplyRepository(client)
+ baseTime := time.Now()
+ for i := 0; i < 2; i++ {
+ err = replyRepo.Insert(ctx, &ReportReply{
+ MessageID: fmt.Sprintf("message-id-%d", i),
+ ReportID: report.ID,
+ Time: baseTime.Add(time.Duration(i) * time.Second),
+ })
+ assert.NoError(t, err)
+ }
+
+ t.Run("insert-dup-reply", func(t *testing.T) {
+ err := replyRepo.Insert(ctx, &ReportReply{
+ MessageID: "message-id-0",
+ ReportID: report.ID,
+ Time: time.Now(),
+ })
+ assert.Error(t, ErrReportReplyExists, err)
+ })
+
+ t.Run("last-report", func(t *testing.T) {
+ reply, err := replyRepo.LastForReporter(ctx, dummyReporter)
+ assert.NoError(t, err)
+ assert.Equal(t, "message-id-1", reply.MessageID)
+ })
+
+ t.Run("last-report-unknown", func(t *testing.T) {
+ reply, err := replyRepo.LastForReporter(ctx, "unknown-reporter")
+ assert.NoError(t, err)
+ assert.Nil(t, reply)
+ })
+
+ t.Run("find-by-parent", func(t *testing.T) {
+ reportID, err := replyRepo.FindParentReportID(ctx, dummyReporter, "message-id-0")
+ assert.NoError(t, err)
+ assert.Equal(t, report.ID, reportID)
+ })
+}
diff --git a/syz-cluster/pkg/db/report_repo.go b/syz-cluster/pkg/db/report_repo.go
index 4e039c4a7..37a09a746 100644
--- a/syz-cluster/pkg/db/report_repo.go
+++ b/syz-cluster/pkg/db/report_repo.go
@@ -44,3 +44,14 @@ func (repo *ReportRepository) ListNotReported(ctx context.Context, reporter stri
addLimit(&stmt, limit)
return repo.readEntities(ctx, stmt)
}
+
+func (repo *ReportRepository) FindByMessageID(ctx context.Context, reporter, messageID string) (*SessionReport, error) {
+ stmt := spanner.Statement{
+ SQL: "SELECT * FROM `SessionReports` WHERE `Reporter` = @reporter AND `MessageID` = @messageID",
+ Params: map[string]interface{}{
+ "reporter": reporter,
+ "messageID": messageID,
+ },
+ }
+ return repo.readEntity(ctx, stmt)
+}
diff --git a/syz-cluster/pkg/db/report_repo_test.go b/syz-cluster/pkg/db/report_repo_test.go
index bc81c37bd..a0cee2a11 100644
--- a/syz-cluster/pkg/db/report_repo_test.go
+++ b/syz-cluster/pkg/db/report_repo_test.go
@@ -40,16 +40,30 @@ func TestReportRepository(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, list, 3)
+ const messageID = "message-id"
err = reportRepo.Update(ctx, keys[0], func(rep *SessionReport) error {
rep.SetReportedAt(time.Now())
+ rep.MessageID = messageID
return nil
})
assert.NoError(t, err)
- // Now one less.
- list, err = reportRepo.ListNotReported(ctx, dummyReporter, 10)
- assert.NoError(t, err)
- assert.Len(t, list, 2)
+ t.Run("not-reported-count", func(t *testing.T) {
+ // Now one less.
+ list, err := reportRepo.ListNotReported(ctx, dummyReporter, 10)
+ assert.NoError(t, err)
+ assert.Len(t, list, 2)
+ })
+ t.Run("find-by-id-found", func(t *testing.T) {
+ report, err := reportRepo.FindByMessageID(ctx, dummyReporter, messageID)
+ assert.NoError(t, err)
+ assert.NotNil(t, report)
+ })
+ t.Run("find-by-id-empty", func(t *testing.T) {
+ report, err := reportRepo.FindByMessageID(ctx, dummyReporter, "non-existing-id")
+ assert.NoError(t, err)
+ assert.Nil(t, report)
+ })
}
func TestSessionsWithoutReports(t *testing.T) {
diff --git a/syz-cluster/pkg/db/spanner.go b/syz-cluster/pkg/db/spanner.go
index d114727b8..8db081c14 100644
--- a/syz-cluster/pkg/db/spanner.go
+++ b/syz-cluster/pkg/db/spanner.go
@@ -333,3 +333,10 @@ func (g *genericEntityOps[EntityType, KeyType]) readEntities(ctx context.Context
defer iter.Stop()
return readEntities[EntityType](iter)
}
+
+func (g *genericEntityOps[EntityType, KeyType]) readEntity(ctx context.Context,
+ stmt spanner.Statement) (*EntityType, error) {
+ iter := g.client.Single().Query(ctx, stmt)
+ defer iter.Stop()
+ return readOne[EntityType](iter)
+}
diff --git a/syz-cluster/pkg/db/util_test.go b/syz-cluster/pkg/db/util_test.go
index 64fedeab2..cd9e1fff7 100644
--- a/syz-cluster/pkg/db/util_test.go
+++ b/syz-cluster/pkg/db/util_test.go
@@ -31,6 +31,14 @@ func (d *dummyTestData) addSessionTest(session *Session, names ...string) {
}
}
+func (d *dummyTestData) dummySeries() *Series {
+ seriesRepo := NewSeriesRepository(d.client)
+ series := &Series{ExtID: "series-ext-id"}
+ err := seriesRepo.Insert(d.ctx, series, nil)
+ assert.NoError(d.t, err)
+ return series
+}
+
func (d *dummyTestData) dummySession(series *Series) *Session {
sessionRepo := NewSessionRepository(d.client)
session := &Session{
diff --git a/syz-cluster/pkg/reporter/api.go b/syz-cluster/pkg/reporter/api.go
index d2c23c912..e06e1312c 100644
--- a/syz-cluster/pkg/reporter/api.go
+++ b/syz-cluster/pkg/reporter/api.go
@@ -16,11 +16,15 @@ import (
)
type APIServer struct {
- service *service.ReportService
+ reportService *service.ReportService
+ discussionService *service.DiscussionService
}
-func NewAPIServer(service *service.ReportService) *APIServer {
- return &APIServer{service: service}
+func NewAPIServer(env *app.AppEnvironment) *APIServer {
+ return &APIServer{
+ reportService: service.NewReportService(env),
+ discussionService: service.NewDiscussionService(env),
+ }
}
func (s *APIServer) Mux() *http.ServeMux {
@@ -28,6 +32,8 @@ func (s *APIServer) Mux() *http.ServeMux {
mux.HandleFunc("/reports/{report_id}/update", s.updateReport)
mux.HandleFunc("/reports/{report_id}/upstream", s.upstreamReport)
mux.HandleFunc("/reports/{report_id}/confirm", s.confirmReport)
+ mux.HandleFunc("/reports/record_reply", s.recordReply)
+ mux.HandleFunc("/reports/last_reply", s.lastReply)
mux.HandleFunc("/reports", s.nextReports)
return mux
}
@@ -38,7 +44,7 @@ func (s *APIServer) updateReport(w http.ResponseWriter, r *http.Request) {
if req == nil {
return // TODO: return StatusBadRequest here and below.
}
- err := s.service.Update(r.Context(), r.PathValue("report_id"), req)
+ err := s.reportService.Update(r.Context(), r.PathValue("report_id"), req)
reply[interface{}](w, nil, err)
}
@@ -49,20 +55,34 @@ func (s *APIServer) upstreamReport(w http.ResponseWriter, r *http.Request) {
return
}
// TODO: journal the action.
- err := s.service.Upstream(r.Context(), r.PathValue("report_id"), req)
+ err := s.reportService.Upstream(r.Context(), r.PathValue("report_id"), req)
reply[interface{}](w, nil, err)
}
func (s *APIServer) nextReports(w http.ResponseWriter, r *http.Request) {
- resp, err := s.service.Next(r.Context(), r.FormValue("reporter"))
+ resp, err := s.reportService.Next(r.Context(), r.FormValue("reporter"))
reply(w, resp, err)
}
func (s *APIServer) confirmReport(w http.ResponseWriter, r *http.Request) {
- err := s.service.Confirm(r.Context(), r.PathValue("report_id"))
+ err := s.reportService.Confirm(r.Context(), r.PathValue("report_id"))
reply[interface{}](w, nil, err)
}
+func (s *APIServer) recordReply(w http.ResponseWriter, r *http.Request) {
+ req := api.ParseJSON[api.RecordReplyReq](w, r)
+ if req == nil {
+ return
+ }
+ resp, err := s.discussionService.RecordReply(r.Context(), req)
+ reply(w, resp, err)
+}
+
+func (s *APIServer) lastReply(w http.ResponseWriter, r *http.Request) {
+ resp, err := s.discussionService.LastReply(r.Context(), r.PathValue("reporter"))
+ reply(w, resp, err)
+}
+
func reply[T any](w http.ResponseWriter, obj T, err error) {
if errors.Is(err, service.ErrReportNotFound) {
http.Error(w, fmt.Sprint(err), http.StatusNotFound)
@@ -78,7 +98,7 @@ func reply[T any](w http.ResponseWriter, obj T, err error) {
}
func TestServer(t *testing.T, env *app.AppEnvironment) *api.ReporterClient {
- apiServer := NewAPIServer(service.NewReportService(env))
+ apiServer := NewAPIServer(env)
server := httptest.NewServer(apiServer.Mux())
t.Cleanup(server.Close)
return api.NewReporterClient(server.URL)
diff --git a/syz-cluster/pkg/reporter/api_test.go b/syz-cluster/pkg/reporter/api_test.go
index 599c10e9c..9f94ce5b6 100644
--- a/syz-cluster/pkg/reporter/api_test.go
+++ b/syz-cluster/pkg/reporter/api_test.go
@@ -5,6 +5,7 @@ package reporter
import (
"testing"
+ "time"
"github.com/google/syzkaller/syz-cluster/pkg/api"
"github.com/google/syzkaller/syz-cluster/pkg/app"
@@ -84,3 +85,80 @@ func TestAPIReportFlow(t *testing.T) {
assert.False(t, nextResp3.Report.Moderation)
assert.Equal(t, nextResp2.Report.Series, nextResp3.Report.Series)
}
+
+func TestReplyReporting(t *testing.T) {
+ env, ctx := app.TestEnvironment(t)
+ client := controller.TestServer(t, env)
+
+ // Create series/session/test/findings.
+ testSeries := controller.DummySeries()
+ controller.FakeSeriesWithFindings(t, ctx, env, client, testSeries)
+
+ generator := NewGenerator(env)
+ err := generator.Process(ctx, 1)
+ assert.NoError(t, err)
+
+ // Create a report.
+ reportClient := TestServer(t, env)
+ nextResp, err := reportClient.GetNextReport(ctx, api.LKMLReporter)
+ assert.NoError(t, err)
+
+ // Confirm the report and set its message ID.
+ reportID := nextResp.Report.ID
+ err = reportClient.ConfirmReport(ctx, reportID)
+ assert.NoError(t, err)
+
+ err = reportClient.UpdateReport(ctx, reportID, &api.UpdateReportReq{
+ MessageID: "message-id",
+ })
+ assert.NoError(t, err)
+
+ // Direct reply to the report.
+ resp, err := reportClient.RecordReply(ctx, &api.RecordReplyReq{
+ MessageID: "direct-reply-id",
+ InReplyTo: "message-id",
+ Reporter: api.LKMLReporter,
+ Time: time.Now(),
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &api.RecordReplyResp{
+ New: true,
+ ReportID: reportID,
+ }, resp)
+
+ // Reply to the reply.
+ replyToReply := &api.RecordReplyReq{
+ MessageID: "reply-to-reply-id",
+ InReplyTo: "direct-reply-id",
+ Reporter: api.LKMLReporter,
+ Time: time.Now(),
+ }
+ resp, err = reportClient.RecordReply(ctx, replyToReply)
+ assert.NoError(t, err)
+ assert.Equal(t, &api.RecordReplyResp{
+ New: true,
+ ReportID: reportID,
+ }, resp)
+
+ t.Run("dup-report", func(t *testing.T) {
+ resp, err := reportClient.RecordReply(ctx, replyToReply)
+ assert.NoError(t, err)
+ assert.Equal(t, &api.RecordReplyResp{
+ New: false,
+ ReportID: reportID,
+ }, resp)
+ })
+
+ t.Run("unknown-message", func(t *testing.T) {
+ resp, err := reportClient.RecordReply(ctx, &api.RecordReplyReq{
+ MessageID: "whatever",
+ InReplyTo: "unknown-id",
+ Reporter: api.LKMLReporter,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, &api.RecordReplyResp{
+ New: false,
+ ReportID: "",
+ }, resp)
+ })
+}
diff --git a/syz-cluster/pkg/service/discussion.go b/syz-cluster/pkg/service/discussion.go
new file mode 100644
index 000000000..f4be21fbd
--- /dev/null
+++ b/syz-cluster/pkg/service/discussion.go
@@ -0,0 +1,80 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package service
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/google/syzkaller/syz-cluster/pkg/api"
+ "github.com/google/syzkaller/syz-cluster/pkg/app"
+ "github.com/google/syzkaller/syz-cluster/pkg/db"
+)
+
+// DiscussionService implements the functionality necessary for tracking replies under the bug reports.
+// Each report is assumed to have an ID and have an InReplyTo ID that either points to another reply or
+// to the original bug report.
+// DiscussionService offers the methods to record such replies and, for each reply, to determine the original
+// discussed bug report.
+type DiscussionService struct {
+ reportRepo *db.ReportRepository
+ reportReplyRepo *db.ReportReplyRepository
+}
+
+func NewDiscussionService(env *app.AppEnvironment) *DiscussionService {
+ return &DiscussionService{
+ reportRepo: db.NewReportRepository(env.Spanner),
+ reportReplyRepo: db.NewReportReplyRepository(env.Spanner),
+ }
+}
+
+func (d *DiscussionService) RecordReply(ctx context.Context, req *api.RecordReplyReq) (*api.RecordReplyResp, error) {
+ // First check if the message was a directl reply to the report.
+ report, err := d.reportRepo.FindByMessageID(ctx, req.Reporter, req.InReplyTo)
+ if err != nil {
+ return nil, fmt.Errorf("failed to search among the reports: %w", err)
+ }
+ var reportID string
+ if report != nil {
+ reportID = report.ID
+ } else {
+ // Now try to find a matching reply.
+ reportID, err = d.reportReplyRepo.FindParentReportID(ctx, req.Reporter, req.InReplyTo)
+ if err != nil {
+ return nil, fmt.Errorf("failed to search among the replies: %w", err)
+ }
+ }
+ if reportID == "" {
+ // We could not find the related report.
+ return &api.RecordReplyResp{}, nil
+ }
+ err = d.reportReplyRepo.Insert(ctx, &db.ReportReply{
+ ReportID: reportID,
+ MessageID: req.MessageID,
+ Time: req.Time,
+ })
+ if errors.Is(err, db.ErrReportReplyExists) {
+ return &api.RecordReplyResp{
+ ReportID: reportID,
+ }, nil
+ } else if err != nil {
+ return nil, fmt.Errorf("failed to save the reply: %w", err)
+ }
+ return &api.RecordReplyResp{
+ ReportID: reportID,
+ New: true,
+ }, nil
+}
+
+func (d *DiscussionService) LastReply(ctx context.Context, reporter string) (*api.LastReplyResp, error) {
+ reply, err := d.reportReplyRepo.LastForReporter(ctx, reporter)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query the last report: %w", err)
+ }
+ if reply != nil {
+ return &api.LastReplyResp{Time: reply.Time}, nil
+ }
+ return &api.LastReplyResp{}, nil
+}
diff --git a/syz-cluster/reporter-server/main.go b/syz-cluster/reporter-server/main.go
index c7d8b88f1..23197cff3 100644
--- a/syz-cluster/reporter-server/main.go
+++ b/syz-cluster/reporter-server/main.go
@@ -10,7 +10,6 @@ import (
"github.com/google/syzkaller/syz-cluster/pkg/app"
"github.com/google/syzkaller/syz-cluster/pkg/reporter"
- "github.com/google/syzkaller/syz-cluster/pkg/service"
)
func main() {
@@ -23,7 +22,7 @@ func main() {
generator := reporter.NewGenerator(env)
go generator.Loop(ctx)
- api := reporter.NewAPIServer(service.NewReportService(env))
+ api := reporter.NewAPIServer(env)
log.Printf("listening on port 8080")
app.Fatalf("listen failed: %v", http.ListenAndServe(":8080", api.Mux()))
}