this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

labelmaker: initial implementation of remaining XRPC endpoints

+398 -115
+158 -55
labeling/admin.go
··· 42 42 return &recordView 43 43 } 44 44 45 - func (s *Server) hydrateModerationActions(ctx context.Context, rows []models.ModerationAction) ([]*comatproto.AdminModerationAction_ViewDetail, error) { 45 + func (s *Server) hydrateModerationActions(ctx context.Context, rows []models.ModerationAction) ([]*comatproto.AdminModerationAction_View, error) { 46 46 47 - // TODO 48 - row := rows[0] 47 + var out []*comatproto.AdminModerationAction_View 49 48 50 - // TODO 51 - resolvedReports := []*comatproto.AdminModerationReport_View{} 52 - subjectBlobs := []*comatproto.AdminBlob_View{} 53 - var reversal *comatproto.AdminModerationAction_Reversal 54 - if row.ReversedAt != nil { 55 - reversal = &comatproto.AdminModerationAction_Reversal{ 56 - CreatedAt: row.ReversedAt.Format(time.RFC3339), 57 - CreatedBy: *row.ReversedByDid, 58 - Reason: *row.ReversedReason, 49 + for _, row := range rows { 50 + // TODO(bnewbold): resolve these 51 + resolvedReportIds := []int64{} 52 + subjectBlobCIDs := []string{} 53 + 54 + var reversal *comatproto.AdminModerationAction_Reversal 55 + if row.ReversedAt != nil { 56 + reversal = &comatproto.AdminModerationAction_Reversal{ 57 + CreatedAt: row.ReversedAt.Format(time.RFC3339), 58 + CreatedBy: *row.ReversedByDid, 59 + Reason: *row.ReversedReason, 60 + } 61 + } 62 + var subj *comatproto.AdminModerationAction_View_Subject 63 + switch row.SubjectType { 64 + case "com.atproto.repo.repoRef": 65 + subj = &comatproto.AdminModerationAction_View_Subject{ 66 + RepoRepoRef: &comatproto.RepoRepoRef{ 67 + LexiconTypeID: "com.atproto.repo.repoRef", 68 + Did: row.SubjectDid, 69 + }, 70 + } 71 + case "com.atproto.repo.recordRef": 72 + subj = &comatproto.AdminModerationAction_View_Subject{ 73 + RepoStrongRef: &comatproto.RepoStrongRef{ 74 + LexiconTypeID: "com.atproto.repo.strongRef", 75 + Uri: *row.SubjectUri, 76 + Cid: *row.SubjectCid, 77 + }, 78 + } 79 + default: 80 + return nil, fmt.Errorf("unsupported moderation SubjectType: %v", row.SubjectType) 81 + } 82 + 83 + view := &comatproto.AdminModerationAction_View{ 84 + Action: &row.Action, 85 + CreatedAt: row.CreatedAt.Format(time.RFC3339), 86 + CreatedBy: row.CreatedByDid, 87 + Id: int64(row.ID), 88 + Reason: row.Reason, 89 + ResolvedReportIds: resolvedReportIds, 90 + Reversal: reversal, 91 + Subject: subj, 92 + SubjectBlobCids: subjectBlobCIDs, 59 93 } 94 + out = append(out, view) 60 95 } 61 - var subj *comatproto.AdminModerationAction_ViewDetail_Subject 62 - switch row.SubjectType { 63 - case "com.atproto.repo.repoRef": 64 - subj = &comatproto.AdminModerationAction_ViewDetail_Subject{ 65 - AdminRepo_View: s.hydrateRepoView(ctx, row.SubjectDid, row.CreatedAt.Format(time.RFC3339)), 96 + return out, nil 97 + } 98 + 99 + func (s *Server) hydrateModerationActionDetails(ctx context.Context, rows []models.ModerationAction) ([]*comatproto.AdminModerationAction_ViewDetail, error) { 100 + 101 + var out []*comatproto.AdminModerationAction_ViewDetail 102 + for _, row := range rows { 103 + 104 + // TODO(bnewbold): resolve these 105 + resolvedReports := []*comatproto.AdminModerationReport_View{} 106 + subjectBlobs := []*comatproto.AdminBlob_View{} 107 + 108 + var reversal *comatproto.AdminModerationAction_Reversal 109 + if row.ReversedAt != nil { 110 + reversal = &comatproto.AdminModerationAction_Reversal{ 111 + CreatedAt: row.ReversedAt.Format(time.RFC3339), 112 + CreatedBy: *row.ReversedByDid, 113 + Reason: *row.ReversedReason, 114 + } 66 115 } 67 - case "com.atproto.repo.recordRef": 68 - subj = &comatproto.AdminModerationAction_ViewDetail_Subject{ 69 - AdminRecord_View: s.hydrateRecordView(ctx, row.SubjectDid, row.SubjectUri, row.SubjectCid, row.CreatedAt.Format(time.RFC3339)), 116 + var subj *comatproto.AdminModerationAction_ViewDetail_Subject 117 + switch row.SubjectType { 118 + case "com.atproto.repo.repoRef": 119 + subj = &comatproto.AdminModerationAction_ViewDetail_Subject{ 120 + AdminRepo_View: s.hydrateRepoView(ctx, row.SubjectDid, row.CreatedAt.Format(time.RFC3339)), 121 + } 122 + case "com.atproto.repo.recordRef": 123 + subj = &comatproto.AdminModerationAction_ViewDetail_Subject{ 124 + AdminRecord_View: s.hydrateRecordView(ctx, row.SubjectDid, row.SubjectUri, row.SubjectCid, row.CreatedAt.Format(time.RFC3339)), 125 + } 126 + default: 127 + return nil, fmt.Errorf("unsupported moderation SubjectType: %v", row.SubjectType) 70 128 } 71 - default: 72 - return nil, fmt.Errorf("unsupported moderation SubjectType: %v", row.SubjectType) 73 - } 74 129 75 - viewDetail := &comatproto.AdminModerationAction_ViewDetail{ 76 - Action: &row.Action, 77 - CreatedAt: row.CreatedAt.Format(time.RFC3339), 78 - CreatedBy: row.CreatedByDid, 79 - Id: int64(row.ID), 80 - Reason: row.Reason, 81 - ResolvedReports: resolvedReports, 82 - Reversal: reversal, 83 - Subject: subj, 84 - SubjectBlobs: subjectBlobs, 130 + viewDetail := &comatproto.AdminModerationAction_ViewDetail{ 131 + Action: &row.Action, 132 + CreatedAt: row.CreatedAt.Format(time.RFC3339), 133 + CreatedBy: row.CreatedByDid, 134 + Id: int64(row.ID), 135 + Reason: row.Reason, 136 + ResolvedReports: resolvedReports, 137 + Reversal: reversal, 138 + Subject: subj, 139 + SubjectBlobs: subjectBlobs, 140 + } 141 + out = append(out, viewDetail) 85 142 } 86 - return []*comatproto.AdminModerationAction_ViewDetail{viewDetail}, nil 143 + return out, nil 87 144 } 88 145 89 - func (s *Server) hydrateModerationReports(ctx context.Context, rows []models.ModerationReport) ([]*comatproto.AdminModerationReport_ViewDetail, error) { 90 - // TODO 91 - row := rows[0] 146 + func (s *Server) hydrateModerationReports(ctx context.Context, rows []models.ModerationReport) ([]*comatproto.AdminModerationReport_View, error) { 147 + 148 + var out []*comatproto.AdminModerationReport_View 149 + for _, row := range rows { 150 + // TODO(bnewbold): fetch these IDs 151 + var resolvedByActionIds []int64 92 152 93 - var subj *comatproto.AdminModerationReport_ViewDetail_Subject 94 - switch row.SubjectType { 95 - case "com.atproto.repo.repoRef": 96 - subj = &comatproto.AdminModerationReport_ViewDetail_Subject{ 97 - AdminRepo_View: s.hydrateRepoView(ctx, row.SubjectDid, row.CreatedAt.Format(time.RFC3339)), 153 + var subj *comatproto.AdminModerationReport_View_Subject 154 + switch row.SubjectType { 155 + case "com.atproto.repo.repoRef": 156 + subj = &comatproto.AdminModerationReport_View_Subject{ 157 + RepoRepoRef: &comatproto.RepoRepoRef{ 158 + LexiconTypeID: "com.atproto.repo.repoRef", 159 + Did: row.SubjectDid, 160 + }, 161 + } 162 + case "com.atproto.repo.recordRef": 163 + subj = &comatproto.AdminModerationReport_View_Subject{ 164 + RepoStrongRef: &comatproto.RepoStrongRef{ 165 + LexiconTypeID: "com.atproto.repo.strongRef", 166 + Uri: *row.SubjectUri, 167 + Cid: *row.SubjectCid, 168 + }, 169 + } 170 + default: 171 + return nil, fmt.Errorf("unsupported moderation SubjectType: %v", row.SubjectType) 98 172 } 99 - case "com.atproto.repo.recordRef": 100 - subj = &comatproto.AdminModerationReport_ViewDetail_Subject{ 101 - AdminRecord_View: s.hydrateRecordView(ctx, row.SubjectDid, row.SubjectUri, row.SubjectCid, row.CreatedAt.Format(time.RFC3339)), 173 + 174 + view := &comatproto.AdminModerationReport_View{ 175 + Id: int64(row.ID), 176 + ReasonType: &row.ReasonType, 177 + Subject: subj, 178 + ReportedByDid: row.ReportedByDid, 179 + CreatedAt: row.CreatedAt.Format(time.RFC3339), 180 + ResolvedByActionIds: resolvedByActionIds, 102 181 } 103 - default: 104 - return nil, fmt.Errorf("unsupported moderation SubjectType: %v", row.SubjectType) 182 + out = append(out, view) 105 183 } 184 + return out, nil 185 + } 106 186 107 - viewDetail := &comatproto.AdminModerationReport_ViewDetail{ 108 - Id: int64(row.ID), 109 - ReasonType: &row.ReasonType, 110 - Subject: subj, 111 - ReportedByDid: row.ReportedByDid, 112 - CreatedAt: row.CreatedAt.Format(time.RFC3339), 113 - // TODO: ResolvedByActions 187 + func (s *Server) hydrateModerationReportDetails(ctx context.Context, rows []models.ModerationReport) ([]*comatproto.AdminModerationReport_ViewDetail, error) { 188 + 189 + var out []*comatproto.AdminModerationReport_ViewDetail 190 + for _, row := range rows { 191 + // TODO(bnewbold): fetch these objects 192 + var resolvedByActions []*comatproto.AdminModerationAction_View 193 + 194 + var subj *comatproto.AdminModerationReport_ViewDetail_Subject 195 + switch row.SubjectType { 196 + case "com.atproto.repo.repoRef": 197 + subj = &comatproto.AdminModerationReport_ViewDetail_Subject{ 198 + AdminRepo_View: s.hydrateRepoView(ctx, row.SubjectDid, row.CreatedAt.Format(time.RFC3339)), 199 + } 200 + case "com.atproto.repo.recordRef": 201 + subj = &comatproto.AdminModerationReport_ViewDetail_Subject{ 202 + AdminRecord_View: s.hydrateRecordView(ctx, row.SubjectDid, row.SubjectUri, row.SubjectCid, row.CreatedAt.Format(time.RFC3339)), 203 + } 204 + default: 205 + return nil, fmt.Errorf("unsupported moderation SubjectType: %v", row.SubjectType) 206 + } 207 + 208 + viewDetail := &comatproto.AdminModerationReport_ViewDetail{ 209 + Id: int64(row.ID), 210 + ReasonType: &row.ReasonType, 211 + Subject: subj, 212 + ReportedByDid: row.ReportedByDid, 213 + CreatedAt: row.CreatedAt.Format(time.RFC3339), 214 + ResolvedByActions: resolvedByActions, 215 + } 216 + out = append(out, viewDetail) 114 217 } 115 - return []*comatproto.AdminModerationReport_ViewDetail{viewDetail}, nil 218 + return out, nil 116 219 }
+5 -43
labeling/xrpc_endpoints.go
··· 1 1 package labeling 2 2 3 3 import ( 4 - "io" 5 4 "net/http/httputil" 6 5 "strconv" 7 6 ··· 17 16 e.GET("/xrpc/com.atproto.identity.resolveHandle", s.HandleComAtprotoIdentityResolveHandle) 18 17 e.GET("/xrpc/com.atproto.server.describeServer", s.HandleComAtprotoServerDescribeServer) 19 18 // TODO: session create/refresh/delete? 20 - //e.GET("/xrpc/com.atproto.account.get", s.HandleComAtprotoAccountGet) 21 19 22 - // label-specific 23 - e.GET("/xrpc/com.atproto.label.query", s.HandleComAtprotoLabelQuery) 24 20 25 21 // minimal moderation reporting/actioning 26 22 e.GET("/xrpc/com.atproto.admin.getModerationAction", s.HandleComAtprotoAdminGetModerationAction) ··· 31 27 e.POST("/xrpc/com.atproto.admin.reverseModerationAction", s.HandleComAtprotoAdminReverseModerationAction) 32 28 e.POST("/xrpc/com.atproto.admin.takeModerationAction", s.HandleComAtprotoAdminTakeModerationAction) 33 29 e.POST("/xrpc/com.atproto.report.create", s.HandleComAtprotoReportCreate) 30 + 31 + // label-specific 32 + e.GET("/xrpc/com.atproto.label.query", s.HandleComAtprotoLabelQuery) 34 33 35 34 return nil 36 35 } ··· 98 97 return c.JSON(200, out) 99 98 } 100 99 101 - func (s *Server) HandleComAtprotoRepoListRecords(c echo.Context) error { 102 - ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleComAtprotoRepoListRecords") 103 - defer span.End() 104 - collection := c.QueryParam("collection") 105 - 106 - var limit int 107 - if p := c.QueryParam("limit"); p != "" { 108 - var err error 109 - limit, err = strconv.Atoi(p) 110 - if err != nil { 111 - return err 112 - } 113 - } else { 114 - limit = 50 115 - } 116 - repo := c.QueryParam("repo") 117 - 118 - var reverse *bool 119 - if p := c.QueryParam("reverse"); p != "" { 120 - reverse_val, err := strconv.ParseBool(p) 121 - if err != nil { 122 - return err 123 - } 124 - reverse = &reverse_val 125 - } 126 - rkeyEnd := c.QueryParam("rkeyEnd") 127 - rkeyStart := c.QueryParam("rkeyStart") 128 - var out *atproto.RepoListRecords_Output 129 - var handleErr error 130 - // func (s *Server) handleComAtprotoRepoListRecords(ctx context.Context,collection string,limit int,repo string,reverse *bool,rkeyEnd string,rkeyStart string) (*comatprototypes.RepoListRecords_Output, error) 131 - out, handleErr = s.handleComAtprotoRepoListRecords(ctx, collection, limit, repo, reverse, rkeyEnd, rkeyStart) 132 - if handleErr != nil { 133 - return handleErr 134 - } 135 - return c.JSON(200, out) 136 - } 137 - 138 100 func (s *Server) HandleComAtprotoServerDescribeServer(c echo.Context) error { 139 101 ctx, span := otel.Tracer("server").Start(c.Request().Context(), "HandleComAtprotoServerDescribeServer") 140 102 defer span.End() 141 103 var out *atproto.ServerDescribeServer_Output 142 104 var handleErr error 143 - // func (s *Server) handleComAtprotoServerDescribeServer(ctx context.Context) (*atproto.ServerDescribeServer_Output, error) 144 - out, handleErr = s.handleComAtprotoServerDescribeServer(ctx) 105 + // func (s *Server) handleComAtprotoServerGetAccountsConfig(ctx context.Context) (*atproto.ServerGetAccountsConfig_Output, error) 106 + out, handleErr = s.handleComAtprotoServerGetAccountsConfig(ctx) 145 107 if handleErr != nil { 146 108 return handleErr 147 109 }
+235 -17
labeling/xrpc_handlers.go
··· 16 16 "github.com/bluesky-social/indigo/util" 17 17 18 18 "github.com/ipfs/go-cid" 19 + "github.com/labstack/echo/v4" 19 20 ) 20 21 21 22 func (s *Server) handleComAtprotoIdentityResolveHandle(ctx context.Context, handle string) (*atproto.IdentityResolveHandle_Output, error) { ··· 25 26 } else if handle == s.user.Handle { 26 27 return &atproto.IdentityResolveHandle_Output{Did: s.user.Did}, nil 27 28 } else { 28 - return nil, fmt.Errorf("handle not found: %s", handle) 29 + return nil, echo.NewHTTPError(404, "user not found: %s", handle) 29 30 } 30 31 } 31 32 32 33 func (s *Server) handleComAtprotoRepoDescribeRepo(ctx context.Context, repo string) (*atproto.RepoDescribeRepo_Output, error) { 33 - panic("not yet implemented") 34 - } 35 - 36 - func (s *Server) handleComAtprotoRepoListRecords(ctx context.Context, collection string, limit int, repo string, reverse *bool, rkeyEnd string, rkeyStart string) (*atproto.RepoListRecords_Output, error) { 37 - panic("not yet implemented") 34 + if user == s.user.Did || user == s.user.Handle { 35 + return &atproto.RepoDescribe_Output{ 36 + Collections: []string{}, 37 + Did: s.user.Did, 38 + //DidDoc 39 + Handle: s.user.Handle, 40 + HandleIsCorrect: true, 41 + }, nil 42 + } 43 + if user == "" { 44 + return nil, echo.NewHTTPError(400, "empty user parameter") 45 + } else { 46 + return nil, echo.NewHTTPError(404, "user not found") 47 + } 38 48 } 39 49 40 50 func (s *Server) handleComAtprotoServerDescribeServer(ctx context.Context) (*atproto.ServerDescribeServer_Output, error) { ··· 104 114 }, nil 105 115 } 106 116 107 - func (s *Server) handleComAtprotoSyncGetHead(ctx context.Context, did string) (*atproto.SyncGetHead_Output, error) { 108 - panic("not yet implemented") 109 - } 110 - 111 117 func (s *Server) handleComAtprotoLabelQuery(ctx context.Context, cursor string, limit int, sources, uriPatterns []string) (*label.Query_Output, error) { 112 118 113 119 if limit <= 0 { ··· 190 196 return nil, result.Error 191 197 } 192 198 193 - full, err := s.hydrateModerationActions(ctx, []models.ModerationAction{row}) 199 + full, err := s.hydrateModerationActionDetails(ctx, []models.ModerationAction{row}) 194 200 if err != nil { 195 201 return nil, err 196 202 } ··· 198 204 } 199 205 200 206 func (s *Server) handleComAtprotoAdminGetModerationActions(ctx context.Context, before string, limit int, subject string) (*atproto.AdminGetModerationActions_Output, error) { 201 - panic("nyi") 207 + 208 + if limit <= 0 { 209 + limit = 20 210 + } 211 + if limit > 100 { 212 + limit = 100 213 + } 214 + 215 + q := s.db.Limit(limit).Order("id desc") 216 + cursor := before 217 + if cursor != "" { 218 + cursorID, err := strconv.Atoi(cursor) 219 + if err != nil { 220 + // XXX: HTTP 400 error 221 + return nil, err 222 + } 223 + q = q.Where("id < ?", cursorID) 224 + } 225 + 226 + if subject != "" { 227 + q = q.Where("subject = ?", subject) 228 + } 229 + 230 + var actionRows []models.ModerationAction 231 + result := q.Find(&actionRows) 232 + if result.Error != nil { 233 + return nil, result.Error 234 + } 235 + 236 + var nextCursor string 237 + if len(actionRows) >= 1 && len(actionRows) == limit { 238 + nextCursor = strconv.FormatUint(actionRows[len(actionRows)-1].ID, 10) 239 + } 240 + 241 + actionObjs, err := s.hydrateModerationActions(ctx, actionRows) 242 + if err != nil { 243 + return nil, err 244 + } 245 + out := atproto.AdminGetModerationActions_Output{ 246 + Actions: actionObjs, 247 + } 248 + if nextCursor != "" { 249 + out.Cursor = &nextCursor 250 + } 251 + return &out, nil 202 252 } 203 253 204 254 func (s *Server) handleComAtprotoAdminGetModerationReport(ctx context.Context, id int) (*atproto.AdminModerationReport_ViewDetail, error) { ··· 209 259 return nil, result.Error 210 260 } 211 261 212 - full, err := s.hydrateModerationReports(ctx, []models.ModerationReport{row}) 262 + full, err := s.hydrateModerationReportDetails(ctx, []models.ModerationReport{row}) 213 263 if err != nil { 214 264 return nil, err 215 265 } ··· 217 267 } 218 268 219 269 func (s *Server) handleComAtprotoAdminGetModerationReports(ctx context.Context, before string, limit int, resolved *bool, subject string) (*atproto.AdminGetModerationReports_Output, error) { 220 - panic("nyi") 270 + 271 + if limit <= 0 { 272 + limit = 20 273 + } 274 + if limit > 100 { 275 + limit = 100 276 + } 277 + 278 + q := s.db.Limit(limit).Order("id desc") 279 + cursor := before 280 + if cursor != "" { 281 + cursorID, err := strconv.Atoi(cursor) 282 + if err != nil { 283 + // XXX: HTTP 400 error 284 + return nil, err 285 + } 286 + q = q.Where("id < ?", cursorID) 287 + } 288 + 289 + if subject != "" { 290 + q = q.Where("subject = ?", subject) 291 + } 292 + 293 + var reportRows []models.ModerationReport 294 + result := q.Find(&reportRows) 295 + if result.Error != nil { 296 + return nil, result.Error 297 + } 298 + 299 + var nextCursor string 300 + if len(reportRows) >= 1 && len(reportRows) == limit { 301 + nextCursor = strconv.FormatUint(reportRows[len(reportRows)-1].ID, 10) 302 + } 303 + 304 + reportObjs, err := s.hydrateModerationReports(ctx, reportRows) 305 + if err != nil { 306 + return nil, err 307 + } 308 + 309 + // TODO: a bit inefficient to do this filter after hydration. could do it 310 + // in the SQL query instead, but this was faster to implement right now 311 + if resolved != nil { 312 + var filtered []*atproto.AdminModerationReport_View 313 + for _, obj := range reportObjs { 314 + if *resolved == true && len(obj.ResolvedByActionIds) > 0 { 315 + filtered = append(filtered, obj) 316 + } 317 + if *resolved == false && len(obj.ResolvedByActionIds) == 0 { 318 + filtered = append(filtered, obj) 319 + } 320 + } 321 + reportObjs = filtered 322 + } 323 + 324 + out := atproto.AdminGetModerationReports_Output{ 325 + Reports: reportObjs, 326 + } 327 + if nextCursor != "" { 328 + out.Cursor = &nextCursor 329 + } 330 + return &out, nil 221 331 } 222 332 223 333 func (s *Server) handleComAtprotoAdminResolveModerationReports(ctx context.Context, body *atproto.AdminResolveModerationReports_Input) (*atproto.AdminModerationAction_View, error) { 224 - panic("nyi") 334 + 335 + // XXX: check that body fields are not nil/empty: CreatedBy, ReportIds 336 + 337 + var rows []models.ModerationReportResolution 338 + for _, reportId := range body.ReportIds { 339 + rows = append(rows, models.ModerationReportResolution{ 340 + ReportId: uint64(reportId), 341 + ActionId: uint64(body.ActionId), 342 + CreatedByDid: body.CreatedBy, 343 + }) 344 + } 345 + result := s.db.Create(&rows) 346 + if result.Error != nil { 347 + return nil, result.Error 348 + } 349 + 350 + return s.fetchSingleModerationAction(ctx, body.ActionId) 351 + } 352 + 353 + // helper for endpoints that return a partially hydrated moderation action 354 + func (s *Server) fetchSingleModerationAction(ctx context.Context, actionId int64) (*atproto.AdminModerationAction_View, error) { 355 + var actionRow models.ModerationAction 356 + result := s.db.First(&actionRow, actionId) 357 + if result.Error != nil { 358 + return nil, result.Error 359 + } 360 + 361 + actionObjs, err := s.hydrateModerationActions(ctx, []models.ModerationAction{actionRow}) 362 + if err != nil { 363 + return nil, err 364 + } 365 + return actionObjs[0], nil 225 366 } 226 367 227 368 func (s *Server) handleComAtprotoAdminReverseModerationAction(ctx context.Context, body *atproto.AdminReverseModerationAction_Input) (*atproto.AdminModerationAction_View, error) { 228 - panic("nyi") 369 + 370 + // XXX: validate body CreatedBy, Reason 371 + 372 + row := models.ModerationAction{ID: uint64(body.Id)} 373 + result := s.db.First(&row) 374 + if result.Error != nil { 375 + // XXX: if not found, 404 376 + return nil, result.Error 377 + } 378 + 379 + if row.ReversedAt != nil { 380 + // XXX: http 400 (already reversed) 381 + return nil, fmt.Errorf("action has already been reversed actionId=%d", body.Id) 382 + } 383 + 384 + now := time.Now() 385 + row.ReversedByDid = &body.CreatedBy 386 + row.ReversedReason = &body.Reason 387 + row.ReversedAt = &now 388 + 389 + result = s.db.Save(&row) 390 + if result.Error != nil { 391 + return nil, result.Error 392 + } 393 + 394 + return s.fetchSingleModerationAction(ctx, body.Id) 229 395 } 230 396 231 397 func (s *Server) handleComAtprotoAdminTakeModerationAction(ctx context.Context, body *atproto.AdminTakeModerationAction_Input) (*atproto.AdminModerationAction_View, error) { 232 - panic("nyi") 398 + 399 + // XXX: check that Action, CreatedBy, and Reason are all non-empty 400 + 401 + // XXX: SubjectBlobCids (how does atproto do it? array in postgresql?) 402 + row := models.ModerationAction{ 403 + Action: body.Action, 404 + Reason: body.Reason, 405 + CreatedByDid: body.CreatedBy, 406 + } 407 + 408 + var outSubj atproto.AdminModerationAction_View_Subject 409 + if body.Subject.RepoRepoRef != nil { 410 + row.SubjectType = "com.atproto.repo.repoRef" 411 + row.SubjectDid = body.Subject.RepoRepoRef.Did 412 + outSubj.RepoRepoRef = &atproto.RepoRepoRef{ 413 + LexiconTypeID: "com.atproto.repo.repoRef", 414 + Did: row.SubjectDid, 415 + } 416 + } else if body.Subject.RepoRecordRef != nil { 417 + if row.SubjectCid == nil { 418 + return nil, fmt.Errorf("this implementation requires a strong record ref (aka, with CID) in reports") 419 + } 420 + row.SubjectType = "com.atproto.repo.recordRef" 421 + // TODO: row.SubjectDid from URI? 422 + row.SubjectUri = &body.Subject.RepoRecordRef.Uri 423 + row.SubjectCid = body.Subject.RepoRecordRef.Cid 424 + outSubj.RepoStrongRef = &atproto.RepoStrongRef{ 425 + LexiconTypeID: "com.atproto.repo.strongRef", 426 + Uri: *row.SubjectUri, 427 + Cid: *row.SubjectCid, 428 + } 429 + } else { 430 + // XXX: 400 error (and similar instances) 431 + return nil, fmt.Errorf("report subject must be a repoRef or a recordRef") 432 + } 433 + 434 + result := s.db.Create(&row) 435 + if result.Error != nil { 436 + return nil, result.Error 437 + } 438 + 439 + out := atproto.AdminModerationAction_View{ 440 + Id: int64(row.ID), 441 + Action: &row.Action, 442 + Reason: row.Reason, 443 + CreatedBy: row.CreatedByDid, 444 + CreatedAt: row.CreatedAt.Format(time.RFC3339), 445 + Subject: &outSubj, 446 + // XXX: SubjectBlobCids 447 + } 448 + return &out, nil 233 449 } 234 450 235 451 func (s *Server) handleComAtprotoReportCreate(ctx context.Context, body *atproto.ReportCreate_Input) (*atproto.ReportCreate_Output, error) { 236 452 237 453 // TODO: shouldn't lexgen and the endpoint handlers help with these already? both are required fields 238 454 if body.ReasonType == nil { 455 + // XXX: 400 error 239 456 return nil, fmt.Errorf("ReasonType is required") 240 457 } 241 458 if body.Subject == nil { 459 + // XXX: 400 error 242 460 return nil, fmt.Errorf("Subject is required") 243 461 } 244 462