diff options
author | 2023-01-25 11:12:17 +0100 | |
---|---|---|
committer | 2023-01-25 11:12:17 +0100 | |
commit | faeb7ded3b5d595910f424fd9cf9c6fe5935e648 (patch) | |
tree | 5c50b950277ab985e73bfaf027b53ee82f4917a6 /internal/typeutils | |
parent | [chore] Settings refactor fix4 (#1383) (diff) | |
download | gotosocial-faeb7ded3b5d595910f424fd9cf9c6fe5935e648.tar.xz |
[feature] Implement reports admin API so admins can view + close reports (#1378)
* add admin report api endpoints + tests
* [chore] remove funky duplicate attachment in testrig
Diffstat (limited to 'internal/typeutils')
-rw-r--r-- | internal/typeutils/converter.go | 2 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 166 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 366 |
3 files changed, 533 insertions, 1 deletions
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index be05a8a48..c7fd31470 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -89,6 +89,8 @@ type TypeConverter interface { DomainBlockToAPIDomainBlock(ctx context.Context, b *gtsmodel.DomainBlock, export bool) (*apimodel.DomainBlock, error) // ReportToAPIReport converts a gts model report into an api model report, for serving at /api/v1/reports ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) (*apimodel.Report, error) + // ReportToAdminAPIReport converts a gts model report into an admin view report, for serving at /api/v1/admin/reports + ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Report, requestingAccount *gtsmodel.Account) (*apimodel.AdminReport, error) /* INTERNAL (gts) MODEL TO FRONTEND (rss) MODEL diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index dbd1a3822..2483fc5ba 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -256,6 +256,83 @@ func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel. }, nil } +func (c *converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Account) (*apimodel.AdminAccountInfo, error) { + var ( + email string + ip *string + domain *string + locale string + confirmed bool + inviteRequest *string + approved bool + disabled bool + silenced bool + suspended bool + role apimodel.AccountRole = apimodel.AccountRoleUser // assume user by default + createdByApplicationID string + ) + + // take user-level information if possible + if a.Domain != "" { + domain = &a.Domain + } else { + user, err := c.db.GetUserByAccountID(ctx, a.ID) + if err != nil { + return nil, fmt.Errorf("AccountToAdminAPIAccount: error getting user from database for account id %s: %w", a.ID, err) + } + + if user.Email != "" { + email = user.Email + } else { + email = user.UnconfirmedEmail + } + + if i := user.CurrentSignInIP.String(); i != "<nil>" { + ip = &i + } + + locale = user.Locale + inviteRequest = &user.Account.Reason + if *user.Admin { + role = apimodel.AccountRoleAdmin + } else if *user.Moderator { + role = apimodel.AccountRoleModerator + } + confirmed = !user.ConfirmedAt.IsZero() + approved = *user.Approved + disabled = *user.Disabled + silenced = !user.Account.SilencedAt.IsZero() + suspended = !user.Account.SuspendedAt.IsZero() + createdByApplicationID = user.CreatedByApplicationID + } + + apiAccount, err := c.AccountToAPIAccountPublic(ctx, a) + if err != nil { + return nil, fmt.Errorf("AccountToAdminAPIAccount: error converting account to api account for account id %s: %w", a.ID, err) + } + + return &apimodel.AdminAccountInfo{ + ID: a.ID, + Username: a.Username, + Domain: domain, + CreatedAt: util.FormatISO8601(a.CreatedAt), + Email: email, + IP: ip, + IPs: []interface{}{}, // not implemented, + Locale: locale, + InviteRequest: inviteRequest, + Role: string(role), + Confirmed: confirmed, + Approved: approved, + Disabled: disabled, + Silenced: silenced, + Suspended: suspended, + Account: apiAccount, + CreatedByApplicationID: createdByApplicationID, + InvitedByAccountID: "", // not implemented (yet) + }, nil +} + func (c *converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Application) (*apimodel.Application, error) { return &apimodel.Application{ ID: a.ID, @@ -825,7 +902,7 @@ func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) ( } if actionComment := r.ActionTaken; actionComment != "" { - report.ActionComment = &actionComment + report.ActionTakenComment = &actionComment } if r.TargetAccount == nil { @@ -845,6 +922,93 @@ func (c *converter) ReportToAPIReport(ctx context.Context, r *gtsmodel.Report) ( return report, nil } +func (c *converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Report, requestingAccount *gtsmodel.Account) (*apimodel.AdminReport, error) { + var ( + err error + actionTakenAt *string + actionTakenComment *string + actionTakenByAccount *apimodel.AdminAccountInfo + ) + + if !r.ActionTakenAt.IsZero() { + ata := util.FormatISO8601(r.ActionTakenAt) + actionTakenAt = &ata + } + + if r.Account == nil { + r.Account, err = c.db.GetAccountByID(ctx, r.AccountID) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error getting account with id %s from the db: %w", r.AccountID, err) + } + } + account, err := c.AccountToAdminAPIAccount(ctx, r.Account) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error converting account with id %s to adminAPIAccount: %w", r.AccountID, err) + } + + if r.TargetAccount == nil { + r.TargetAccount, err = c.db.GetAccountByID(ctx, r.TargetAccountID) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error getting target account with id %s from the db: %w", r.TargetAccountID, err) + } + } + targetAccount, err := c.AccountToAdminAPIAccount(ctx, r.TargetAccount) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error converting target account with id %s to adminAPIAccount: %w", r.TargetAccountID, err) + } + + if r.ActionTakenByAccountID != "" { + if r.ActionTakenByAccount == nil { + r.ActionTakenByAccount, err = c.db.GetAccountByID(ctx, r.ActionTakenByAccountID) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error getting action taken by account with id %s from the db: %w", r.ActionTakenByAccountID, err) + } + } + + actionTakenByAccount, err = c.AccountToAdminAPIAccount(ctx, r.ActionTakenByAccount) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error converting action taken by account with id %s to adminAPIAccount: %w", r.ActionTakenByAccountID, err) + } + } + + statuses := make([]*apimodel.Status, 0, len(r.StatusIDs)) + if len(r.StatusIDs) != 0 && len(r.Statuses) == 0 { + r.Statuses, err = c.db.GetStatuses(ctx, r.StatusIDs) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error getting statuses from the db: %w", err) + } + } + for _, s := range r.Statuses { + status, err := c.StatusToAPIStatus(ctx, s, requestingAccount) + if err != nil { + return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err) + } + statuses = append(statuses, status) + } + + if ac := r.ActionTaken; ac != "" { + actionTakenComment = &ac + } + + return &apimodel.AdminReport{ + ID: r.ID, + ActionTaken: !r.ActionTakenAt.IsZero(), + ActionTakenAt: actionTakenAt, + Category: "other", // todo: only support default 'other' category right now + Comment: r.Comment, + Forwarded: *r.Forwarded, + CreatedAt: util.FormatISO8601(r.CreatedAt), + UpdatedAt: util.FormatISO8601(r.UpdatedAt), + Account: account, + TargetAccount: targetAccount, + AssignedAccount: actionTakenByAccount, + ActionTakenByAccount: actionTakenByAccount, + ActionTakenComment: actionTakenComment, + Statuses: statuses, + Rules: []interface{}{}, // not implemented + }, nil +} + // convertAttachmentsToAPIAttachments will convert a slice of GTS model attachments to frontend API model attachments, falling back to IDs if no GTS models supplied. func (c *converter) convertAttachmentsToAPIAttachments(ctx context.Context, attachments []*gtsmodel.MediaAttachment, attachmentIDs []string) ([]apimodel.Attachment, error) { var errs gtserror.MultiError diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 7fd08ee05..0c888a521 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -691,6 +691,372 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() { }`, string(b)) } +func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() { + requestingAccount := suite.testAccounts["admin_account"] + adminReport, err := suite.typeconverter.ReportToAdminAPIReport(context.Background(), suite.testReports["remote_account_1_report_local_account_2"], requestingAccount) + suite.NoError(err) + + b, err := json.MarshalIndent(adminReport, "", " ") + suite.NoError(err) + + suite.Equal(`{ + "id": "01GP3DFY9XQ1TJMZT5BGAZPXX7", + "action_taken": true, + "action_taken_at": "2022-05-15T15:01:56.000Z", + "category": "other", + "comment": "this is a turtle, not a person, therefore should not be a poster", + "forwarded": true, + "created_at": "2022-05-15T14:20:12.000Z", + "updated_at": "2022-05-15T14:20:12.000Z", + "account": { + "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX", + "username": "foss_satan", + "domain": "fossbros-anonymous.io", + "created_at": "2021-09-26T10:52:36.000Z", + "email": "", + "ip": null, + "ips": [], + "locale": "", + "invite_request": null, + "role": "user", + "confirmed": false, + "approved": false, + "disabled": false, + "silenced": false, + "suspended": false, + "account": { + "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX", + "username": "foss_satan", + "acct": "foss_satan@fossbros-anonymous.io", + "display_name": "big gerald", + "locked": false, + "bot": false, + "created_at": "2021-09-26T10:52:36.000Z", + "note": "i post about like, i dunno, stuff, or whatever!!!!", + "url": "http://fossbros-anonymous.io/@foss_satan", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 0, + "following_count": 0, + "statuses_count": 1, + "last_status_at": "2021-09-20T10:40:37.000Z", + "emojis": [], + "fields": [] + } + }, + "target_account": { + "id": "01F8MH5NBDF2MV7CTC4Q5128HF", + "username": "1happyturtle", + "domain": null, + "created_at": "2022-06-04T13:12:00.000Z", + "email": "tortle.dude@example.org", + "ip": "118.44.18.196", + "ips": [], + "locale": "en", + "invite_request": "", + "role": "user", + "confirmed": true, + "approved": true, + "disabled": false, + "silenced": false, + "suspended": false, + "account": { + "id": "01F8MH5NBDF2MV7CTC4Q5128HF", + "username": "1happyturtle", + "acct": "1happyturtle", + "display_name": "happy little turtle :3", + "locked": true, + "bot": false, + "created_at": "2022-06-04T13:12:00.000Z", + "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e", + "url": "http://localhost:8080/@1happyturtle", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 1, + "following_count": 1, + "statuses_count": 7, + "last_status_at": "2021-10-20T10:40:37.000Z", + "emojis": [], + "fields": [], + "role": "user" + }, + "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG" + }, + "assigned_account": { + "id": "01F8MH17FWEB39HZJ76B6VXSKF", + "username": "admin", + "domain": null, + "created_at": "2022-05-17T13:10:59.000Z", + "email": "admin@example.org", + "ip": "89.122.255.1", + "ips": [], + "locale": "en", + "invite_request": "", + "role": "admin", + "confirmed": true, + "approved": true, + "disabled": false, + "silenced": false, + "suspended": false, + "account": { + "id": "01F8MH17FWEB39HZJ76B6VXSKF", + "username": "admin", + "acct": "admin", + "display_name": "", + "locked": false, + "bot": false, + "created_at": "2022-05-17T13:10:59.000Z", + "note": "", + "url": "http://localhost:8080/@admin", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 1, + "following_count": 1, + "statuses_count": 4, + "last_status_at": "2021-10-20T10:41:37.000Z", + "emojis": [], + "fields": [], + "enable_rss": true, + "role": "admin" + }, + "created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F" + }, + "action_taken_by_account": { + "id": "01F8MH17FWEB39HZJ76B6VXSKF", + "username": "admin", + "domain": null, + "created_at": "2022-05-17T13:10:59.000Z", + "email": "admin@example.org", + "ip": "89.122.255.1", + "ips": [], + "locale": "en", + "invite_request": "", + "role": "admin", + "confirmed": true, + "approved": true, + "disabled": false, + "silenced": false, + "suspended": false, + "account": { + "id": "01F8MH17FWEB39HZJ76B6VXSKF", + "username": "admin", + "acct": "admin", + "display_name": "", + "locked": false, + "bot": false, + "created_at": "2022-05-17T13:10:59.000Z", + "note": "", + "url": "http://localhost:8080/@admin", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 1, + "following_count": 1, + "statuses_count": 4, + "last_status_at": "2021-10-20T10:41:37.000Z", + "emojis": [], + "fields": [], + "enable_rss": true, + "role": "admin" + }, + "created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F" + }, + "statuses": [], + "rule_ids": [], + "action_taken_comment": "user was warned not to be a turtle anymore" +}`, string(b)) +} + +func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() { + requestingAccount := suite.testAccounts["admin_account"] + adminReport, err := suite.typeconverter.ReportToAdminAPIReport(context.Background(), suite.testReports["local_account_2_report_remote_account_1"], requestingAccount) + suite.NoError(err) + + b, err := json.MarshalIndent(adminReport, "", " ") + suite.NoError(err) + + suite.Equal(`{ + "id": "01GP3AWY4CRDVRNZKW0TEAMB5R", + "action_taken": false, + "action_taken_at": null, + "category": "other", + "comment": "dark souls sucks, please yeet this nerd", + "forwarded": true, + "created_at": "2022-05-14T10:20:03.000Z", + "updated_at": "2022-05-14T10:20:03.000Z", + "account": { + "id": "01F8MH5NBDF2MV7CTC4Q5128HF", + "username": "1happyturtle", + "domain": null, + "created_at": "2022-06-04T13:12:00.000Z", + "email": "tortle.dude@example.org", + "ip": "118.44.18.196", + "ips": [], + "locale": "en", + "invite_request": "", + "role": "user", + "confirmed": true, + "approved": true, + "disabled": false, + "silenced": false, + "suspended": false, + "account": { + "id": "01F8MH5NBDF2MV7CTC4Q5128HF", + "username": "1happyturtle", + "acct": "1happyturtle", + "display_name": "happy little turtle :3", + "locked": true, + "bot": false, + "created_at": "2022-06-04T13:12:00.000Z", + "note": "\u003cp\u003ei post about things that concern me\u003c/p\u003e", + "url": "http://localhost:8080/@1happyturtle", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 1, + "following_count": 1, + "statuses_count": 7, + "last_status_at": "2021-10-20T10:40:37.000Z", + "emojis": [], + "fields": [], + "role": "user" + }, + "created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG" + }, + "target_account": { + "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX", + "username": "foss_satan", + "domain": "fossbros-anonymous.io", + "created_at": "2021-09-26T10:52:36.000Z", + "email": "", + "ip": null, + "ips": [], + "locale": "", + "invite_request": null, + "role": "user", + "confirmed": false, + "approved": false, + "disabled": false, + "silenced": false, + "suspended": false, + "account": { + "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX", + "username": "foss_satan", + "acct": "foss_satan@fossbros-anonymous.io", + "display_name": "big gerald", + "locked": false, + "bot": false, + "created_at": "2021-09-26T10:52:36.000Z", + "note": "i post about like, i dunno, stuff, or whatever!!!!", + "url": "http://fossbros-anonymous.io/@foss_satan", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 0, + "following_count": 0, + "statuses_count": 1, + "last_status_at": "2021-09-20T10:40:37.000Z", + "emojis": [], + "fields": [] + } + }, + "assigned_account": null, + "action_taken_by_account": null, + "statuses": [ + { + "id": "01FVW7JHQFSFK166WWKR8CBA6M", + "created_at": "2021-09-20T10:40:37.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "unlisted", + "language": "en", + "uri": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M", + "url": "http://fossbros-anonymous.io/@foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M", + "replies_count": 0, + "reblogs_count": 0, + "favourites_count": 0, + "favourited": false, + "reblogged": false, + "muted": false, + "bookmarked": false, + "pinned": false, + "content": "dark souls status bot: \"thoughts of dog\"", + "reblog": null, + "account": { + "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX", + "username": "foss_satan", + "acct": "foss_satan@fossbros-anonymous.io", + "display_name": "big gerald", + "locked": false, + "bot": false, + "created_at": "2021-09-26T10:52:36.000Z", + "note": "i post about like, i dunno, stuff, or whatever!!!!", + "url": "http://fossbros-anonymous.io/@foss_satan", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 0, + "following_count": 0, + "statuses_count": 1, + "last_status_at": "2021-09-20T10:40:37.000Z", + "emojis": [], + "fields": [] + }, + "media_attachments": [ + { + "id": "01FVW7RXPQ8YJHTEXYPE7Q8ZY0", + "type": "image", + "url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg", + "text_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg", + "preview_url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg", + "remote_url": "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpg", + "preview_remote_url": "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg", + "meta": { + "original": { + "width": 472, + "height": 291, + "size": "472x291", + "aspect": 1.6219932 + }, + "small": { + "width": 472, + "height": 291, + "size": "472x291", + "aspect": 1.6219932 + }, + "focus": { + "x": 0, + "y": 0 + } + }, + "description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted", + "blurhash": "LARysgM_IU_3~pD%M_Rj_39FIAt6" + } + ], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null + } + ], + "rule_ids": [], + "action_taken_comment": null +}`, string(b)) +} + func TestInternalToFrontendTestSuite(t *testing.T) { suite.Run(t, new(InternalToFrontendTestSuite)) } |