package arn import ( "errors" "fmt" "reflect" "strings" "github.com/animenotifier/notify.moe/arn/validate" "github.com/aerogo/aero" "github.com/aerogo/api" "github.com/aerogo/http/client" "github.com/akyoto/color" "github.com/animenotifier/notify.moe/arn/autocorrect" ) // Force interface implementations var ( _ PostParent = (*User)(nil) _ api.Editable = (*User)(nil) ) // Authorize returns an error if the given API POST request is not authorized. func (user *User) Authorize(ctx aero.Context, action string) error { editor := GetUserFromContext(ctx) if editor == nil { return errors.New("Not authorized") } if editor.ID != ctx.Get("id") && editor.Role != "admin" { return errors.New("Can not modify data from other users") } return nil } // Edit updates the user object. func (user *User) Edit(ctx aero.Context, key string, value reflect.Value, newValue reflect.Value) (bool, error) { switch key { case "Nick": newNick := newValue.String() err := user.SetNick(newNick) return true, err case "Email": newEmail := newValue.String() err := user.SetEmail(newEmail) return true, err case "Gender": newGender := newValue.String() if newGender != "male" && newGender != "female" { return true, errors.New("Invalid gender") } user.Gender = newGender return true, nil case "Website": newSite := newValue.String() if newSite == "" { user.Website = newSite return true, nil } if autocorrect.IsTrackerLink(newSite) { return true, errors.New("Not an actual personal website or homepage") } newSite = autocorrect.Website(newSite) if !validate.URI("https://" + newSite) { return true, errors.New("Not a valid website link") } response, err := client.Get("https://" + newSite).End() if err != nil || response.StatusCode() >= 400 { return true, fmt.Errorf("https://%s seems to be inaccessible", newSite) } user.Website = newSite return true, nil case "BirthDay": newBirthDay := newValue.String() if AgeInYears(newBirthDay) <= 0 { return true, errors.New("Invalid birthday (make sure to use YYYY-MM-DD format, e.g. 2000-01-17)") } user.BirthDay = newBirthDay return true, nil case "ProExpires", "Avatar.Extension", "Avatar.LastModified": editor := GetUserFromContext(ctx) if editor == nil || editor.Role != "admin" { return true, errors.New("Not authorized to edit") } case "Accounts.Discord.Nick": newNick := newValue.String() if newNick == "" { value.SetString(newNick) user.Accounts.Discord.Verified = false return true, nil } if !validate.DiscordNick(newNick) { return true, errors.New("Discord username must include your name and the 4-digit Discord tag (e.g. Yandere#1234)") } // Trim spaces parts := strings.Split(newNick, "#") parts[0] = strings.TrimSpace(parts[0]) parts[1] = strings.TrimSpace(parts[1]) newNick = strings.Join(parts, "#") if value.String() != newNick { value.SetString(newNick) user.Accounts.Discord.Verified = false } return true, nil case "Accounts.Overwatch.BattleTag": newBattleTag := newValue.String() value.SetString(newBattleTag) if newBattleTag == "" { user.Accounts.Overwatch.SkillRating = 0 user.Accounts.Overwatch.Tier = "" } else { // Refresh Overwatch info if the battletag changed go func() { err := user.RefreshOverwatchInfo() if err != nil { color.Red("Error refreshing Overwatch info of user '%s' with Overwatch battle tag '%s': %v", user.Nick, newBattleTag, err) return } color.Green("Refreshed Overwatch info of user '%s' with Overwatch battle tag '%s': %v", user.Nick, newBattleTag, user.Accounts.Overwatch.SkillRating) user.Save() }() } return true, nil case "Accounts.FinalFantasyXIV.Nick", "Accounts.FinalFantasyXIV.Server": newValue := newValue.String() value.SetString(newValue) if newValue == "" { user.Accounts.FinalFantasyXIV.Class = "" user.Accounts.FinalFantasyXIV.Level = 0 user.Accounts.FinalFantasyXIV.ItemLevel = 0 } else if user.Accounts.FinalFantasyXIV.Nick != "" && user.Accounts.FinalFantasyXIV.Server != "" { // Refresh FinalFantasyXIV info if the name or server changed go func() { err := user.RefreshFFXIVInfo() if err != nil { color.Red("Error refreshing FinalFantasy XIV info of user '%s' with nick '%s' on server '%s': %v", user.Nick, user.Accounts.FinalFantasyXIV.Nick, user.Accounts.FinalFantasyXIV.Server, err) return } user.Save() }() } return true, nil } // Automatically correct account nicks if strings.HasPrefix(key, "Accounts.") && strings.HasSuffix(key, ".Nick") { newNick := newValue.String() newNick = autocorrect.AccountNick(newNick) value.SetString(newNick) // Refresh osu info if the name changed if key == "Accounts.Osu.Nick" { if newNick == "" { user.Accounts.Osu.PP = 0 user.Accounts.Osu.Level = 0 user.Accounts.Osu.Accuracy = 0 } else { go func() { err := user.RefreshOsuInfo() if err != nil { color.Red("Error refreshing osu info of user '%s' with osu nick '%s': %v", user.Nick, newNick, err) return } color.Green("Refreshed osu info of user '%s' with osu nick '%s': %v", user.Nick, newNick, user.Accounts.Osu.PP) user.Save() }() } } return true, nil } return false, nil } // Save saves the user object in the database. func (user *User) Save() { DB.Set("User", user.ID, user) } // Filter removes privacy critical fields from the user object. func (user *User) Filter() { user.Email = "" user.Gender = "" user.FirstName = "" user.LastName = "" user.IP = "" user.UserAgent = "" user.LastLogin = "" user.LastSeen = "" user.Accounts.Facebook.ID = "" user.Accounts.Google.ID = "" user.Accounts.Twitter.ID = "" user.BirthDay = "" user.Location = &Location{} user.Browser = UserBrowser{} user.OS = UserOS{} } // ShouldFilter tells whether data needs to be filtered in the given context. func (user *User) ShouldFilter(ctx aero.Context) bool { ctxUser := GetUserFromContext(ctx) if ctxUser != nil && ctxUser.Role == "admin" { return false } return true }