2019-08-28 12:07:37 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
knownTotalXP int64
|
|
|
|
knownFeed time.Time
|
|
|
|
|
|
|
|
playerInfoCache *playerInfo
|
|
|
|
)
|
|
|
|
|
|
|
|
type activity struct {
|
|
|
|
Date string `json:"date"`
|
|
|
|
Details string `json:"details"`
|
|
|
|
Text string `json:"text"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a activity) GetParsedDate() (time.Time, error) {
|
|
|
|
loc, err := time.LoadLocation("Europe/London")
|
|
|
|
if err != nil {
|
|
|
|
return time.Time{}, errors.Wrap(err, "Unable to load London time information")
|
|
|
|
}
|
|
|
|
|
|
|
|
return time.ParseInLocation("02-Jan-2006 15:04", a.Date, loc)
|
|
|
|
}
|
|
|
|
|
|
|
|
type skill struct {
|
|
|
|
ID skillID `json:"id"`
|
|
|
|
Level int `json:"level"`
|
|
|
|
Rank int64 `json:"rank"`
|
|
|
|
XP int64 `json:"xp"`
|
|
|
|
|
2019-09-28 19:47:42 +00:00
|
|
|
TargetLevel int
|
|
|
|
Updated time.Time
|
2019-08-28 12:07:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type playerInfo struct {
|
|
|
|
Activities []activity `json:"activities"`
|
|
|
|
CombatLevel int `json:"combatlevel"`
|
|
|
|
LoggedIn bool `json:"loggedIn,string"`
|
|
|
|
Magic int64 `json:"magic"`
|
|
|
|
Melee int64 `json:"melee"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
QuestsComplete int `json:"questscomplete"`
|
|
|
|
QuestsNotStarted int `json:"questsnotstarted"`
|
|
|
|
QuestsStarted int `json:"questsstarted"`
|
|
|
|
Ranged int64 `json:"ranged"`
|
|
|
|
Rank string `json:"rank"`
|
|
|
|
SkillValues []skill `json:"skillvalues"`
|
|
|
|
TotalSkill int64 `json:"totalskill"`
|
|
|
|
TotalXP int64 `json:"totalxp"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p playerInfo) NumericRank() int64 {
|
|
|
|
v, _ := strconv.ParseInt(strings.Replace(p.Rank, ",", "", -1), 10, 64)
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p playerInfo) GetSkill(s skillID) skill {
|
|
|
|
for _, sk := range p.SkillValues {
|
|
|
|
if sk.ID == s {
|
|
|
|
return sk
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return skill{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p playerInfo) storeCache() error {
|
|
|
|
cacheDir, err := os.UserCacheDir()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "Unable to retrieve user cache dir")
|
|
|
|
}
|
|
|
|
|
|
|
|
cacheDir = path.Join(cacheDir, "luzifer", "runemetrics")
|
|
|
|
if err = os.MkdirAll(cacheDir, 0755); err != nil {
|
|
|
|
return errors.Wrap(err, "Unable to create cache dir")
|
|
|
|
}
|
|
|
|
|
|
|
|
cacheFile := path.Join(cacheDir, "metrics.json")
|
|
|
|
f, err := os.Create(cacheFile)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "Unable to create cache file")
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
return errors.Wrap(json.NewEncoder(f).Encode(p), "Unable to marshal into cache file")
|
|
|
|
}
|
|
|
|
|
|
|
|
func getPlayerInfo(name string, activities int) (*playerInfo, error) {
|
|
|
|
if name == "" {
|
|
|
|
return nil, errors.New("Player name must not be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
params := url.Values{
|
|
|
|
"user": []string{name},
|
|
|
|
"activities": []string{strconv.Itoa(activities)},
|
|
|
|
}
|
|
|
|
uri := "https://apps.runescape.com/runemetrics/profile/profile?" + params.Encode()
|
|
|
|
|
|
|
|
resp, err := http.Get(uri)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "Unable to query profile data")
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
out := &playerInfo{}
|
|
|
|
if err = json.NewDecoder(resp.Body).Decode(out); err != nil {
|
|
|
|
return nil, errors.Wrap(err, "Unable to decode profile data")
|
|
|
|
}
|
|
|
|
|
|
|
|
if playerInfoCache != nil {
|
|
|
|
for i, nSk := range out.SkillValues {
|
|
|
|
if oSk := playerInfoCache.GetSkill(nSk.ID); oSk.XP == nSk.XP {
|
|
|
|
out.SkillValues[i].Updated = oSk.Updated
|
2019-09-28 19:47:42 +00:00
|
|
|
out.SkillValues[i].TargetLevel = oSk.TargetLevel
|
2019-08-28 12:07:37 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
out.SkillValues[i].Updated = time.Now()
|
|
|
|
}
|
2019-09-07 16:16:14 +00:00
|
|
|
|
|
|
|
var (
|
|
|
|
lastActivity = out.Activities[len(out.Activities)-1]
|
|
|
|
skip = true
|
|
|
|
)
|
|
|
|
|
|
|
|
for _, a := range playerInfoCache.Activities {
|
|
|
|
// Times are no good match: they might be duplicated, we search
|
|
|
|
// last message which should never duplicate.
|
|
|
|
if a.Details == lastActivity.Details {
|
|
|
|
skip = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if skip {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
out.Activities = append(out.Activities, a)
|
|
|
|
}
|
2019-08-28 12:07:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if knownTotalXP != out.TotalXP {
|
|
|
|
knownTotalXP = out.TotalXP
|
|
|
|
lastUpdate[updateKeyTotalXP] = time.Now()
|
|
|
|
}
|
|
|
|
|
|
|
|
if d, _ := out.Activities[0].GetParsedDate(); !d.Equal(knownFeed) {
|
|
|
|
knownFeed = d
|
|
|
|
lastUpdate[updateKeyFeed] = time.Now()
|
|
|
|
}
|
|
|
|
|
|
|
|
lastUpdate[updateKeyGeneral] = time.Now()
|
|
|
|
|
|
|
|
playerInfoCache = out
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadPlayerInfoCache() (*playerInfo, error) {
|
|
|
|
cacheDir, err := os.UserCacheDir()
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "Unable to retrieve user cache dir")
|
|
|
|
}
|
|
|
|
|
|
|
|
cacheFile := path.Join(cacheDir, "luzifer", "runemetrics", "metrics.json")
|
|
|
|
if _, err := os.Stat(cacheFile); err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
// Empty cache
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return nil, errors.Wrap(err, "Unable to stat cache file")
|
|
|
|
}
|
|
|
|
|
|
|
|
f, err := os.Open(cacheFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "Unable to open cache file")
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
p := &playerInfo{}
|
|
|
|
return p, errors.Wrap(json.NewDecoder(f).Decode(p), "Unable to unmarshal cache file")
|
|
|
|
}
|