github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/profile.go (about)

     1  package lib
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  
     9  	"github.com/qri-io/qfs"
    10  	"github.com/qri-io/qri/config"
    11  	qhttp "github.com/qri-io/qri/lib/http"
    12  	"github.com/qri-io/qri/profile"
    13  	"github.com/qri-io/qri/registry"
    14  )
    15  
    16  // ProfileMethods encapsulates business logic for this node's
    17  // user profile
    18  // TODO (b5) - alterations to user profile are a subset of configuration
    19  // changes. all of this code should be refactored into subroutines of general
    20  // configuration getters & setters
    21  type ProfileMethods struct {
    22  	d dispatcher
    23  }
    24  
    25  // Name returns the name of this method group
    26  func (m ProfileMethods) Name() string {
    27  	return "profile"
    28  }
    29  
    30  // Attributes defines attributes for each method
    31  func (m ProfileMethods) Attributes() map[string]AttributeSet {
    32  	return map[string]AttributeSet{
    33  		"getprofile":      {Endpoint: qhttp.AEGetProfile, HTTPVerb: "POST", DenyRPC: true},
    34  		"setprofile":      {Endpoint: qhttp.AESetProfile, HTTPVerb: "POST", DenyRPC: true},
    35  		"setprofilephoto": {Endpoint: qhttp.AESetProfilePhoto, HTTPVerb: "POST", DenyRPC: true},
    36  		"setposterphoto":  {Endpoint: qhttp.AESetPosterPhoto, HTTPVerb: "POST", DenyRPC: true},
    37  	}
    38  }
    39  
    40  // ProfileParams define parameters for getting a profile
    41  type ProfileParams struct{}
    42  
    43  // GetProfile get's this node's peer profile
    44  func (m ProfileMethods) GetProfile(ctx context.Context, p *ProfileParams) (*config.ProfilePod, error) {
    45  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "getprofile"), p)
    46  	if res, ok := got.(*config.ProfilePod); ok {
    47  		return res, err
    48  	}
    49  	return nil, dispatchReturnError(got, err)
    50  }
    51  
    52  // SetProfileParams defines parameters for setting parts of a profile
    53  // Cannot use this to set private keys, your peername, or peer id
    54  type SetProfileParams struct {
    55  	Pro *config.ProfilePod `json:"pro"`
    56  }
    57  
    58  // SetProfile stores changes to the active peer's editable profile
    59  func (m ProfileMethods) SetProfile(ctx context.Context, p *SetProfileParams) (*config.ProfilePod, error) {
    60  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "setprofile"), p)
    61  	if res, ok := got.(*config.ProfilePod); ok {
    62  		return res, err
    63  	}
    64  	return nil, dispatchReturnError(got, err)
    65  }
    66  
    67  // FileParams defines parameters for Files as arguments to lib methods
    68  // either `Filename` or `Data` is required. If both fields are set, the content in the `Data` field is favored
    69  type FileParams struct {
    70  	// url to download data from. either Url or Data is required
    71  	// Url      string
    72  	// Filename of data file. extension is used for filetype detection
    73  	Filename string `json:"filename" qri:"fspath"`
    74  	// Data is the file as slice of bytes
    75  	Data []byte `json:"data"`
    76  }
    77  
    78  // SetProfilePhoto changes the active peer's profile image
    79  func (m ProfileMethods) SetProfilePhoto(ctx context.Context, p *FileParams) (*config.ProfilePod, error) {
    80  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "setprofilephoto"), p)
    81  	if res, ok := got.(*config.ProfilePod); ok {
    82  		return res, err
    83  	}
    84  	return nil, dispatchReturnError(got, err)
    85  }
    86  
    87  // SetPosterPhoto changes this active peer's poster image
    88  func (m ProfileMethods) SetPosterPhoto(ctx context.Context, p *FileParams) (*config.ProfilePod, error) {
    89  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "setposterphoto"), p)
    90  	if res, ok := got.(*config.ProfilePod); ok {
    91  		return res, err
    92  	}
    93  	return nil, dispatchReturnError(got, err)
    94  }
    95  
    96  // profileImpl holds the method implementations for ProfileMethods
    97  type profileImpl struct{}
    98  
    99  // GetProfile get's this node's peer profile
   100  func (profileImpl) GetProfile(scope scope, p *ProfileParams) (*config.ProfilePod, error) {
   101  	pro := scope.ActiveProfile()
   102  	cfg := scope.Config()
   103  	// TODO (b5) - this isn't the right way to check if you're online
   104  	if cfg != nil && cfg.P2P != nil {
   105  		pro.Online = cfg.P2P.Enabled
   106  	}
   107  
   108  	enc, err := pro.Encode()
   109  	if err != nil {
   110  		log.Debug(err.Error())
   111  		return nil, err
   112  	}
   113  
   114  	enc.PrivKey = ""
   115  	return enc, nil
   116  }
   117  
   118  // SetProfile stores changes to the active peer's editable profile
   119  func (profileImpl) SetProfile(scope scope, p *SetProfileParams) (*config.ProfilePod, error) {
   120  	if p.Pro == nil {
   121  		return nil, fmt.Errorf("profile required for update")
   122  	}
   123  
   124  	pro := p.Pro
   125  	cfg := scope.Config()
   126  	r := scope.Repo()
   127  
   128  	cfg.Set("profile.name", pro.Name)
   129  	cfg.Set("profile.email", pro.Email)
   130  	cfg.Set("profile.description", pro.Description)
   131  	cfg.Set("profile.homeurl", pro.HomeURL)
   132  	cfg.Set("profile.twitter", pro.Twitter)
   133  
   134  	if pro.Color != "" {
   135  		cfg.Set("profile.color", pro.Color)
   136  	}
   137  	// TODO (b5) - strange bug:
   138  	if cfg.Profile.Type == "" {
   139  		cfg.Profile.Type = "peer"
   140  	}
   141  
   142  	prevPeername := cfg.Profile.Peername
   143  	if pro.Peername != "" && pro.Peername != cfg.Profile.Peername {
   144  		cfg.Set("profile.peername", pro.Peername)
   145  	}
   146  
   147  	if err := cfg.Profile.Validate(); err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	if pro.Peername != "" && pro.Peername != prevPeername {
   152  		if reg := scope.RegistryClient(); reg != nil {
   153  			current, err := profile.NewProfile(cfg.Profile)
   154  			if err != nil {
   155  				return nil, err
   156  			}
   157  
   158  			if _, err := reg.PutProfile(&registry.Profile{Username: pro.Peername}, current.PrivKey); err != nil {
   159  				return nil, err
   160  			}
   161  		}
   162  	}
   163  
   164  	enc, err := profile.NewProfile(cfg.Profile)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	if err := r.Profiles().SetOwner(scope.Context(), enc); err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	res := &config.ProfilePod{}
   173  	// Copy the global config, except without the private key.
   174  	*res = *cfg.Profile
   175  	res.PrivKey = ""
   176  
   177  	// TODO (b5) - we should have a betteer way of determining onlineness
   178  	if cfg.P2P != nil {
   179  		res.Online = cfg.P2P.Enabled
   180  	}
   181  
   182  	if err := scope.ChangeConfig(cfg); err != nil {
   183  		return nil, err
   184  	}
   185  	return res, nil
   186  }
   187  
   188  // SetProfilePhoto changes the active peer's profile image
   189  func (profileImpl) SetProfilePhoto(scope scope, p *FileParams) (*config.ProfilePod, error) {
   190  	if err := loadAndValidateJPEG(p, 256000); err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	// TODO - if file extension is .jpg / .jpeg ipfs does weird shit that makes this not work
   195  	path, err := scope.Filesystem().DefaultWriteFS().Put(scope.Context(), qfs.NewMemfileBytes("plz_just_encode", p.Data))
   196  	if err != nil {
   197  		log.Debug(err.Error())
   198  		return nil, fmt.Errorf("error saving photo: %s", err.Error())
   199  	}
   200  
   201  	cfg := scope.Config().Copy()
   202  	cfg.Set("profile.photo", path)
   203  	// TODO - resize photo for thumb
   204  	cfg.Set("profile.thumb", path)
   205  	if err := scope.ChangeConfig(cfg); err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	pro := scope.ActiveProfile()
   210  	pro.Photo = path
   211  	pro.Thumb = path
   212  
   213  	if err := scope.Profiles().SetOwner(scope.Context(), pro); err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	pp, err := pro.Encode()
   218  	if err != nil {
   219  		return nil, fmt.Errorf("error encoding new profile: %s", err)
   220  	}
   221  
   222  	return pp, nil
   223  }
   224  
   225  // SetPosterPhoto changes the active peer's poster image
   226  func (profileImpl) SetPosterPhoto(scope scope, p *FileParams) (*config.ProfilePod, error) {
   227  	if err := loadAndValidateJPEG(p, 2<<20); err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	// TODO - if file extension is .jpg / .jpeg ipfs does weird shit that makes this not work
   232  	path, err := scope.Filesystem().DefaultWriteFS().Put(scope.Context(), qfs.NewMemfileBytes("plz_just_encode", p.Data))
   233  	if err != nil {
   234  		log.Debug(err.Error())
   235  		return nil, fmt.Errorf("error saving photo: %s", err.Error())
   236  	}
   237  
   238  	cfg := scope.Config().Copy()
   239  	cfg.Set("profile.poster", path)
   240  	if err := scope.ChangeConfig(cfg); err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	pro := scope.ActiveProfile()
   245  	pro.Poster = path
   246  	if err := scope.Profiles().SetOwner(scope.Context(), pro); err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	pp, err := pro.Encode()
   251  	if err != nil {
   252  		return nil, fmt.Errorf("error encoding new profile: %s", err)
   253  	}
   254  
   255  	return pp, nil
   256  }
   257  
   258  func loadAndValidateJPEG(p *FileParams, maxBytes int) (err error) {
   259  	if p.Filename == "" && (p.Data == nil || len(p.Data) == 0) {
   260  		return fmt.Errorf("filename or data required")
   261  	}
   262  	if p.Data == nil || len(p.Data) == 0 {
   263  		if p.Data, err = ioutil.ReadFile(p.Filename); err != nil {
   264  			return fmt.Errorf("error opening file: %w", err)
   265  		}
   266  	}
   267  
   268  	if len(p.Data) > maxBytes {
   269  		return fmt.Errorf("file size too large. max size is %s", byteCount(int64(maxBytes)))
   270  
   271  	} else if len(p.Data) == 0 {
   272  		return fmt.Errorf("file is empty")
   273  	}
   274  
   275  	mimetype := http.DetectContentType(p.Data)
   276  	if mimetype != "image/jpeg" {
   277  		return fmt.Errorf("invalid file format. only .jpg images allowed")
   278  	}
   279  	return nil
   280  }
   281  
   282  // provides human readable byte count
   283  func byteCount(b int64) string {
   284  	const unit = 1024
   285  	if b < unit {
   286  		return fmt.Sprintf("%dB", b)
   287  	}
   288  	div, exp := int64(unit), 0
   289  	for n := b / unit; n >= unit; n /= unit {
   290  		div *= unit
   291  		exp++
   292  	}
   293  	return fmt.Sprintf("%.1f %ciB",
   294  		float64(b)/float64(div), "KMGTPE"[exp])
   295  }