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  }