github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/feedback/reporter.go (about) 1 /* 2 * Copyright (C) 2019 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package feedback 19 20 import ( 21 "fmt" 22 "net/mail" 23 "strings" 24 25 "github.com/mysteriumnetwork/feedback/client" 26 "github.com/mysteriumnetwork/feedback/feedback" 27 "github.com/mysteriumnetwork/go-rest/apierror" 28 "github.com/mysteriumnetwork/node/core/location" 29 "github.com/mysteriumnetwork/node/identity" 30 "github.com/rs/zerolog/log" 31 ) 32 33 // Reporter reports issues from users 34 type Reporter struct { 35 logCollector logCollector 36 identityProvider identityProvider 37 feedbackAPI *client.FeedbackAPI 38 originResolver location.OriginResolver 39 } 40 41 // NewReporter constructs a new Reporter 42 func NewReporter( 43 logCollector logCollector, 44 identityProvider identityProvider, 45 originResolver location.OriginResolver, 46 feedbackURL string, 47 ) (*Reporter, error) { 48 log.Info().Msg("Using feedback API at: " + feedbackURL) 49 api, err := client.NewFeedbackAPI(feedbackURL) 50 if err != nil { 51 return nil, err 52 } 53 return &Reporter{ 54 logCollector: logCollector, 55 identityProvider: identityProvider, 56 originResolver: originResolver, 57 feedbackAPI: api, 58 }, nil 59 } 60 61 type logCollector interface { 62 Archive() (filepath string, err error) 63 } 64 65 type identityProvider interface { 66 GetIdentities() []identity.Identity 67 } 68 69 // BugReport represents user input when submitting an issue report 70 // swagger:model BugReport 71 type BugReport struct { 72 Email string `json:"email"` 73 Description string `json:"description"` 74 } 75 76 // Validate validates a bug report 77 func (br *BugReport) Validate() *apierror.APIError { 78 v := apierror.NewValidator() 79 br.Email = strings.TrimSpace(br.Email) 80 if br.Email == "" { 81 v.Required("email") 82 } else if _, err := mail.ParseAddress(br.Email); err != nil { 83 v.Invalid("email", "Invalid email address") 84 } 85 86 br.Description = strings.TrimSpace(br.Description) 87 if len(br.Description) < 30 { 88 v.Invalid("description", "Description too short. Provide at least 30 character long description.") 89 } 90 91 return v.Err() 92 } 93 94 // NewIssue sends node logs, Identity and UserReport to the feedback service 95 func (r *Reporter) NewIssue(report BugReport) (*feedback.CreateGithubIssueResponse, *apierror.APIError, error) { 96 if apiErr := report.Validate(); apiErr != nil { 97 return nil, apiErr, fmt.Errorf("invalid report: %w", apiErr) 98 } 99 100 userID := r.currentIdentity() 101 102 archiveFilepath, err := r.logCollector.Archive() 103 if err != nil { 104 return nil, apierror.Internal("could not create log archive", "cannot_get_logs"), fmt.Errorf("could not create log archive: %w", err) 105 } 106 107 result, apierr, err := r.feedbackAPI.CreateGithubIssue(feedback.CreateGithubIssueRequest{ 108 UserId: userID, 109 Description: report.Description, 110 Email: report.Email, 111 }, archiveFilepath) 112 if err != nil { 113 return nil, nil, fmt.Errorf("could not create github issue: %w", err) 114 } 115 116 return result, apierr, nil 117 } 118 119 // UserReport represents user input when submitting an issue report 120 // swagger:model UserReport 121 type UserReport struct { 122 BugReport 123 UserId string `json:"user_id"` 124 UserType string `json:"user_type"` 125 } 126 127 // Validate validate UserReport 128 func (ur *UserReport) Validate() *apierror.APIError { 129 return ur.BugReport.Validate() 130 } 131 132 // NewIntercomIssue sends node logs, Identity and UserReport to intercom 133 func (r *Reporter) NewIntercomIssue(report UserReport) (*feedback.CreateIntercomIssueResponse, *apierror.APIError, error) { 134 if apiErr := report.Validate(); apiErr != nil { 135 return nil, apiErr, fmt.Errorf("invalid report: %w", apiErr) 136 } 137 138 nodeID := r.currentIdentity() 139 location := r.originResolver.GetOrigin() 140 141 archiveFilepath, err := r.logCollector.Archive() 142 if err != nil { 143 return nil, apierror.Internal("could not create log archive", "cannot_get_logs"), fmt.Errorf("could not create log archive: %w", err) 144 } 145 146 result, apierr, err := r.feedbackAPI.CreateIntercomIssue(feedback.CreateIntercomIssueRequest{ 147 UserId: report.UserId, 148 Description: report.Description, 149 Email: report.Email, 150 NodeIdentity: nodeID, 151 NodeCountry: location.Country, 152 IpType: location.IPType, 153 Ip: location.IP, 154 UserType: report.UserType, 155 }, archiveFilepath) 156 if err != nil { 157 return nil, nil, fmt.Errorf("could not create intercom issue: %w", err) 158 } 159 160 return result, apierr, nil 161 } 162 163 // CreateBugReportResponse response for bug report creation 164 // swagger:model CreateBugReportResponse 165 type CreateBugReportResponse struct { 166 Message string `json:"message"` 167 Email string `json:"email"` 168 Identity string `json:"identity"` 169 NodeCountry string `json:"node_country"` 170 IpType string `json:"ip_type"` 171 Ip string `json:"ip"` 172 } 173 174 // NewBugReport creates a new bug report and returns the message that can be sent to intercom 175 func (r *Reporter) NewBugReport(report BugReport) (*CreateBugReportResponse, *apierror.APIError, error) { 176 if apiErr := report.Validate(); apiErr != nil { 177 return nil, apiErr, fmt.Errorf("invalid report: %w", apiErr) 178 } 179 180 nodeID := r.currentIdentity() 181 location := r.originResolver.GetOrigin() 182 183 archiveFilepath, err := r.logCollector.Archive() 184 if err != nil { 185 return nil, apierror.Internal("could not create log archive", "cannot_get_logs"), fmt.Errorf("could not create log archive: %w", err) 186 } 187 188 result, apierr, err := r.feedbackAPI.CreateBugReport(feedback.CreateBugReportRequest{ 189 NodeIdentity: nodeID, 190 Description: report.Description, 191 Email: report.Email, 192 }, archiveFilepath) 193 if err != nil { 194 return nil, nil, fmt.Errorf("could not create intercom issue: %w", err) 195 } else if apierr != nil { 196 return nil, apierr, apierr 197 } 198 199 return &CreateBugReportResponse{ 200 Message: result.Message, 201 Email: result.Email, 202 Identity: result.NodeIdentity, 203 NodeCountry: location.Country, 204 IpType: location.IPType, 205 Ip: location.IP, 206 }, nil, nil 207 } 208 209 func (r *Reporter) currentIdentity() (identity string) { 210 identities := r.identityProvider.GetIdentities() 211 if len(identities) > 0 { 212 return identities[0].Address 213 } 214 return "unknown_identity" 215 }