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(®istry.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 }