// PUT http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM // {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"} // func (u *ProfileApi) update(r *restful.Request, w *restful.Response) { c := appengine.NewContext(r.Request) // Decode the request parameter to determine the key for the entity. k, err := datastore.DecodeKey(r.PathParameter("profile-id")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Marshall the entity from the request into a struct. p := new(Profile) err = r.ReadEntity(&p) if err != nil { w.WriteError(http.StatusNotAcceptable, err) return } // Retrieve the old entity from the datastore. old := Profile{} if err := datastore.Get(c, k, &old); err != nil { if err.Error() == "datastore: no such entity" { http.Error(w, err.Error(), http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // Check we own the profile before allowing them to update it. // Optionally, return a 404 instead to help prevent guessing ids. // TODO: Allow admins access. if old.Email != user.Current(c).String() { http.Error(w, "You do not have access to this resource", http.StatusForbidden) return } // Since the whole entity is re-written, we need to assign any invariant fields again // e.g. the owner of the entity. p.Email = user.Current(c).String() // Keep track of the last modification date. p.LastModified = time.Now() // Attempt to overwrite the old entity. _, err = datastore.Put(c, k, p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Let them know it succeeded. w.WriteHeader(http.StatusNoContent) }
func createProfile(res http.ResponseWriter, req *http.Request) { ctx := appengine.NewContext(req) u := user.Current(ctx) if req.Method == "POST" { username := req.FormValue("username") // TODO Confirm input is valid // TODO Make sure username is not taken key := datastore.NewKey(ctx, "profile", u.Email, 0, nil) p := profile{ Username: username, } _, err := datastore.Put(ctx, key, &p) if err != nil { http.Error(res, "Server error!", http.StatusInternalServerError) log.Errorf(ctx, "Create profile Error: %s\n", err.Error()) return } } err := tpl.ExecuteTemplate(res, "createProfile.gohtml", nil) if err != nil { http.Error(res, "Server error!", http.StatusInternalServerError) log.Errorf(ctx, "Template Execute Error: %s\n", err.Error()) return } }
// The same as putTodoHandler, but it expects there to be an "id" parameter. // It then writes a new record with that id, overwriting the old one. func updateTaskHandler(w http.ResponseWriter, r *http.Request) { // create AppEngine context ctx := appengine.NewContext(r) // get description from request description := r.FormValue("description") // get due date from request dueDate := r.FormValue("dueDate") d, err := time.Parse("2006-01-02", dueDate) // get item ID from request id := r.FormValue("id") itemID, err1 := strconv.ParseInt(id, 10, 64) // get user from logged-in user email := user.Current(ctx).Email if err != nil { http.Error(w, dueDate+" doesn't look like a valid date to me!", 400) } else if err1 != nil { http.Error(w, id+" doesn't look like an item ID to me!", 400) } else { state := r.FormValue("state") respondWith(w, *(updateTodoItem(ctx, email, description, d, state == "on", itemID))) rootHandler(w, r) } }
func home(res http.ResponseWriter, req *http.Request) { if req.URL.Path != "/" { profile(res, req) return } ctx := appengine.NewContext(req) u := user.Current(ctx) log.Infof(ctx, "user: "******"/login", 302) return } model.Profile = *profile } // TODO: get recent tweets var tweets []Tweet tweets, err := recentTweets(ctx) if err != nil { http.Error(res, err.Error(), 500) } model.Tweets = tweets renderTemplate(res, "home.html", model) }
func showUser(res http.ResponseWriter, req *http.Request) { ctx := appengine.NewContext(req) u := user.Current(ctx) email := u.Email key := datastore.NewKey(ctx, "User", email, 0, nil) var entity User err := datastore.Get(ctx, key, &entity) if err == datastore.ErrNoSuchEntity { http.NotFound(res, req) return } else if err != nil { http.Error(res, err.Error(), 500) return } log.Infof(ctx, "%v", entity) res.Header().Set("Content-Type", "text/html") fmt.Fprintln(res, ` <dl> <dt>`+entity.FirstName+`</dt> <dd>`+entity.LastName+`</dd> <dd>`+u.Email+`</dd> </dl> `) }
func registerHandler(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) log.Infof(c, "Register") u := user.Current(c) r.ParseForm() userKey := r.FormValue("UserKey") log.Infof(c, "UserKey:%s", userKey) if existUser(r, userKey) { log.Infof(c, "Exists") return } rtn := User{ UserKey: userKey, Size: 0, } _, err := datastore.Put(c, datastore.NewKey(c, "User", u.ID, 0, nil), &rtn) if err != nil { panic(err) } //Profile Page meRender(w, "./templates/me/profile.tmpl", rtn) }
func createProfile(res http.ResponseWriter, req *http.Request) { ctx := appengine.NewContext(req) if req.Method == "POST" { u := user.Current(ctx) profile := Profile{ Email: u.Email, FirstName: req.FormValue("firstname"), LastName: req.FormValue("lastname"), } key := datastore.NewKey(ctx, "Profile", u.Email, 0, nil) _, err := datastore.Put(ctx, key, &profile) if err != nil { http.Error(res, "Server Error", http.StatusInternalServerError) log.Errorf(ctx, err.Error()) return } http.Redirect(res, req, "/", http.StatusSeeOther) } f, err := os.Open("createProfile.gohtml") if err != nil { http.Error(res, "Server Error", http.StatusInternalServerError) log.Errorf(ctx, err.Error()) return } io.Copy(res, f) }
func emailMentions(ctx context.Context, tweet *Tweet) { u := user.Current(ctx) var words []string words = strings.Fields(tweet.Message) for _, value := range words { if strings.HasPrefix(value, "@") { username := value[1:] profile, err := getProfileByUsername(ctx, username) if err != nil { // they don't have a profile, so skip it continue } msg := &mail.Message{ Sender: u.Email, To: []string{profile.Username + " <" + profile.Email + ">"}, Subject: "You were mentioned in a tweet", Body: tweet.Message + " from " + tweet.Username + " - " + humanize.Time(tweet.Time), } if err := mail.Send(ctx, msg); err != nil { log.Errorf(ctx, "Alas, my user, the email failed to sendeth: %v", err) continue } } } }
// If the user logs in (and grants permission), they will be redirected here func loginHandler(w http.ResponseWriter, r *http.Request) { ctx := appengine.NewContext(r) u := user.Current(ctx) if u == nil { log.Errorf(ctx, "No identifiable Google user; is this browser in privacy mode ?") http.Error(w, "No identifiable Google user; is this browser in privacy mode ?", http.StatusInternalServerError) return } //c.Infof(" ** Google user logged in ! [%s]", u.Email) // Snag their email address forever more session, err := sessions.Get(r) if err != nil { // This isn't usually an important error (the session was most likely expired, which is why // we're logging in) - so log as Info, not Error. log.Infof(ctx, "sessions.Get [failing is OK for this call] had err: %v", err) } session.Values["email"] = u.Email session.Values["tstamp"] = time.Now().Format(time.RFC3339) if err := session.Save(r, w); err != nil { log.Errorf(ctx, "session.Save: %v", err) } // Now head back to the main page http.Redirect(w, r, "/", http.StatusFound) }
func putUser(r *http.Request) (*User, error) { c := appengine.NewContext(r) u := user.Current(c) r.ParseForm() size, err := strconv.ParseInt(r.FormValue("Size"), 10, 64) if err != nil { return nil, err } rtn := User{ UserKey: r.FormValue("UserKey"), Name: r.FormValue("Name"), Job: r.FormValue("Job"), Email: r.FormValue("Email"), Url: r.FormValue("Url"), TwitterId: r.FormValue("TwitterId"), LastWord: r.FormValue("LastWord"), Size: size, } _, err = datastore.Put(c, datastore.NewKey(c, "User", u.ID, 0, nil), &rtn) if err != nil { return nil, err } return &rtn, nil }
// GET http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM // func (u ProfileApi) read(r *restful.Request, w *restful.Response) { c := appengine.NewContext(r.Request) // Decode the request parameter to determine the key for the entity. k, err := datastore.DecodeKey(r.PathParameter("profile-id")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Retrieve the entity from the datastore. p := Profile{} if err := datastore.Get(c, k, &p); err != nil { if err.Error() == "datastore: no such entity" { http.Error(w, err.Error(), http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // Check we own the profile before allowing them to view it. // Optionally, return a 404 instead to help prevent guessing ids. // TODO: Allow admins access. if p.Email != user.Current(c).String() { http.Error(w, "You do not have access to this resource", http.StatusForbidden) return } w.WriteEntity(p) }
// POST http://localhost:8080/profiles // {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"} // func (u *ProfileApi) insert(r *restful.Request, w *restful.Response) { c := appengine.NewContext(r.Request) // Marshall the entity from the request into a struct. p := new(Profile) err := r.ReadEntity(&p) if err != nil { w.WriteError(http.StatusNotAcceptable, err) return } // Ensure we start with a sensible value for this field. p.LastModified = time.Now() // The profile belongs to this user. p.Email = user.Current(c).String() k, err := datastore.Put(c, datastore.NewIncompleteKey(c, "profiles", nil), p) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Let them know the location of the newly created resource. // TODO: Use a safe Url path append function. w.AddHeader("Location", u.Path+"/"+k.Encode()) // Return the resultant entity. w.WriteHeader(http.StatusCreated) w.WriteEntity(p) }
func APIKeyAddHandler(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) u := user.Current(c) if u == nil { loginUrl, _ := user.LoginURL(c, r.URL.RequestURI()) http.Redirect(w, r, loginUrl, http.StatusFound) return } else { if !u.Admin { w.WriteHeader(http.StatusForbidden) w.Write([]byte("You're not an admin. Go away.")) } else { key := randomString(26) owner := r.FormValue("owner") if owner == "" { w.Write([]byte("You forgot a parameter.")) } else { apiKey := APIKey{ APIKey: key, OwnerEmail: owner, } dkey := datastore.NewIncompleteKey(c, "APIKey", nil) _, err := datastore.Put(c, dkey, &apiKey) if err != nil { w.Write([]byte(fmt.Sprintf("error! %s", err.Error()))) } else { w.Write([]byte(key)) } } } } }
func createProfile(res http.ResponseWriter, req *http.Request) { ctx := appengine.NewContext(req) u := user.Current(ctx) if req.Method == "POST" { username := req.FormValue("username") // check if name is taken if !confirmCreateProfile(username) { http.Error(res, "Invalid input!", http.StatusBadRequest) log.Warningf(ctx, "Invalid profile information from %s\n", req.RemoteAddr) return } key := datastore.NewKey(ctx, "profile", u.Email, 0, nil) p := profile{ Username: username, Email: u.Email, } _, err := datastore.Put(ctx, key, &p) if err != nil { http.Error(res, "server error!", http.StatusInternalServerError) log.Errorf(ctx, "Create profile Error: &s\n", err.Error()) return } } err := tpl.ExecuteTemplate(res, "createProfile.gohtml", nil) if err != nil { http.Error(res, "Serever error!", http.StatusInternalServerError) log.Errorf(ctx, "Template Parse Error: %s\n", err.Error()) return } }
// If the user is not logged in, then return the login url. Otherwise return a json // structure containing the user's name and email address, and which team they are on. func Api1UserProfileHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") data := UserProfileData{} ctx := appengine.NewContext(r) u := user.Current(ctx) if u == nil { url, _ := user.LoginURL(ctx, "/") data.LoginUrl = url datajson, err := json.Marshal(data) if err != nil { http.Error(w, "Internal Service Error", http.StatusInternalServerError) return } fmt.Fprintf(w, "%s", datajson) return } url, _ := user.LogoutURL(ctx, "/") data.LogoutUrl = url data.Email = u.Email data.IsAdmin = u.Admin data.IsLoggedIn = true datajson, err := json.Marshal(data) if err != nil { http.Error(w, "Internal Service Error", http.StatusInternalServerError) return } fmt.Fprintf(w, "%s", datajson) }
func handleIndex(res http.ResponseWriter, req *http.Request) { // for anything but "/" treat it like a user profile if req.URL.Path != "/" { handleUserProfile(res, req) return } ctx := appengine.NewContext(req) u := user.Current(ctx) // get recent tweets var tweets []*Tweet var err error if u == nil { tweets, err = getTweets(ctx) } else { tweets, err = getHomeTweets(ctx, u.Email) } if err != nil { http.Error(res, err.Error(), 500) return } type Model struct { Tweets []*Tweet } model := Model{ Tweets: tweets, } renderTemplate(res, req, "index", model) }
func registerFeed(w http.ResponseWriter, r *http.Request) *appError { s := strings.TrimSpace(r.FormValue("url")) if s == "" { return &appError{ Error: errInvalidRequest, Message: "url is required", Code: http.StatusBadRequest, } } u, err := url.Parse(s) if err != nil { return &appError{ Error: err, Message: "can't parse url", Code: http.StatusBadRequest, } } if !u.IsAbs() { return &appError{ Error: errInvalidRequest, Message: "require an absolute url", Code: http.StatusBadRequest, } } c := appengine.NewContext(r) client := urlfetch.Client(c) f, err := fetchFeed(client, s) if err != nil { return &appError{ Error: err, Message: "failed to get", Code: http.StatusInternalServerError, } } m := Magazine{ Title: f.Title, URL: s, Creation: time.Now(), LastMod: time.Now(), } m.Init(c) usr := user.Current(c) if usr == nil { return &appError{ Error: errUserRejected, Message: "failed to initialize user environment", Code: http.StatusUnauthorized, } } err = RegisterMagazine(c, usr, &m, f) if err != nil { return &appError{ Error: err, Message: "failed to register", Code: http.StatusInternalServerError, } } http.Redirect(w, r, "/", http.StatusFound) return nil }
func handleSign(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "POST requests only", http.StatusMethodNotAllowed) return } c := appengine.NewContext(r) if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } g := &Greeting{ Content: r.FormValue("content"), Date: time.Now(), } if u := user.Current(c); u != nil { g.Author = u.String() } key := datastore.NewIncompleteKey(c, "Greeting", guestbookKey(c)) if _, err := datastore.Put(c, key, g); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Redirect with 303 which causes the subsequent request to use GET. http.Redirect(w, r, "/", http.StatusSeeOther) }
func handle(res http.ResponseWriter, req *http.Request) { if req.URL.Path != "/" { http.NotFound(res, req) return } ctx := appengine.NewContext(req) u := user.Current(ctx) key := datastore.NewKey(ctx, "Profile", u.Email, 0, nil) var profile Profile err := datastore.Get(ctx, key, &profile) if err == datastore.ErrNoSuchEntity { http.Redirect(res, req, "/createProfile", http.StatusSeeOther) return } else if err != nil { http.Error(res, "Server Error", http.StatusInternalServerError) log.Errorf(ctx, err.Error()) return } tpl, err := template.ParseFiles("viewProfile.gohtml") if err != nil { http.Error(res, "Server Error", http.StatusInternalServerError) log.Errorf(ctx, err.Error()) return } err = tpl.Execute(res, &profile) if err != nil { http.Error(res, "Server Error", http.StatusInternalServerError) log.Errorf(ctx, err.Error()) return } }
func editpost(rw http.ResponseWriter, req *http.Request) { c := appengine.NewContext(req) s := req.FormValue("encoded_key") k, err := datastore.DecodeKey(s) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } mypost := Post{} err = datastore.Get(c, k, &mypost) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if mypost.Author == user.Current(c).String() { message := mypost.Message title := "Edit a Post" t := template.Must(template.ParseFiles("assets/edit.html")) t.ExecuteTemplate(rw, "New", struct { Title string Message string ID string Parent string }{Title: title, Message: message, ID: s}) } else { http.Redirect(rw, req, "/", http.StatusOK) } }
func showList(res http.ResponseWriter, req *http.Request, params httprouter.Params) { tpl, err := template.ParseFiles("assets/templates/templates.gohtml") if err != nil { panic(err) } ctx := appengine.NewContext(req) u := user.Current(ctx) qp := datastore.NewQuery("ToDo") ToDos := []ToDo{} keys, error := qp.GetAll(ctx, &ToDos) for _, value := range ToDos { } if error != nil { log.Infof(ctx, "%v", error.Error()) } json.NewEncoder(res).Encode(ToDos) err = tpl.ExecuteTemplate(res, "todo-list", ToDos) if err != nil { http.Error(res, err.Error(), 500) } }
func board(rw http.ResponseWriter, req *http.Request) { // rw.Header().Set("Content-type", "text/html") c := appengine.NewContext(req) u := user.Current(c) // rw.Header().Set("Location", req.URL.String()) // rw.WriteHeader(http.StatusFound) posts := []Post{} q := datastore.NewQuery("post").Filter("OP =", true).Order("-PostDate") k, err := q.GetAll(c, &posts) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } threads := []Thread{} for i, value := range posts { threads = append(threads, Thread{value, k[i].Encode()}) } t := template.Must(template.ParseFiles("assets/board.html")) t.ExecuteTemplate(rw, "Board", struct { User string Thread []Thread }{ u.String(), threads, }) }
func loginWithGoogle() gin.HandlerFunc { return func(c *gin.Context) { ctx := appengine.NewContext(c.Request) u := user.Current(ctx) if u == nil { url, _ := user.LoginURL(ctx, c.Request.URL.String()) c.HTML(302, "login.tmpl", gin.H{ "url": url, }) c.Abort() return } email := strings.Split(u.Email, "@") if email[1] == "elo7.com" && len(email) == 2 { developer := models.Developer{Email: u.Email} developer.Create(&db) log.Infof(ctx, developer.Email) } else { url, _ := user.LogoutURL(ctx, "/") c.Redirect(http.StatusTemporaryRedirect, url) } c.Next() } }
func followHandler(res http.ResponseWriter, req *http.Request) { ctx := appengine.NewContext(req) u := user.Current(ctx) switch req.Method { case "POST": var userName string err := json.NewDecoder(req.Body).Decode(&userName) if err != nil { http.Error(res, err.Error(), 500) return } err = follow(ctx, u.Email, userName) if err != nil { http.Error(res, err.Error(), 500) return } json.NewEncoder(res).Encode(true) case "GET": profile, err := getProfile(ctx, u.Email) if err != nil { http.Error(res, err.Error(), 500) return } json.NewEncoder(res).Encode(profile.Following) case "DELETE": // delete a follower default: http.Error(res, "method not allowed", 405) } }
func index(res http.ResponseWriter, req *http.Request) { ctx := appengine.NewContext(req) // user.Current gives data about what the requester is // logged in as, or nil if they are not logged in. u := user.Current(ctx) var model indexModel // If they are not nil, they are logged in. if u != nil { // So let the template know, and get the logout url. model.Login = true logoutURL, err := user.LogoutURL(ctx, "/") if err != nil { log.Errorf(ctx, err.Error()) http.Error(res, "Server Error", http.StatusInternalServerError) return } model.LogoutURL = logoutURL } else { // Otherwise, get the login url. loginURL, err := user.LoginURL(ctx, "/") if err != nil { log.Errorf(ctx, err.Error()) http.Error(res, "Server Error", http.StatusInternalServerError) return } model.LoginURL = loginURL } tpl.ExecuteTemplate(res, "index", model) }
func appstatsHandler(w http.ResponseWriter, r *http.Request) { ctx := storeContext(appengine.NewContext(r)) if appengine.IsDevAppServer() { // noop } else if u := user.Current(ctx); u == nil { if loginURL, err := user.LoginURL(ctx, r.URL.String()); err == nil { http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) } else { serveError(w, err) } return } else if !u.Admin { http.Error(w, "Forbidden", http.StatusForbidden) return } if detailsURL == r.URL.Path { details(ctx, w, r) } else if fileURL == r.URL.Path { file(ctx, w, r) } else if strings.HasPrefix(r.URL.Path, staticURL) { name := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] content, ok := static[name] if !ok { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } http.ServeContent(w, r, name, initTime, content) } else { index(ctx, w, r) } }
func rootHandler(w http.ResponseWriter, r *http.Request) { ctx := appengine.NewContext(r) u := user.Current(ctx) if u == nil { url, _ := user.LoginURL(ctx, "/") fmt.Fprintf(w, `<a href="%s">Sign in or register</a>`, url) return } fmt.Fprint(w, `<html><h1>Hi! Welcome to Tada</h1>`) fmt.Fprint(w, "<!-- About to call writeItems -->") fmt.Fprint(w, `<ol>`) writeItems(w, r, u) fmt.Fprint(w, `</ol>`) fmt.Fprint(w, "<!-- Called writeItems -->") url, _ := user.LogoutURL(ctx, "/") fmt.Fprintf(w, `Welcome, %s! (<a href="%s">sign out</a>)`, u, url) fmt.Fprint(w, `</html>`) makeNewItemForm(w) }
func isBanned(r *http.Request) bool { ctx := req2ctx(r) cdb := complaintdb.NewDB(ctx) u := user.Current(cdb.Ctx()) userWhitelist := map[string]int{ "*****@*****.**": 1, "*****@*****.**": 1, "*****@*****.**": 1, "*****@*****.**": 1, } reqBytes, _ := httputil.DumpRequest(r, true) cdb.Infof("remoteAddr: '%v'", r.RemoteAddr) cdb.Infof("user: '******' (%s)", u, u.Email) cdb.Infof("inbound IP determined as: '%v'", getIP(r)) cdb.Infof("HTTP req:-\n%s", string(reqBytes)) if strings.HasPrefix(r.UserAgent(), "python") { cdb.Infof("User-Agent rejected") return true } if _, exists := userWhitelist[u.Email]; !exists { cdb.Infof("user not found in whitelist") return true } return false }
func deleteData(res http.ResponseWriter, req *http.Request) { ctx := appengine.NewContext(req) u := user.Current(ctx) keyVal := req.FormValue("keyVal") key, err := datastore.DecodeKey(keyVal) if err != nil { http.Error(res, "Invalid data", http.StatusBadRequest) log.Warningf(ctx, err.Error()) return } var l list err = datastore.Get(ctx, key, &l) if err != nil { http.Error(res, "Invalid data", http.StatusBadRequest) log.Warningf(ctx, err.Error()) return } if l.Owner != u.Email { http.Error(res, "Not authorized to delete this entry", http.StatusUnauthorized) log.Warningf(ctx, err.Error()) return } err = datastore.Delete(ctx, key) if err != nil { http.Error(res, "Server Error", http.StatusInternalServerError) log.Errorf(ctx, err.Error()) return } }
func serveHome(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) u := user.Current(c) su, err := getUser(c, u) if err != nil { w.WriteHeader(400) fmt.Fprintf(w, "You are not permitted, %v", u) return } log.Infof(c, "Got a request from %v", u) projected, earned, err := projections(c, su, 90) if err != nil { log.Errorf(c, "Error finding projections: %v", err) } execTemplate(c, w, "index.html", map[string]interface{}{ "user": u, "tasks": iterateUserTasks(c, su, false), "projected": int(projected), "earned": int(earned), "missed": int(projected - earned), }) }