From cb52d32d683effe005740305513bd59c2d7e4842 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Tue, 4 Apr 2023 15:02:23 -0700 Subject: [PATCH] labelmaker: progress on admin XRPC endpoints --- labeling/admin.go | 84 ++++++++++++++++++++----- labeling/helpers_test.go | 128 ++++++++++++++++++++++++++++++++++++- labeling/service.go | 1 + labeling/xrpc_handlers.go | 45 ++++++++----- labeling/xrpc_test.go | 129 +++++++++++++++++++++++++++++++++----- models/moderation.go | 10 ++- 6 files changed, 348 insertions(+), 49 deletions(-) diff --git a/labeling/admin.go b/labeling/admin.go index 13b6f4bc..17772ad9 100644 --- a/labeling/admin.go +++ b/labeling/admin.go @@ -14,10 +14,10 @@ import ( // This is probably only a temporary method func (s *Server) hydrateRepoView(ctx context.Context, did, indexedAt string) *comatproto.AdminDefs_RepoView { return &comatproto.AdminDefs_RepoView{ - // TODO(bnewbold): populate more, or more correctly, from some backend? + // XXX(bnewbold): populate more, or more correctly, from some backend? Did: did, Email: nil, - Handle: "TODO", + Handle: "XXX", IndexedAt: indexedAt, Moderation: nil, RelatedRecords: nil, @@ -27,7 +27,7 @@ func (s *Server) hydrateRepoView(ctx context.Context, did, indexedAt string) *co // This is probably only a temporary method func (s *Server) hydrateRecordView(ctx context.Context, did string, uri, cid *string, indexedAt string) *comatproto.AdminDefs_RecordView { repoView := s.hydrateRepoView(ctx, did, indexedAt) - // TODO(bnewbold): populate more, or more correctly, from some backend? + // XXX(bnewbold): populate more, or more correctly, from some backend? recordView := comatproto.AdminDefs_RecordView{ BlobCids: []string{}, IndexedAt: indexedAt, @@ -45,14 +45,31 @@ func (s *Server) hydrateRecordView(ctx context.Context, did string, uri, cid *st return &recordView } -func (s *Server) hydrateModerationActions(ctx context.Context, rows []models.ModerationAction) ([]*comatproto.AdminDefs_ActionView, error) { +func (s *Server) hydrateModerationActionViews(ctx context.Context, rows []models.ModerationAction) ([]*comatproto.AdminDefs_ActionView, error) { var out []*comatproto.AdminDefs_ActionView for _, row := range rows { - // TODO(bnewbold): resolve these + resolvedReportIds := []int64{} + var resolutionRows []models.ModerationReportResolution + result := s.db.Where("action_id = ?", row.ID).Find(&resolutionRows) + if result.Error != nil { + return nil, result.Error + } + for _, row := range resolutionRows { + resolvedReportIds = append(resolvedReportIds, int64(row.ReportId)) + } + subjectBlobCIDs := []string{} + var cidRows []models.ModerationActionSubjectBlobCid + result = s.db.Where("action_id = ?", row.ID).Find(&cidRows) + if result.Error != nil { + return nil, result.Error + } + for _, row := range cidRows { + subjectBlobCIDs = append(subjectBlobCIDs, row.Cid) + } var reversal *comatproto.AdminDefs_ActionReversal if row.ReversedAt != nil { @@ -104,9 +121,34 @@ func (s *Server) hydrateModerationActionDetails(ctx context.Context, rows []mode var out []*comatproto.AdminDefs_ActionViewDetail for _, row := range rows { - // TODO(bnewbold): resolve these - resolvedReports := []*comatproto.AdminDefs_ReportView{} - subjectBlobs := []*comatproto.AdminDefs_BlobView{} + var reportRows []models.ModerationReport + result := s.db.Joins("left join moderation_report_resolutions on moderation_report_resolutions.report_id = moderation_reports.id").Where("moderation_report_resolutions.action_id = ?", row.ID).Find(&reportRows) + if result.Error != nil { + return nil, result.Error + } + resolvedReports, err := s.hydrateModerationReportViews(ctx, reportRows) + if err != nil { + return nil, err + } + + subjectBlobViews := []*comatproto.AdminDefs_BlobView{} + var cidRows []models.ModerationActionSubjectBlobCid + result = s.db.Where("action_id = ?", row.ID).Find(&cidRows) + if result.Error != nil { + return nil, result.Error + } + for _, row := range cidRows { + subjectBlobViews = append(subjectBlobViews, &comatproto.AdminDefs_BlobView{ + Cid: row.Cid, + /* XXX: all these other fields + CreatedAt string + Details *AdminDefs_BlobView_Details + MimeType string + Moderation *AdminDefs_Moderation + Size int64 + */ + }) + } var reversal *comatproto.AdminDefs_ActionReversal if row.ReversedAt != nil { @@ -139,19 +181,26 @@ func (s *Server) hydrateModerationActionDetails(ctx context.Context, rows []mode ResolvedReports: resolvedReports, Reversal: reversal, Subject: subj, - SubjectBlobs: subjectBlobs, + SubjectBlobs: subjectBlobViews, } out = append(out, viewDetail) } return out, nil } -func (s *Server) hydrateModerationReports(ctx context.Context, rows []models.ModerationReport) ([]*comatproto.AdminDefs_ReportView, error) { +func (s *Server) hydrateModerationReportViews(ctx context.Context, rows []models.ModerationReport) ([]*comatproto.AdminDefs_ReportView, error) { var out []*comatproto.AdminDefs_ReportView for _, row := range rows { - // TODO(bnewbold): fetch these IDs var resolvedByActionIds []int64 + var actionRows []models.ModerationAction + result := s.db.Joins("left join moderation_report_resolutions on moderation_report_resolutions.action_id = moderation_actions.id").Where("moderation_report_resolutions.report_id = ?", row.ID).Where("moderation_actions.reversed_at IS NULL").Find(&actionRows) + if result.Error != nil { + return nil, result.Error + } + for _, actionRow := range actionRows { + resolvedByActionIds = append(resolvedByActionIds, int64(actionRow.ID)) + } var subj *comatproto.AdminDefs_ReportView_Subject switch row.SubjectType { @@ -192,8 +241,15 @@ func (s *Server) hydrateModerationReportDetails(ctx context.Context, rows []mode var out []*comatproto.AdminDefs_ReportViewDetail for _, row := range rows { - // TODO(bnewbold): fetch these objects - var resolvedByActions []*comatproto.AdminDefs_ActionView + var actionRows []models.ModerationAction + result := s.db.Joins("left join moderation_report_resolutions on moderation_report_resolutions.action_id = moderation_actions.id").Where("moderation_report_resolutions.report_id = ?", row.ID).Where("moderation_actions.reversed_at IS NULL").Find(&actionRows) + if result.Error != nil { + return nil, result.Error + } + resolvedByActionViews, err := s.hydrateModerationActionViews(ctx, actionRows) + if err != nil { + return nil, err + } var subj *comatproto.AdminDefs_ReportViewDetail_Subject switch row.SubjectType { @@ -216,7 +272,7 @@ func (s *Server) hydrateModerationReportDetails(ctx context.Context, rows []mode Subject: subj, ReportedBy: row.ReportedByDid, CreatedAt: row.CreatedAt.Format(time.RFC3339), - ResolvedByActions: resolvedByActions, + ResolvedByActions: resolvedByActionViews, } out = append(out, viewDetail) } diff --git a/labeling/helpers_test.go b/labeling/helpers_test.go index dffb98a0..994c5a75 100644 --- a/labeling/helpers_test.go +++ b/labeling/helpers_test.go @@ -105,11 +105,135 @@ func testCreateReport(t *testing.T, e *echo.Echo, lm *Server, input *comatproto. reportViewDetail := testGetReport(t, e, lm, out.Id) assert.Equal(out.Id, reportViewDetail.Id) assert.Equal(out.CreatedAt, reportViewDetail.CreatedAt) + assert.Equal(out.ReportedBy, reportViewDetail.ReportedBy) assert.Equal(out.Reason, reportViewDetail.Reason) assert.Equal(out.ReasonType, reportViewDetail.ReasonType) assert.Equal(0, len(reportViewDetail.ResolvedByActions)) - // XXX: Subject - // XXX: ReportedBy + if out.Subject.AdminDefs_RepoRef != nil { + assert.Equal(out.Subject.AdminDefs_RepoRef.Did, reportViewDetail.Subject.AdminDefs_RepoView.Did) + } else if out.Subject.RepoStrongRef != nil { + assert.Equal(out.Subject.RepoStrongRef.Uri, reportViewDetail.Subject.AdminDefs_RecordView.Uri) + assert.Equal(out.Subject.RepoStrongRef.Cid, reportViewDetail.Subject.AdminDefs_RecordView.Cid) + } else { + t.Fatal("expected non-empty actionviewdetail.subject enum") + } + + return out +} + +// fetches action, both getModerationAction and getModerationActions; verifies match +func testGetAction(t *testing.T, e *echo.Echo, lm *Server, actionId int64) comatproto.AdminDefs_ActionViewDetail { + assert := assert.New(t) + + params := make(url.Values) + params.Set("id", strconv.Itoa(int(actionId))) + req := httptest.NewRequest(http.MethodGet, "/xrpc/com.atproto.admin.getModerationAction?"+params.Encode(), nil) + recorder := httptest.NewRecorder() + c := e.NewContext(req, recorder) + assert.NoError(lm.HandleComAtprotoAdminGetModerationAction(c)) + assert.Equal(200, recorder.Code) + var actionViewDetail comatproto.AdminDefs_ActionViewDetail + if err := json.Unmarshal([]byte(recorder.Body.String()), &actionViewDetail); err != nil { + t.Fatal(err) + } + assert.Equal(actionId, actionViewDetail.Id) + + // read back (getModerationActions) and verify output + // TODO: include 'subject' param + req = httptest.NewRequest(http.MethodGet, "/xrpc/com.atproto.admin.getModerationActions", nil) + recorder = httptest.NewRecorder() + c = e.NewContext(req, recorder) + assert.NoError(lm.HandleComAtprotoAdminGetModerationActions(c)) + assert.Equal(200, recorder.Code) + var actionsOut comatproto.AdminGetModerationActions_Output + if err := json.Unmarshal([]byte(recorder.Body.String()), &actionsOut); err != nil { + t.Fatal(err) + } + var actionView *comatproto.AdminDefs_ActionView + for _, rv := range actionsOut.Actions { + if rv.Id == actionId { + actionView = rv + break + } + } + if actionView == nil { + t.Fatal("expected to find action by subject") + } + + assert.Equal(actionViewDetail.Id, actionView.Id) + assert.Equal(actionViewDetail.CreatedAt, actionView.CreatedAt) + assert.Equal(actionViewDetail.Action, actionView.Action) + assert.Equal(actionViewDetail.Reason, actionView.Reason) + assert.Equal(actionViewDetail.CreatedBy, actionView.CreatedBy) + assert.Equal(actionViewDetail.Reversal, actionView.Reversal) + assert.Equal(len(actionViewDetail.ResolvedReports), len(actionView.ResolvedReportIds)) + for i, reportId := range actionView.ResolvedReportIds { + assert.Equal(reportId, actionViewDetail.ResolvedReports[i].Id) + } + for i, blobCid := range actionView.SubjectBlobCids { + assert.Equal(blobCid, actionViewDetail.SubjectBlobs[i].Cid) + } + if actionViewDetail.Subject.AdminDefs_RepoView != nil { + assert.Equal(actionViewDetail.Subject.AdminDefs_RepoView.Did, actionView.Subject.AdminDefs_RepoRef.Did) + } else if actionViewDetail.Subject.AdminDefs_RecordView != nil { + assert.Equal(actionViewDetail.Subject.AdminDefs_RecordView.Uri, actionView.Subject.RepoStrongRef.Uri) + assert.Equal(actionViewDetail.Subject.AdminDefs_RecordView.Cid, actionView.Subject.RepoStrongRef.Cid) + } else { + t.Fatal("expected non-empty actionviewdetail.subject enum") + } + + return actionViewDetail +} + +// "happy path" test helper. creates a action, reads it back 2x ways, verifies match, then returns the original output +func testCreateAction(t *testing.T, e *echo.Echo, lm *Server, input *comatproto.AdminTakeModerationAction_Input) comatproto.AdminDefs_ActionView { + assert := assert.New(t) + + // create action and verify output + actionJSON, err := json.Marshal(input) + if err != nil { + t.Fatal(err) + } + req := httptest.NewRequest(http.MethodPost, "/xrpc/com.atproto.action.create", strings.NewReader(string(actionJSON))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + recorder := httptest.NewRecorder() + c := e.NewContext(req, recorder) + + assert.NoError(lm.HandleComAtprotoAdminTakeModerationAction(c)) + assert.Equal(200, recorder.Code) + + var out comatproto.AdminDefs_ActionView + if err := json.Unmarshal([]byte(recorder.Body.String()), &out); err != nil { + t.Fatal(err) + } + assert.Equal(input.Action, *out.Action) + assert.Equal(input.CreatedBy, out.CreatedBy) + assert.Equal(input.Reason, out.Reason) + assert.Equal(input.Subject.RepoStrongRef, out.Subject.RepoStrongRef) + assert.Equal(input.Subject.AdminDefs_RepoRef, out.Subject.AdminDefs_RepoRef) + assert.Equal(input.SubjectBlobCids, out.SubjectBlobCids) + + // read it back and verify output + actionViewDetail := testGetAction(t, e, lm, out.Id) + assert.Equal(out.Id, actionViewDetail.Id) + assert.Equal(out.CreatedAt, actionViewDetail.CreatedAt) + + assert.Equal(out.Action, actionViewDetail.Action) + assert.Equal(out.CreatedBy, actionViewDetail.CreatedBy) + assert.Equal(out.Reason, actionViewDetail.Reason) + if out.Subject.AdminDefs_RepoRef != nil { + assert.Equal(out.Subject.AdminDefs_RepoRef.Did, actionViewDetail.Subject.AdminDefs_RepoView.Did) + } else if out.Subject.RepoStrongRef != nil { + assert.Equal(out.Subject.RepoStrongRef.Uri, actionViewDetail.Subject.AdminDefs_RecordView.Uri) + assert.Equal(out.Subject.RepoStrongRef.Cid, actionViewDetail.Subject.AdminDefs_RecordView.Cid) + } else { + t.Fatal("expected non-empty actionviewdetail.subject enum") + } + for i, blobCid := range out.SubjectBlobCids { + assert.Equal(blobCid, actionViewDetail.SubjectBlobs[i].Cid) + } + assert.Equal(0, len(actionViewDetail.ResolvedReports)) + assert.Nil(actionViewDetail.Reversal) return out } diff --git a/labeling/service.go b/labeling/service.go index cde04a16..d82ec60c 100644 --- a/labeling/service.go +++ b/labeling/service.go @@ -66,6 +66,7 @@ func NewServer(db *gorm.DB, cs *carstore.CarStore, repoUser RepoConfig, plcURL, db.AutoMigrate(models.PDS{}) db.AutoMigrate(models.Label{}) db.AutoMigrate(models.ModerationAction{}) + db.AutoMigrate(models.ModerationActionSubjectBlobCid{}) db.AutoMigrate(models.ModerationReport{}) db.AutoMigrate(models.ModerationReportResolution{}) diff --git a/labeling/xrpc_handlers.go b/labeling/xrpc_handlers.go index 8921ae24..732cc0fe 100644 --- a/labeling/xrpc_handlers.go +++ b/labeling/xrpc_handlers.go @@ -2,10 +2,10 @@ package labeling import ( "context" + "errors" "strconv" "strings" "time" - "errors" atproto "github.com/bluesky-social/indigo/api/atproto" label "github.com/bluesky-social/indigo/api/label" @@ -158,7 +158,7 @@ func (s *Server) handleComAtprotoAdminGetModerationActions(ctx context.Context, nextCursor = strconv.FormatUint(actionRows[len(actionRows)-1].ID, 10) } - actionObjs, err := s.hydrateModerationActions(ctx, actionRows) + actionObjs, err := s.hydrateModerationActionViews(ctx, actionRows) if err != nil { return nil, err } @@ -220,7 +220,7 @@ func (s *Server) handleComAtprotoAdminGetModerationReports(ctx context.Context, nextCursor = strconv.FormatUint(reportRows[len(reportRows)-1].ID, 10) } - reportObjs, err := s.hydrateModerationReports(ctx, reportRows) + reportObjs, err := s.hydrateModerationReportViews(ctx, reportRows) if err != nil { return nil, err } @@ -282,7 +282,7 @@ func (s *Server) fetchSingleModerationAction(ctx context.Context, actionId int64 return nil, result.Error } - actionObjs, err := s.hydrateModerationActions(ctx, []models.ModerationAction{actionRow}) + actionObjs, err := s.hydrateModerationActionViews(ctx, []models.ModerationAction{actionRow}) if err != nil { return nil, err } @@ -337,7 +337,6 @@ func (s *Server) handleComAtprotoAdminTakeModerationAction(ctx context.Context, return nil, echo.NewHTTPError(400, "reason param was provided, but empty string") } - // XXX: SubjectBlobCids (how does atproto do it? array in postgresql?) row := models.ModerationAction{ Action: body.Action, Reason: body.Reason, @@ -357,7 +356,7 @@ func (s *Server) handleComAtprotoAdminTakeModerationAction(ctx context.Context, return nil, echo.NewHTTPError(400, "this implementation requires a strong record ref (aka, with CID) in reports") } row.SubjectType = "com.atproto.repo.recordRef" - // TODO: row.SubjectDid from URI? + // XXX: row.SubjectDid from URI? row.SubjectUri = &body.Subject.RepoStrongRef.Uri row.SubjectCid = &body.Subject.RepoStrongRef.Cid outSubj.RepoStrongRef = &atproto.RepoStrongRef{ @@ -374,14 +373,29 @@ func (s *Server) handleComAtprotoAdminTakeModerationAction(ctx context.Context, return nil, result.Error } + var cidRows []models.ModerationActionSubjectBlobCid + for _, sbc := range body.SubjectBlobCids { + cidRows = append(cidRows, models.ModerationActionSubjectBlobCid{ + ActionId: row.ID, + Cid: sbc, + }) + } + + if len(cidRows) > 0 { + result = s.db.Create(&cidRows) + if result.Error != nil { + return nil, result.Error + } + } + out := atproto.AdminDefs_ActionView{ - Id: int64(row.ID), - Action: &row.Action, - Reason: row.Reason, - CreatedBy: row.CreatedByDid, - CreatedAt: row.CreatedAt.Format(time.RFC3339), - Subject: &outSubj, - // XXX: SubjectBlobCids + Id: int64(row.ID), + Action: &row.Action, + Reason: row.Reason, + CreatedBy: row.CreatedByDid, + CreatedAt: row.CreatedAt.Format(time.RFC3339), + Subject: &outSubj, + SubjectBlobCids: body.SubjectBlobCids, } return &out, nil } @@ -398,7 +412,7 @@ func (s *Server) handleComAtprotoReportCreate(ctx context.Context, body *atproto row := models.ModerationReport{ ReasonType: *body.ReasonType, Reason: body.Reason, - // TODO(bnewbold): from auth, via context? as a new lexicon field? + // XXX(bnewbold): from auth, via context? as a new lexicon field? ReportedByDid: "did:plc:FAKE", } var outSubj atproto.ModerationCreateReport_Output_Subject @@ -420,7 +434,7 @@ func (s *Server) handleComAtprotoReportCreate(ctx context.Context, body *atproto return nil, echo.NewHTTPError(400, "this implementation requires a strong record ref (aka, with CID) in reports") } row.SubjectType = "com.atproto.repo.recordRef" - // TODO: row.SubjectDid from URI? + // XXX: row.SubjectDid from URI? row.SubjectUri = &body.Subject.RepoStrongRef.Uri row.SubjectCid = &body.Subject.RepoStrongRef.Cid outSubj.RepoStrongRef = &atproto.RepoStrongRef{ @@ -442,6 +456,7 @@ func (s *Server) handleComAtprotoReportCreate(ctx context.Context, body *atproto CreatedAt: row.CreatedAt.Format(time.RFC3339), Reason: row.Reason, ReasonType: &row.ReasonType, + ReportedBy: row.ReportedByDid, Subject: &outSubj, } return &out, nil diff --git a/labeling/xrpc_test.go b/labeling/xrpc_test.go index f0ff405e..03262d1b 100644 --- a/labeling/xrpc_test.go +++ b/labeling/xrpc_test.go @@ -2,6 +2,7 @@ package labeling import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "strings" @@ -101,9 +102,12 @@ func TestLabelMakerXRPCReportRecord(t *testing.T) { }, } out := testCreateReport(t, e, lm, &report) - assert.Equal(rt, *out.ReasonType) - assert.Equal(reason, *out.Reason) - // XXX: more fields + assert.Equal(report.ReasonType, out.ReasonType) + assert.Equal(report.Reason, out.Reason) + assert.NotNil(out.CreatedAt) + assert.NotNil(out.ReportedBy) + assert.Equal(report.Subject.AdminDefs_RepoRef, out.Subject.AdminDefs_RepoRef) + assert.Equal(report.Subject.RepoStrongRef, out.Subject.RepoStrongRef) } func TestLabelMakerXRPCReportRecordBad(t *testing.T) { @@ -161,14 +165,14 @@ func TestLabelMakerXRPCReportAction(t *testing.T) { e := echo.New() lm := testLabelMaker(t) - // create a report - rt := "spam" - reason := "I just don't like it!" + // create report + reasonType := "spam" + reportReason := "I just don't like it!" uri := "at://did:plc:123/com.example.record/bcd234" cid := "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454" report := comatproto.ModerationCreateReport_Input{ - Reason: &reason, - ReasonType: &rt, + Reason: &reportReason, + ReasonType: &reasonType, Subject: &comatproto.ModerationCreateReport_Input_Subject{ RepoStrongRef: &comatproto.RepoStrongRef{ //com.atproto.repo.strongRef @@ -178,15 +182,106 @@ func TestLabelMakerXRPCReportAction(t *testing.T) { }, } reportOut := testCreateReport(t, e, lm, &report) + reportId := reportOut.Id + + // create action + actionVerb := "acknowledge" + actionDid := "did:plc:ADMIN" + actionReason := "chaos reigns" + action := comatproto.AdminTakeModerationAction_Input{ + Action: actionVerb, + CreatedBy: actionDid, + Reason: actionReason, + Subject: &comatproto.AdminTakeModerationAction_Input_Subject{ + RepoStrongRef: &comatproto.RepoStrongRef{ + //com.atproto.repo.strongRef + Uri: uri, + Cid: cid, + }, + }, + // XXX: cid support + /* + SubjectBlobCids: []string{ + "abc", + "onetwothree", + }, + */ + } + actionOut := testCreateAction(t, e, lm, &action) + actionId := actionOut.Id + + // resolve report with action + resolution := comatproto.AdminResolveModerationReports_Input{ + ActionId: actionId, + CreatedBy: actionDid, + ReportIds: []int64{reportId}, + } + resolutionJSON, err := json.Marshal(resolution) + if err != nil { + t.Fatal(err) + } + req := httptest.NewRequest(http.MethodPost, "/xrpc/com.atproto.report.resolveModerationReports", strings.NewReader(string(resolutionJSON))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + recorder := httptest.NewRecorder() + c := e.NewContext(req, recorder) + assert.NoError(lm.HandleComAtprotoAdminResolveModerationReports(c)) + var resolutionOut comatproto.AdminDefs_ActionView + if err := json.Unmarshal([]byte(recorder.Body.String()), &resolutionOut); err != nil { + t.Fatal(err) + } + fmt.Println(recorder.Body.String()) + assert.Equal(actionId, resolutionOut.Id) + assert.Equal(1, len(resolutionOut.ResolvedReportIds)) + assert.Equal(reportId, resolutionOut.ResolvedReportIds[0]) + + // get report (should have action included) + reportOutDetail := testGetReport(t, e, lm, reportId) + assert.Equal(reportId, reportOutDetail.Id) + assert.Equal(1, len(reportOutDetail.ResolvedByActions)) + assert.Equal(actionId, reportOutDetail.ResolvedByActions[0].Id) + + // get action (should have report included) + actionOutDetail := testGetAction(t, e, lm, actionId) + assert.Equal(actionId, actionOutDetail.Id) + assert.Equal(1, len(actionOutDetail.ResolvedReports)) + assert.Equal(reportId, actionOutDetail.ResolvedReports[0].Id) + + // reverse action + reversalReason := "changed my mind" + reversal := comatproto.AdminReverseModerationAction_Input{ + Id: actionId, + CreatedBy: actionDid, + Reason: reversalReason, + } + reversalJSON, err := json.Marshal(reversal) + if err != nil { + t.Fatal(err) + } + req = httptest.NewRequest(http.MethodPost, "/xrpc/com.atproto.report.reverseModerationAction", strings.NewReader(string(reversalJSON))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + recorder = httptest.NewRecorder() + c = e.NewContext(req, recorder) + assert.NoError(lm.HandleComAtprotoAdminReverseModerationAction(c)) + var reversalOut comatproto.AdminDefs_ActionView + if err := json.Unmarshal([]byte(recorder.Body.String()), &reversalOut); err != nil { + t.Fatal(err) + } + assert.Equal(actionId, reversalOut.Id) + assert.Equal(1, len(reversalOut.ResolvedReportIds)) + assert.Equal(reportId, reversalOut.ResolvedReportIds[0]) + assert.Equal(reversal.Reason, reversalOut.Reversal.Reason) + assert.Equal(reversal.CreatedBy, reversalOut.Reversal.CreatedBy) + assert.NotNil(reversalOut.Reversal.CreatedAt) - _ = assert - _ = reportOut - // TODO: getReport helper (does single and multi, verifies equal, returns single) - // TODO: getAction helper (does single and multi, verifies equal, returns single) + // get report (should *not* have action included) + reportOutDetail = testGetReport(t, e, lm, reportId) + assert.Equal(reportId, reportOutDetail.Id) + assert.Equal(0, len(reportOutDetail.ResolvedByActions)) - // XXX: create action (including get, get plural, verifications) - // XXX: get report (should have action included) - // XXX: reverse action - // XXX: get action (single and plural) - // XXX: get report (should not have action included) + // get action (should still have report included) + actionOutDetail = testGetAction(t, e, lm, actionId) + assert.Equal(actionId, actionOutDetail.Id) + assert.Equal(1, len(actionOutDetail.ResolvedReports)) + assert.Equal(reportId, actionOutDetail.ResolvedReports[0].Id) + assert.Equal(reversalOut.Reversal, actionOutDetail.Reversal) } diff --git a/models/moderation.go b/models/moderation.go index ffa8872a..f1ea5ce9 100644 --- a/models/moderation.go +++ b/models/moderation.go @@ -19,6 +19,12 @@ type ModerationAction struct { ReversedReason *string } +type ModerationActionSubjectBlobCid struct { + // TODO: foreign key + ActionId uint64 `gorm:"primaryKey"` + Cid string `gorm:"primaryKey"` +} + type ModerationReport struct { ID uint64 `gorm:"primaryKey"` SubjectType string `gorm:"not null"` @@ -32,7 +38,9 @@ type ModerationReport struct { } type ModerationReportResolution struct { - ReportId uint64 `gorm:"primaryKey"` + // TODO: foreign key + ReportId uint64 `gorm:"primaryKey"` + // TODO: foreign key ActionId uint64 `gorm:"primaryKey;index:"` CreatedAt time.Time `gorm:"not null"` CreatedByDid string `gorm:"not null"` -- 2.51.2