Skip to content

Commit

Permalink
feat: add paginated fetching and token refreshing
Browse files Browse the repository at this point in the history
  • Loading branch information
hazcod committed Aug 14, 2024
1 parent 31c4d80 commit 0604ec1
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 36 deletions.
4 changes: 2 additions & 2 deletions cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/hazcod/personio-abscences/pkg/slack"
"github.com/sirupsen/logrus"
"os"
"sort"
"slices"
)

func main() {
Expand Down Expand Up @@ -53,7 +53,7 @@ func main() {
os.Exit(0)
}

sort.Strings(absentees)
slices.Sort(absentees)

message := fmt.Sprintf(":x: *Out today* (%d):\n", len(absentees))
for _, absentee := range absentees {
Expand Down
95 changes: 66 additions & 29 deletions pkg/personio/abscences.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)

const (
timeOffURL = "https://api.personio.de/v1/company/time-offs"
queryLimit = 200
)

type Employee struct {
Type string `json:"type"`
Attributes struct {
Expand Down Expand Up @@ -78,50 +84,81 @@ type abscenceResponse struct {
func (p *Personio) GetAbscences() ([]string, error) {
token, err := p.getToken()
if err != nil {
return nil, fmt.Errorf("could not get auth token: %w", err)
}
if token == "" {
return nil, fmt.Errorf("token was empty")
return nil, fmt.Errorf("could not get auth value: %w", err)
}

today := time.Now().Format("2006-01-02")

url := "https://api.personio.de/v1/company/time-offs?limit=200&offset=0&start_date=" + today + "&end_date=" + today
var absentees []string

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("could not create request: %w", err)
}
page := 0
pages := 1

req.Header.Add("accept", "application/json")
req.Header.Add("authorization", "Bearer "+token)
for {
params := url.Values{}
params.Add("limit", fmt.Sprintf("%d", queryLimit))
// weird bug where page=0 and page=1 return same results from personio API. so just immediately fetch page=1
params.Add("offset", fmt.Sprintf("%d", page+1))
params.Add("start_date", today)
params.Add("end_date", today)

p.logger.Debugf("retrieving abscences from personio")
fullURL := fmt.Sprintf("%s?%s", timeOffURL, params.Encode())

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("could not get abscences: %w", err)
}
req, err := http.NewRequest(http.MethodGet, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("could not create request: %w", err)
}

defer res.Body.Close()
req.Header.Add("accept", "application/json")
req.Header.Add("authorization", "Bearer "+token)

body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("could not read abscences: %w", err)
}
p.logger.WithField("page", page).WithField("total_pages", pages).WithField("url", fullURL).
Debug("fetching abscences")

var response abscenceResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("could not parse abscences: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("could not get abscences: %w", err)
}

defer res.Body.Close()

body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("could not read abscences: %w", err)
}

var response abscenceResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("could not parse abscences: %w", err)
}

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("could not get abscences: status code %d", res.StatusCode)
}

p.logger.WithField("page", page).WithField("total_pages", pages).WithField("url", fullURL).
WithField("returned", len(response.Data)).
Debug("received abscences")

for _, data := range response.Data {
absentees = append(absentees,
data.Attributes.Employee.Attributes.FirstName.Value+" "+
data.Attributes.Employee.Attributes.LastName.Value,
)
}

// Determine if there are more pages to fetch
p.logger.Tracef("set total pages to %d", response.Metadata.TotalPages)

absentees := make([]string, len(response.Data))
if page+1 >= response.Metadata.TotalPages {
break
}

for i, data := range response.Data {
absentees[i] = data.Attributes.Employee.Attributes.FirstName.Value + " " + data.Attributes.Employee.Attributes.LastName.Value
pages = response.Metadata.TotalPages
page += 1
}

p.logger.WithField("total", len(absentees)).Debug("retrieved abscenes")
p.logger.WithField("total", len(absentees)).Debug("retrieved abscences")

return absentees, nil
}
32 changes: 28 additions & 4 deletions pkg/personio/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
"io"
"net/http"
"strings"
"time"
)

const (
authURL = "https://api.personio.de/v1/auth"
)

type authResponse struct {
Expand All @@ -20,8 +25,19 @@ type authResponse struct {
} `json:"error"`
}

type tokenFactory struct {
expires time.Time
value string
}

func (p *Personio) getToken() (string, error) {
url := "https://api.personio.de/v1/auth"
if p.token == nil {
p.token = &tokenFactory{}
}

if p.token.expires.After(time.Now().Add(time.Second * 5)) {
return p.token.value, nil
}

payload := struct {
ClientID string `json:"client_id"`
Expand All @@ -38,7 +54,7 @@ func (p *Personio) getToken() (string, error) {

p.logger.WithField("client_id", p.clientID).Debug("authenticating to personio")

req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(string(payloadBytes)))
req, err := http.NewRequest(http.MethodPost, authURL, strings.NewReader(string(payloadBytes)))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
Expand Down Expand Up @@ -66,7 +82,15 @@ func (p *Personio) getToken() (string, error) {
return "", fmt.Errorf("failed with status code %d: %s", res.StatusCode, response.Error.Message)
}

p.logger.Debug("successfully retrieved token")
if response.Data.Token == "" {
return "", fmt.Errorf("received empty token from personio")
}

p.token.value = response.Data.Token
p.token.expires = time.Now().Add(time.Duration(response.Data.ExpiresIn) * time.Second)

p.logger.WithField("expires", p.token.expires.Format(time.RFC822)).
Debug("successfully retrieved new token")

return response.Data.Token, nil
return p.token.value, nil
}
3 changes: 2 additions & 1 deletion pkg/personio/personio.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Personio struct {
logger *logrus.Logger
clientID string
secret string
token *tokenFactory
}

func New(l *logrus.Logger, clientID, secret string) (*Personio, error) {
Expand All @@ -18,7 +19,7 @@ func New(l *logrus.Logger, clientID, secret string) (*Personio, error) {
return nil, fmt.Errorf("clientID or secret is empty")
}

p := Personio{logger: l, clientID: clientID, secret: secret}
p := Personio{logger: l, clientID: clientID, secret: secret, token: &tokenFactory{}}

return &p, nil
}

0 comments on commit 0604ec1

Please sign in to comment.