porting all github actions from bluesky-social/indigo to tangled CI

labelmaker: progress on admin XRPC endpoints

+70 -14
labeling/admin.go
··· 14 14 // This is probably only a temporary method 15 15 func (s *Server) hydrateRepoView(ctx context.Context, did, indexedAt string) *comatproto.AdminDefs_RepoView { 16 16 return &comatproto.AdminDefs_RepoView{ 17 - // TODO(bnewbold): populate more, or more correctly, from some backend? 17 + // XXX(bnewbold): populate more, or more correctly, from some backend? 18 18 Did: did, 19 19 Email: nil, 20 - Handle: "TODO", 20 + Handle: "XXX", 21 21 IndexedAt: indexedAt, 22 22 Moderation: nil, 23 23 RelatedRecords: nil, ··· 27 27 // This is probably only a temporary method 28 28 func (s *Server) hydrateRecordView(ctx context.Context, did string, uri, cid *string, indexedAt string) *comatproto.AdminDefs_RecordView { 29 29 repoView := s.hydrateRepoView(ctx, did, indexedAt) 30 - // TODO(bnewbold): populate more, or more correctly, from some backend? 30 + // XXX(bnewbold): populate more, or more correctly, from some backend? 31 31 recordView := comatproto.AdminDefs_RecordView{ 32 32 BlobCids: []string{}, 33 33 IndexedAt: indexedAt, ··· 45 45 return &recordView 46 46 } 47 47 48 - func (s *Server) hydrateModerationActions(ctx context.Context, rows []models.ModerationAction) ([]*comatproto.AdminDefs_ActionView, error) { 48 + func (s *Server) hydrateModerationActionViews(ctx context.Context, rows []models.ModerationAction) ([]*comatproto.AdminDefs_ActionView, error) { 49 49 50 50 var out []*comatproto.AdminDefs_ActionView 51 51 52 52 for _, row := range rows { 53 - // TODO(bnewbold): resolve these 53 + 54 54 resolvedReportIds := []int64{} 55 + var resolutionRows []models.ModerationReportResolution 56 + result := s.db.Where("action_id = ?", row.ID).Find(&resolutionRows) 57 + if result.Error != nil { 58 + return nil, result.Error 59 + } 60 + for _, row := range resolutionRows { 61 + resolvedReportIds = append(resolvedReportIds, int64(row.ReportId)) 62 + } 63 + 55 64 subjectBlobCIDs := []string{} 65 + var cidRows []models.ModerationActionSubjectBlobCid 66 + result = s.db.Where("action_id = ?", row.ID).Find(&cidRows) 67 + if result.Error != nil { 68 + return nil, result.Error 69 + } 70 + for _, row := range cidRows { 71 + subjectBlobCIDs = append(subjectBlobCIDs, row.Cid) 72 + } 56 73 57 74 var reversal *comatproto.AdminDefs_ActionReversal 58 75 if row.ReversedAt != nil { ··· 104 121 var out []*comatproto.AdminDefs_ActionViewDetail 105 122 for _, row := range rows { 106 123 107 - // TODO(bnewbold): resolve these 108 - resolvedReports := []*comatproto.AdminDefs_ReportView{} 109 - subjectBlobs := []*comatproto.AdminDefs_BlobView{} 124 + var reportRows []models.ModerationReport 125 + 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) 126 + if result.Error != nil { 127 + return nil, result.Error 128 + } 129 + resolvedReports, err := s.hydrateModerationReportViews(ctx, reportRows) 130 + if err != nil { 131 + return nil, err 132 + } 133 + 134 + subjectBlobViews := []*comatproto.AdminDefs_BlobView{} 135 + var cidRows []models.ModerationActionSubjectBlobCid 136 + result = s.db.Where("action_id = ?", row.ID).Find(&cidRows) 137 + if result.Error != nil { 138 + return nil, result.Error 139 + } 140 + for _, row := range cidRows { 141 + subjectBlobViews = append(subjectBlobViews, &comatproto.AdminDefs_BlobView{ 142 + Cid: row.Cid, 143 + /* XXX: all these other fields 144 + CreatedAt string 145 + Details *AdminDefs_BlobView_Details 146 + MimeType string 147 + Moderation *AdminDefs_Moderation 148 + Size int64 149 + */ 150 + }) 151 + } 110 152 111 153 var reversal *comatproto.AdminDefs_ActionReversal 112 154 if row.ReversedAt != nil { ··· 139 181 ResolvedReports: resolvedReports, 140 182 Reversal: reversal, 141 183 Subject: subj, 142 - SubjectBlobs: subjectBlobs, 184 + SubjectBlobs: subjectBlobViews, 143 185 } 144 186 out = append(out, viewDetail) 145 187 } 146 188 return out, nil 147 189 } 148 190 149 - func (s *Server) hydrateModerationReports(ctx context.Context, rows []models.ModerationReport) ([]*comatproto.AdminDefs_ReportView, error) { 191 + func (s *Server) hydrateModerationReportViews(ctx context.Context, rows []models.ModerationReport) ([]*comatproto.AdminDefs_ReportView, error) { 150 192 151 193 var out []*comatproto.AdminDefs_ReportView 152 194 for _, row := range rows { 153 - // TODO(bnewbold): fetch these IDs 154 195 var resolvedByActionIds []int64 196 + var actionRows []models.ModerationAction 197 + 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) 198 + if result.Error != nil { 199 + return nil, result.Error 200 + } 201 + for _, actionRow := range actionRows { 202 + resolvedByActionIds = append(resolvedByActionIds, int64(actionRow.ID)) 203 + } 155 204 156 205 var subj *comatproto.AdminDefs_ReportView_Subject 157 206 switch row.SubjectType { ··· 192 241 193 242 var out []*comatproto.AdminDefs_ReportViewDetail 194 243 for _, row := range rows { 195 - // TODO(bnewbold): fetch these objects 196 - var resolvedByActions []*comatproto.AdminDefs_ActionView 244 + var actionRows []models.ModerationAction 245 + 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) 246 + if result.Error != nil { 247 + return nil, result.Error 248 + } 249 + resolvedByActionViews, err := s.hydrateModerationActionViews(ctx, actionRows) 250 + if err != nil { 251 + return nil, err 252 + } 197 253 198 254 var subj *comatproto.AdminDefs_ReportViewDetail_Subject 199 255 switch row.SubjectType { ··· 216 272 Subject: subj, 217 273 ReportedBy: row.ReportedByDid, 218 274 CreatedAt: row.CreatedAt.Format(time.RFC3339), 219 - ResolvedByActions: resolvedByActions, 275 + ResolvedByActions: resolvedByActionViews, 220 276 } 221 277 out = append(out, viewDetail) 222 278 }
+126 -2
labeling/helpers_test.go
··· 105 105 reportViewDetail := testGetReport(t, e, lm, out.Id) 106 106 assert.Equal(out.Id, reportViewDetail.Id) 107 107 assert.Equal(out.CreatedAt, reportViewDetail.CreatedAt) 108 + assert.Equal(out.ReportedBy, reportViewDetail.ReportedBy) 108 109 assert.Equal(out.Reason, reportViewDetail.Reason) 109 110 assert.Equal(out.ReasonType, reportViewDetail.ReasonType) 110 111 assert.Equal(0, len(reportViewDetail.ResolvedByActions)) 111 - // XXX: Subject 112 - // XXX: ReportedBy 112 + if out.Subject.AdminDefs_RepoRef != nil { 113 + assert.Equal(out.Subject.AdminDefs_RepoRef.Did, reportViewDetail.Subject.AdminDefs_RepoView.Did) 114 + } else if out.Subject.RepoStrongRef != nil { 115 + assert.Equal(out.Subject.RepoStrongRef.Uri, reportViewDetail.Subject.AdminDefs_RecordView.Uri) 116 + assert.Equal(out.Subject.RepoStrongRef.Cid, reportViewDetail.Subject.AdminDefs_RecordView.Cid) 117 + } else { 118 + t.Fatal("expected non-empty actionviewdetail.subject enum") 119 + } 120 + 121 + return out 122 + } 123 + 124 + // fetches action, both getModerationAction and getModerationActions; verifies match 125 + func testGetAction(t *testing.T, e *echo.Echo, lm *Server, actionId int64) comatproto.AdminDefs_ActionViewDetail { 126 + assert := assert.New(t) 127 + 128 + params := make(url.Values) 129 + params.Set("id", strconv.Itoa(int(actionId))) 130 + req := httptest.NewRequest(http.MethodGet, "/xrpc/com.atproto.admin.getModerationAction?"+params.Encode(), nil) 131 + recorder := httptest.NewRecorder() 132 + c := e.NewContext(req, recorder) 133 + assert.NoError(lm.HandleComAtprotoAdminGetModerationAction(c)) 134 + assert.Equal(200, recorder.Code) 135 + var actionViewDetail comatproto.AdminDefs_ActionViewDetail 136 + if err := json.Unmarshal([]byte(recorder.Body.String()), &actionViewDetail); err != nil { 137 + t.Fatal(err) 138 + } 139 + assert.Equal(actionId, actionViewDetail.Id) 140 + 141 + // read back (getModerationActions) and verify output 142 + // TODO: include 'subject' param 143 + req = httptest.NewRequest(http.MethodGet, "/xrpc/com.atproto.admin.getModerationActions", nil) 144 + recorder = httptest.NewRecorder() 145 + c = e.NewContext(req, recorder) 146 + assert.NoError(lm.HandleComAtprotoAdminGetModerationActions(c)) 147 + assert.Equal(200, recorder.Code) 148 + var actionsOut comatproto.AdminGetModerationActions_Output 149 + if err := json.Unmarshal([]byte(recorder.Body.String()), &actionsOut); err != nil { 150 + t.Fatal(err) 151 + } 152 + var actionView *comatproto.AdminDefs_ActionView 153 + for _, rv := range actionsOut.Actions { 154 + if rv.Id == actionId { 155 + actionView = rv 156 + break 157 + } 158 + } 159 + if actionView == nil { 160 + t.Fatal("expected to find action by subject") 161 + } 162 + 163 + assert.Equal(actionViewDetail.Id, actionView.Id) 164 + assert.Equal(actionViewDetail.CreatedAt, actionView.CreatedAt) 165 + assert.Equal(actionViewDetail.Action, actionView.Action) 166 + assert.Equal(actionViewDetail.Reason, actionView.Reason) 167 + assert.Equal(actionViewDetail.CreatedBy, actionView.CreatedBy) 168 + assert.Equal(actionViewDetail.Reversal, actionView.Reversal) 169 + assert.Equal(len(actionViewDetail.ResolvedReports), len(actionView.ResolvedReportIds)) 170 + for i, reportId := range actionView.ResolvedReportIds { 171 + assert.Equal(reportId, actionViewDetail.ResolvedReports[i].Id) 172 + } 173 + for i, blobCid := range actionView.SubjectBlobCids { 174 + assert.Equal(blobCid, actionViewDetail.SubjectBlobs[i].Cid) 175 + } 176 + if actionViewDetail.Subject.AdminDefs_RepoView != nil { 177 + assert.Equal(actionViewDetail.Subject.AdminDefs_RepoView.Did, actionView.Subject.AdminDefs_RepoRef.Did) 178 + } else if actionViewDetail.Subject.AdminDefs_RecordView != nil { 179 + assert.Equal(actionViewDetail.Subject.AdminDefs_RecordView.Uri, actionView.Subject.RepoStrongRef.Uri) 180 + assert.Equal(actionViewDetail.Subject.AdminDefs_RecordView.Cid, actionView.Subject.RepoStrongRef.Cid) 181 + } else { 182 + t.Fatal("expected non-empty actionviewdetail.subject enum") 183 + } 184 + 185 + return actionViewDetail 186 + } 187 + 188 + // "happy path" test helper. creates a action, reads it back 2x ways, verifies match, then returns the original output 189 + func testCreateAction(t *testing.T, e *echo.Echo, lm *Server, input *comatproto.AdminTakeModerationAction_Input) comatproto.AdminDefs_ActionView { 190 + assert := assert.New(t) 191 + 192 + // create action and verify output 193 + actionJSON, err := json.Marshal(input) 194 + if err != nil { 195 + t.Fatal(err) 196 + } 197 + req := httptest.NewRequest(http.MethodPost, "/xrpc/com.atproto.action.create", strings.NewReader(string(actionJSON))) 198 + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 199 + recorder := httptest.NewRecorder() 200 + c := e.NewContext(req, recorder) 201 + 202 + assert.NoError(lm.HandleComAtprotoAdminTakeModerationAction(c)) 203 + assert.Equal(200, recorder.Code) 204 + 205 + var out comatproto.AdminDefs_ActionView 206 + if err := json.Unmarshal([]byte(recorder.Body.String()), &out); err != nil { 207 + t.Fatal(err) 208 + } 209 + assert.Equal(input.Action, *out.Action) 210 + assert.Equal(input.CreatedBy, out.CreatedBy) 211 + assert.Equal(input.Reason, out.Reason) 212 + assert.Equal(input.Subject.RepoStrongRef, out.Subject.RepoStrongRef) 213 + assert.Equal(input.Subject.AdminDefs_RepoRef, out.Subject.AdminDefs_RepoRef) 214 + assert.Equal(input.SubjectBlobCids, out.SubjectBlobCids) 215 + 216 + // read it back and verify output 217 + actionViewDetail := testGetAction(t, e, lm, out.Id) 218 + assert.Equal(out.Id, actionViewDetail.Id) 219 + assert.Equal(out.CreatedAt, actionViewDetail.CreatedAt) 220 + 221 + assert.Equal(out.Action, actionViewDetail.Action) 222 + assert.Equal(out.CreatedBy, actionViewDetail.CreatedBy) 223 + assert.Equal(out.Reason, actionViewDetail.Reason) 224 + if out.Subject.AdminDefs_RepoRef != nil { 225 + assert.Equal(out.Subject.AdminDefs_RepoRef.Did, actionViewDetail.Subject.AdminDefs_RepoView.Did) 226 + } else if out.Subject.RepoStrongRef != nil { 227 + assert.Equal(out.Subject.RepoStrongRef.Uri, actionViewDetail.Subject.AdminDefs_RecordView.Uri) 228 + assert.Equal(out.Subject.RepoStrongRef.Cid, actionViewDetail.Subject.AdminDefs_RecordView.Cid) 229 + } else { 230 + t.Fatal("expected non-empty actionviewdetail.subject enum") 231 + } 232 + for i, blobCid := range out.SubjectBlobCids { 233 + assert.Equal(blobCid, actionViewDetail.SubjectBlobs[i].Cid) 234 + } 235 + assert.Equal(0, len(actionViewDetail.ResolvedReports)) 236 + assert.Nil(actionViewDetail.Reversal) 113 237 114 238 return out 115 239 }
+1
labeling/service.go
··· 66 66 db.AutoMigrate(models.PDS{}) 67 67 db.AutoMigrate(models.Label{}) 68 68 db.AutoMigrate(models.ModerationAction{}) 69 + db.AutoMigrate(models.ModerationActionSubjectBlobCid{}) 69 70 db.AutoMigrate(models.ModerationReport{}) 70 71 db.AutoMigrate(models.ModerationReportResolution{}) 71 72
+30 -15
labeling/xrpc_handlers.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "strconv" 6 7 "strings" 7 8 "time" 8 - "errors" 9 9 10 10 atproto "github.com/bluesky-social/indigo/api/atproto" 11 11 label "github.com/bluesky-social/indigo/api/label" ··· 158 158 nextCursor = strconv.FormatUint(actionRows[len(actionRows)-1].ID, 10) 159 159 } 160 160 161 - actionObjs, err := s.hydrateModerationActions(ctx, actionRows) 161 + actionObjs, err := s.hydrateModerationActionViews(ctx, actionRows) 162 162 if err != nil { 163 163 return nil, err 164 164 } ··· 220 220 nextCursor = strconv.FormatUint(reportRows[len(reportRows)-1].ID, 10) 221 221 } 222 222 223 - reportObjs, err := s.hydrateModerationReports(ctx, reportRows) 223 + reportObjs, err := s.hydrateModerationReportViews(ctx, reportRows) 224 224 if err != nil { 225 225 return nil, err 226 226 } ··· 282 282 return nil, result.Error 283 283 } 284 284 285 - actionObjs, err := s.hydrateModerationActions(ctx, []models.ModerationAction{actionRow}) 285 + actionObjs, err := s.hydrateModerationActionViews(ctx, []models.ModerationAction{actionRow}) 286 286 if err != nil { 287 287 return nil, err 288 288 } ··· 337 337 return nil, echo.NewHTTPError(400, "reason param was provided, but empty string") 338 338 } 339 339 340 - // XXX: SubjectBlobCids (how does atproto do it? array in postgresql?) 341 340 row := models.ModerationAction{ 342 341 Action: body.Action, 343 342 Reason: body.Reason, ··· 357 356 return nil, echo.NewHTTPError(400, "this implementation requires a strong record ref (aka, with CID) in reports") 358 357 } 359 358 row.SubjectType = "com.atproto.repo.recordRef" 360 - // TODO: row.SubjectDid from URI? 359 + // XXX: row.SubjectDid from URI? 361 360 row.SubjectUri = &body.Subject.RepoStrongRef.Uri 362 361 row.SubjectCid = &body.Subject.RepoStrongRef.Cid 363 362 outSubj.RepoStrongRef = &atproto.RepoStrongRef{ ··· 374 373 return nil, result.Error 375 374 } 376 375 376 + var cidRows []models.ModerationActionSubjectBlobCid 377 + for _, sbc := range body.SubjectBlobCids { 378 + cidRows = append(cidRows, models.ModerationActionSubjectBlobCid{ 379 + ActionId: row.ID, 380 + Cid: sbc, 381 + }) 382 + } 383 + 384 + if len(cidRows) > 0 { 385 + result = s.db.Create(&cidRows) 386 + if result.Error != nil { 387 + return nil, result.Error 388 + } 389 + } 390 + 377 391 out := atproto.AdminDefs_ActionView{ 378 - Id: int64(row.ID), 379 - Action: &row.Action, 380 - Reason: row.Reason, 381 - CreatedBy: row.CreatedByDid, 382 - CreatedAt: row.CreatedAt.Format(time.RFC3339), 383 - Subject: &outSubj, 384 - // XXX: SubjectBlobCids 392 + Id: int64(row.ID), 393 + Action: &row.Action, 394 + Reason: row.Reason, 395 + CreatedBy: row.CreatedByDid, 396 + CreatedAt: row.CreatedAt.Format(time.RFC3339), 397 + Subject: &outSubj, 398 + SubjectBlobCids: body.SubjectBlobCids, 385 399 } 386 400 return &out, nil 387 401 } ··· 398 412 row := models.ModerationReport{ 399 413 ReasonType: *body.ReasonType, 400 414 Reason: body.Reason, 401 - // TODO(bnewbold): from auth, via context? as a new lexicon field? 415 + // XXX(bnewbold): from auth, via context? as a new lexicon field? 402 416 ReportedByDid: "did:plc:FAKE", 403 417 } 404 418 var outSubj atproto.ModerationCreateReport_Output_Subject ··· 420 434 return nil, echo.NewHTTPError(400, "this implementation requires a strong record ref (aka, with CID) in reports") 421 435 } 422 436 row.SubjectType = "com.atproto.repo.recordRef" 423 - // TODO: row.SubjectDid from URI? 437 + // XXX: row.SubjectDid from URI? 424 438 row.SubjectUri = &body.Subject.RepoStrongRef.Uri 425 439 row.SubjectCid = &body.Subject.RepoStrongRef.Cid 426 440 outSubj.RepoStrongRef = &atproto.RepoStrongRef{ ··· 442 456 CreatedAt: row.CreatedAt.Format(time.RFC3339), 443 457 Reason: row.Reason, 444 458 ReasonType: &row.ReasonType, 459 + ReportedBy: row.ReportedByDid, 445 460 Subject: &outSubj, 446 461 } 447 462 return &out, nil
+112 -17
labeling/xrpc_test.go
··· 2 2 3 3 import ( 4 4 "encoding/json" 5 + "fmt" 5 6 "net/http" 6 7 "net/http/httptest" 7 8 "strings" ··· 101 102 }, 102 103 } 103 104 out := testCreateReport(t, e, lm, &report) 104 - assert.Equal(rt, *out.ReasonType) 105 - assert.Equal(reason, *out.Reason) 106 - // XXX: more fields 105 + assert.Equal(report.ReasonType, out.ReasonType) 106 + assert.Equal(report.Reason, out.Reason) 107 + assert.NotNil(out.CreatedAt) 108 + assert.NotNil(out.ReportedBy) 109 + assert.Equal(report.Subject.AdminDefs_RepoRef, out.Subject.AdminDefs_RepoRef) 110 + assert.Equal(report.Subject.RepoStrongRef, out.Subject.RepoStrongRef) 107 111 } 108 112 109 113 func TestLabelMakerXRPCReportRecordBad(t *testing.T) { ··· 161 165 e := echo.New() 162 166 lm := testLabelMaker(t) 163 167 164 - // create a report 165 - rt := "spam" 166 - reason := "I just don't like it!" 168 + // create report 169 + reasonType := "spam" 170 + reportReason := "I just don't like it!" 167 171 uri := "at://did:plc:123/com.example.record/bcd234" 168 172 cid := "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454" 169 173 report := comatproto.ModerationCreateReport_Input{ 170 - Reason: &reason, 171 - ReasonType: &rt, 174 + Reason: &reportReason, 175 + ReasonType: &reasonType, 172 176 Subject: &comatproto.ModerationCreateReport_Input_Subject{ 173 177 RepoStrongRef: &comatproto.RepoStrongRef{ 174 178 //com.atproto.repo.strongRef ··· 178 182 }, 179 183 } 180 184 reportOut := testCreateReport(t, e, lm, &report) 185 + reportId := reportOut.Id 181 186 182 - _ = assert 183 - _ = reportOut 184 - // TODO: getReport helper (does single and multi, verifies equal, returns single) 185 - // TODO: getAction helper (does single and multi, verifies equal, returns single) 187 + // create action 188 + actionVerb := "acknowledge" 189 + actionDid := "did:plc:ADMIN" 190 + actionReason := "chaos reigns" 191 + action := comatproto.AdminTakeModerationAction_Input{ 192 + Action: actionVerb, 193 + CreatedBy: actionDid, 194 + Reason: actionReason, 195 + Subject: &comatproto.AdminTakeModerationAction_Input_Subject{ 196 + RepoStrongRef: &comatproto.RepoStrongRef{ 197 + //com.atproto.repo.strongRef 198 + Uri: uri, 199 + Cid: cid, 200 + }, 201 + }, 202 + // XXX: cid support 203 + /* 204 + SubjectBlobCids: []string{ 205 + "abc", 206 + "onetwothree", 207 + }, 208 + */ 209 + } 210 + actionOut := testCreateAction(t, e, lm, &action) 211 + actionId := actionOut.Id 212 + 213 + // resolve report with action 214 + resolution := comatproto.AdminResolveModerationReports_Input{ 215 + ActionId: actionId, 216 + CreatedBy: actionDid, 217 + ReportIds: []int64{reportId}, 218 + } 219 + resolutionJSON, err := json.Marshal(resolution) 220 + if err != nil { 221 + t.Fatal(err) 222 + } 223 + req := httptest.NewRequest(http.MethodPost, "/xrpc/com.atproto.report.resolveModerationReports", strings.NewReader(string(resolutionJSON))) 224 + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 225 + recorder := httptest.NewRecorder() 226 + c := e.NewContext(req, recorder) 227 + assert.NoError(lm.HandleComAtprotoAdminResolveModerationReports(c)) 228 + var resolutionOut comatproto.AdminDefs_ActionView 229 + if err := json.Unmarshal([]byte(recorder.Body.String()), &resolutionOut); err != nil { 230 + t.Fatal(err) 231 + } 232 + fmt.Println(recorder.Body.String()) 233 + assert.Equal(actionId, resolutionOut.Id) 234 + assert.Equal(1, len(resolutionOut.ResolvedReportIds)) 235 + assert.Equal(reportId, resolutionOut.ResolvedReportIds[0]) 186 236 187 - // XXX: create action (including get, get plural, verifications) 188 - // XXX: get report (should have action included) 189 - // XXX: reverse action 190 - // XXX: get action (single and plural) 191 - // XXX: get report (should not have action included) 237 + // get report (should have action included) 238 + reportOutDetail := testGetReport(t, e, lm, reportId) 239 + assert.Equal(reportId, reportOutDetail.Id) 240 + assert.Equal(1, len(reportOutDetail.ResolvedByActions)) 241 + assert.Equal(actionId, reportOutDetail.ResolvedByActions[0].Id) 242 + 243 + // get action (should have report included) 244 + actionOutDetail := testGetAction(t, e, lm, actionId) 245 + assert.Equal(actionId, actionOutDetail.Id) 246 + assert.Equal(1, len(actionOutDetail.ResolvedReports)) 247 + assert.Equal(reportId, actionOutDetail.ResolvedReports[0].Id) 248 + 249 + // reverse action 250 + reversalReason := "changed my mind" 251 + reversal := comatproto.AdminReverseModerationAction_Input{ 252 + Id: actionId, 253 + CreatedBy: actionDid, 254 + Reason: reversalReason, 255 + } 256 + reversalJSON, err := json.Marshal(reversal) 257 + if err != nil { 258 + t.Fatal(err) 259 + } 260 + req = httptest.NewRequest(http.MethodPost, "/xrpc/com.atproto.report.reverseModerationAction", strings.NewReader(string(reversalJSON))) 261 + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 262 + recorder = httptest.NewRecorder() 263 + c = e.NewContext(req, recorder) 264 + assert.NoError(lm.HandleComAtprotoAdminReverseModerationAction(c)) 265 + var reversalOut comatproto.AdminDefs_ActionView 266 + if err := json.Unmarshal([]byte(recorder.Body.String()), &reversalOut); err != nil { 267 + t.Fatal(err) 268 + } 269 + assert.Equal(actionId, reversalOut.Id) 270 + assert.Equal(1, len(reversalOut.ResolvedReportIds)) 271 + assert.Equal(reportId, reversalOut.ResolvedReportIds[0]) 272 + assert.Equal(reversal.Reason, reversalOut.Reversal.Reason) 273 + assert.Equal(reversal.CreatedBy, reversalOut.Reversal.CreatedBy) 274 + assert.NotNil(reversalOut.Reversal.CreatedAt) 275 + 276 + // get report (should *not* have action included) 277 + reportOutDetail = testGetReport(t, e, lm, reportId) 278 + assert.Equal(reportId, reportOutDetail.Id) 279 + assert.Equal(0, len(reportOutDetail.ResolvedByActions)) 280 + 281 + // get action (should still have report included) 282 + actionOutDetail = testGetAction(t, e, lm, actionId) 283 + assert.Equal(actionId, actionOutDetail.Id) 284 + assert.Equal(1, len(actionOutDetail.ResolvedReports)) 285 + assert.Equal(reportId, actionOutDetail.ResolvedReports[0].Id) 286 + assert.Equal(reversalOut.Reversal, actionOutDetail.Reversal) 192 287 }
+9 -1
models/moderation.go
··· 19 19 ReversedReason *string 20 20 } 21 21 22 + type ModerationActionSubjectBlobCid struct { 23 + // TODO: foreign key 24 + ActionId uint64 `gorm:"primaryKey"` 25 + Cid string `gorm:"primaryKey"` 26 + } 27 + 22 28 type ModerationReport struct { 23 29 ID uint64 `gorm:"primaryKey"` 24 30 SubjectType string `gorm:"not null"` ··· 32 38 } 33 39 34 40 type ModerationReportResolution struct { 35 - ReportId uint64 `gorm:"primaryKey"` 41 + // TODO: foreign key 42 + ReportId uint64 `gorm:"primaryKey"` 43 + // TODO: foreign key 36 44 ActionId uint64 `gorm:"primaryKey;index:"` 37 45 CreatedAt time.Time `gorm:"not null"` 38 46 CreatedByDid string `gorm:"not null"`