Currently we have some license issues. We are working on it.

Commit 5cb67e6a authored by zauberstuhl's avatar zauberstuhl
Browse files

Replace custom API

see github.com/mdlayher/untappd
parent c8b5b7d9
/*
* Unmappd - Visualize your Untappd checkins
* Copyright (C) 2019 Lukas Matt <lukas@matt.wf>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"time"
"net/http"
"io/ioutil"
"encoding/json"
"fmt"
"log"
"github.com/spf13/viper"
)
const rootUrl = "https://api.untappd.com"
type Uresp struct {
Response struct {
Pagination struct {
Max_id int
}
Checkins struct {
Items []struct {
Checkin_id uint
Created_at string
Venue interface{}
}
}
}
}
type Uauthresp struct {
Response struct {
Access_token string
}
}
type Uinfo struct {
Response struct {
User struct {
User_name string
Stats struct {
Total_badges int
Total_friends int
Total_checkins int
Total_beers int
Total_created_beers int
Total_followings int
Total_photos int
}
}
}
}
func auth_handler(w http.ResponseWriter, r *http.Request) {
db, err := openDatabase()
if err != nil {
log.Print(err)
http.Redirect(w, r, "/", 301)
return
}
defer db.Close()
if codes, ok := r.URL.Query()["code"]; ok {
token, err := fetch_auth(codes[0])
if err != nil {
log.Print("Cannot fetch auth token: ", err)
http.Redirect(w, r, "/", 301)
return
}
info, err := fetch_user_info(token)
username := info.Response.User.User_name
if err != nil || username == "" {
log.Print("Cannot fetch user info: ", err)
http.Redirect(w, r, "/", 301)
return
}
userParam := "#/map/%s"
err = db.First(&User{}, "username = ?", username).Error
if err == nil {
http.Redirect(w, r,
fmt.Sprintf(userParam, username), 301)
return
}
var stats = info.Response.User.Stats
var user = User{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
TotalBadges: stats.Total_beers,
TotalFriends: stats.Total_friends,
TotalCheckins: stats.Total_checkins,
TotalBeers: stats.Total_beers,
TotalCreatedBeers: stats.Total_created_beers,
TotalFollowings: stats.Total_followings,
TotalPhotos: stats.Total_photos,
Username: username,
Token: token,
}
err = db.Create(&user).Error
if err != nil {
log.Print("Cannot create user: ", err)
http.Redirect(w, r, "/", 301)
return
}
http.Redirect(w, r,
fmt.Sprintf(userParam, user.Username), 301)
return
}
http.Redirect(w, r, "/", 301)
}
func fetch_auth(code string) (string, error) {
var uauth Uauthresp
return uauth.Response.Access_token, get(fmt.Sprintf(
"https://untappd.com/oauth/authorize/?client_id=%s&client_secret=%s&response_type=code&redirect_url=%s&code=%s",
viper.GetString("clientID"),
viper.GetString("clientSecret"),
viper.GetString("redirectUrl"),
code,
), &uauth)
}
func fetch_user_info(token string) (info Uinfo, err error) {
url := fmt.Sprintf(
"%s/v4/user/info/?access_token=%s", rootUrl, token)
return info, get(url, &info)
}
func fetch_checkins(user *User, id uint, next bool) (resp Uresp, err error) {
url := fmt.Sprintf(
"%s/v4/user/checkins/%s?access_token=%s",
rootUrl, user.Username, user.Token)
if id > 0 {
param := "min_id"
if next {
param = "max_id"
}
url = fmt.Sprintf("%s&%s=%d", url, param, id)
}
log.Print("url ", url)
return resp, get(url, &resp)
}
func get(url string, v interface{}) error {
resp, err := http.Get(url)
if err != nil {
log.Print("http.Get", err)
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Print("ioutil.ReadAll", err)
return err
}
if viper.GetBool("debug") {
log.Printf("get url body: %s\n", string(body))
}
return json.Unmarshal(body, v)
}
......@@ -2,17 +2,18 @@ module git.feneas.org/zauberstuhl/unmappd
go 1.16
replace github.com/mdlayher/untappd => git.feneas.org/zauberstuhl/untappd-library v0.0.0-20210308145245-1a32f76cfce0
require (
github.com/jinzhu/gorm v1.9.10
github.com/lib/pq v1.2.0 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.2
github.com/mdlayher/untappd v0.0.0-00010101000000-000000000000
github.com/pelletier/go-toml v1.4.0 // indirect
github.com/robfig/cron v1.2.0
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.4.0
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
google.golang.org/appengine v1.6.1 // indirect
)
......@@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
git.feneas.org/zauberstuhl/untappd-library v0.0.0-20210308145245-1a32f76cfce0 h1:BC9pch4Ti/e0BU6BkDJeTMsnhb4OpyvNCbxBybxKONE=
git.feneas.org/zauberstuhl/untappd-library v0.0.0-20210308145245-1a32f76cfce0/go.mod h1:GL8/pdyTa81GelywA7xo8uGkkUKAqTDC5zA8v+DncVc=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
......@@ -20,6 +22,7 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA=
......@@ -93,8 +96,6 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
......@@ -127,6 +128,8 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
......@@ -147,6 +150,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
......@@ -176,8 +180,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
......
......@@ -23,6 +23,7 @@ import (
"log"
"github.com/spf13/viper"
"github.com/mdlayher/untappd"
)
func init() {
......@@ -49,12 +50,22 @@ func main() {
Addr: "0.0.0.0:8080",
}
auth, _, err := untappd.NewAuthHandler(
viper.GetString("clientID"),
viper.GetString("clientSecret"),
viper.GetString("redirectUrl"),
token_handler,
nil,
); if err != nil {
panic(err.Error())
}
http.HandleFunc("/api/v0/config", config_handler)
http.HandleFunc("/api/v0/users/", user_handler)
http.HandleFunc("/api/v0/locations/", location_handler)
http.HandleFunc("/api/v0/auth", auth_handler)
http.HandleFunc("/api/v0/auth", auth.ServeHTTP)
// NOTE will be obsolete in future
http.HandleFunc("/auth", auth_handler)
http.HandleFunc("/auth", auth.ServeHTTP)
// assets directory
fs := http.FileServer(http.Dir("dist"))
http.Handle("/", http.StripPrefix("/", fs))
......
......@@ -22,6 +22,7 @@ import (
"time"
"fmt"
"github.com/mdlayher/untappd"
"github.com/spf13/viper"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
......@@ -100,7 +101,66 @@ func userByUsername(username string) (*User, error) {
var user User
err = db.First(&user, "username = ?", username).Error
if err == nil && viper.GetBool("worker") && len(user.Locations) == 0 {
go checkin_worker(&user, true)
go checkin_worker(&user)
}
return &user, err
}
func minLocationID(user *User) (uint, error) {
return locationID("min", user)
}
func maxLocationID(user *User) (uint, error) {
return locationID("max", user)
}
func locationID(sqlfunc string, user *User) (uint, error) {
var location Location
db, err := openDatabase()
if err != nil {
return location.ID, err
}
defer db.Close()
return location.ID, db.
Where("user_id = ?", user.ID).
First(&location).
Order("id asc").
Error
}
func insertCheckins(user *User, checkins []*untappd.Checkin) (uint, error) {
var maxID uint = 0
db, err := openDatabase()
if err != nil {
return maxID, err
}
defer db.Close()
for _, checkin := range checkins {
location := Location{
ID: uint(checkin.ID),
UserID: user.ID,
CreatedAt: checkin.Created,
}
if checkin.Venue != nil {
location.Lat = checkin.Venue.Location.Latitude
location.Lng = checkin.Venue.Location.Longitude
location.Name = checkin.Venue.Name
location.Category = checkin.Venue.Category
location.Url = checkin.Venue.Foursquare.URL
location.Icon = checkin.Venue.Icon.SmallIcon.String()
}
err := db.Create(&location).Error
if err != nil {
return maxID, err
}
user.Locations = append(user.Locations, location)
if maxID == 0 || uint(checkin.ID) < maxID {
maxID = uint(checkin.ID)
}
}
return maxID, nil
}
......@@ -65,7 +65,7 @@ export default {
},
data () {
return {
atHome: 'Untappd at Home',
atHome: 'UNTAPPD AT HOME',
loaded: false,
failed: false,
noUserFound: false,
......@@ -105,16 +105,37 @@ export default {
// load location data
Api.get('/locations/' + username).then(response => {
// NOTE ignore untappd-at-home checkins and display them as toast message
this.locations = response.data.filter(l => l.name !== this.atHome)
this.locations = response.data.filter(
l => l.name.toUpperCase() !== this.atHome && l.lng !== 0 && l.lat !== 0
)
if (this.locations.length > 0) {
this.loaded = true
var h = this.$createElement
// display untappd-at-home as toast message
// since we probably never visited the untappd HQ
var atHome = response.data.filter(l => l.name === this.atHome)
var atHome = response.data.filter(l => l.name.toUpperCase() === this.atHome)
if (atHome.length > 0) {
this.toast(
`${atHome.length} checkins`,
this.atHomeTable(atHome[0])
atHome[0].name,
h('table', { class: { 'text-center': true }, style: { 'width': '100%' } }, [
h('tr', {}, [
h('td', {}, [h('b-icon', { attrs: { 'icon': 'house-door', 'font-size': '64px' }})]),
h('td', {}, [h('b', {}, `${atHome.length} checkins`)])
])
])
)
}
// display checkins without a location as toast message
var unknown = response.data.filter(l => l.lng === 0 && l.lat === 0)
if (unknown.length > 0) {
this.toast(
'Checkins without a location',
h('table', { class: { 'text-center': true }, style: { 'width': '100%' } }, [
h('tr', {}, [
h('td', {}, [h('b-icon', { attrs: { 'icon': 'question-square', 'font-size': '64px' }})]),
h('td', {}, [h('b', {}, `${unknown.length} checkins`)])
])
])
)
}
} else {
......@@ -136,22 +157,6 @@ export default {
noAutoHide: true,
appendToast: true
})
},
atHomeTable (loc) {
var h = this.$createElement
var link = h('p', {}, ['Link: ', h('a', { attrs: { 'href': loc.url } }, loc.url)])
var category = h('p', {}, ['Category: ', loc.category])
var table = h('table', { class: { 'text-center': true }, style: { 'width': '100%' } }, [
h('tr', {}, [
h('td', {}, [h('img', { attrs: { 'src': loc.icon, 'alt': loc.name } })]),
h('td', {}, [h('b', {}, loc.name)])
]),
h('tr', {}, [
h('td', { class: { 'pt-2': true } }),
h('td', { class: { 'pt-2': true } }, [link, category])
])
])
return table
}
}
}
......
......@@ -19,11 +19,14 @@
package main
import (
"fmt"
"time"
"net/http"
"encoding/json"
"regexp"
"log"
"github.com/mdlayher/untappd"
"github.com/spf13/viper"
)
......@@ -78,3 +81,66 @@ func config_handler(w http.ResponseWriter, r *http.Request) {
Worker: viper.GetBool("worker"),
})
}
func token_handler(token string, w http.ResponseWriter, r *http.Request) {
db, err := openDatabase()
if err != nil {
log.Print(err)
http.Redirect(w, r, "/", 301)
return
}
defer db.Close()
client, err := untappd.NewAuthenticatedClient(token, nil)
if err != nil {
log.Print(err)
http.Redirect(w, r, "/", 301)
return
}
info, _, err := client.User.Info("", true)
if err != nil {
log.Print(err)
http.Redirect(w, r, "/", 301)
return
}
userParam := "#/map/%s"
err = db.First(&User{}, "username = ?", info.UserName).Error
if err == nil {
http.Redirect(w, r, fmt.Sprintf(userParam, info.UserName), 301)
return
}
var user = User{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
TotalBadges: info.Stats.TotalBadges,
TotalFriends: info.Stats.TotalFriends,
TotalCheckins: info.Stats.TotalCheckins,
TotalBeers: info.Stats.TotalBeers,
TotalCreatedBeers: info.Stats.TotalCreatedBeers,
TotalFollowings: info.Stats.TotalFollowings,
TotalPhotos: info.Stats.TotalPhotos,
Username: info.UserName,
Token: token,
}
err = db.Create(&user).Error
if err != nil {
log.Print("Cannot create user: ", err)
http.Redirect(w, r, "/", 301)
return
}
// first-time setup
go func() {
err = checkin_worker(&user)
if err != nil {
log.Print("Cannot fetch checkins: ", err)
}
}()
http.Redirect(w, r, fmt.Sprintf(userParam, user.Username), 301)
return
}
......@@ -19,9 +19,11 @@
package main
import (
"strings"
"time"
"log"
"github.com/mdlayher/untappd"
"github.com/spf13/viper"
"github.com/robfig/cron"
)
......@@ -55,7 +57,7 @@ func cronHandler() {
for _, user := range users {
err = info_worker(&user)
if err == nil {
err = checkin_worker(&user, false)
err = checkin_worker(&user)
if err != nil && viper.GetBool("debug") {
log.Printf("checkin_worker failed: %+v\n", err)
}
......@@ -72,12 +74,16 @@ func info_worker(user *User) error {
}
defer db.Close()
info, err := fetch_user_info(user.Token)
client, err := untappd.NewAuthenticatedClient(user.Token, nil)
if err != nil {
return err
}
info, _, err := client.User.Info(user.Username, true)
if err != nil {
return err
}
var stats = info.Response.User.Stats
// db.Save is too recursive so we will use db.Updates
// also we should use a map instead of a struct cause
// of some known problems with default values
......@@ -85,97 +91,81 @@ func info_worker(user *User) error {
return db.Model(&User{}).Where("id = ?", user.ID).
Updates(map[string]interface{}{
"updated_at": time.Now(),
"total_badges": stats.Total_badges,
"total_friends": stats.Total_friends,
"total_checkins": stats.Total_checkins,
"total_beers": stats.Total_beers,
"total_created_beers": stats.Total_created_beers,
"total_followings": stats.Total_followings,
"total_photos": stats.Total_photos,
"total_badges": info.Stats.TotalBadges,
"total_friends": info.Stats.TotalFriends,
"total_checkins": info.Stats.TotalCheckins,
"total_beers": info.Stats.TotalBeers,
"total_created_beers": info.Stats.TotalCreatedBeers,
"total_followings": info.Stats.TotalFollowings,
"total_photos": info.Stats.TotalPhotos,
}).Error
}
func checkin_worker(user *User, all bool) error {
func checkin_worker(user *User) error {
db, err := openDatabase()
if err != nil {
return err
}
defer db.Close()
var id uint
for {
resp, err := fetch_checkins(user, id, all)
client, err := untappd.NewAuthenticatedClient(user.Token, nil)
if err != nil {
return err
}
var maxID uint = 0
lastID, err := maxLocationID(user)
if err == nil {
checkins, _, err := client.User.CheckinsMinMaxIDLimit(user.Username, 0, 0, 50)
if err != nil {
return err
}
// avoid a re-run if the last result was empty
if len(resp.Response.Checkins.Items) == 0 {
var unknownCheckins []*untappd.Checkin
for _, checkin := range checkins {
if uint(checkin.ID) > lastID {
unknownCheckins = append(unknownCheckins, checkin)
}
}