service-notifications/planningcenter.go
2023-09-09 22:39:48 -05:00

216 lines
4.9 KiB
Go

package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
// Make an API request to Planning Center.
func NewPCRequest(uri string) (*http.Request, error) {
url := uri
// If request URI doesn't include full URL, prepend the PC API URL.
if !strings.HasPrefix(url, "http") {
url = "https://api.planningcenteronline.com" + uri
}
// Make the request.
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// Append the basic authentication from the configuration.
auth := app.config.PlanningCenter.AppID + ":" + app.config.PlanningCenter.Secret
authString := base64.StdEncoding.EncodeToString([]byte(auth))
req.Header.Add("Authorization", "Basic "+authString)
// Return the request made.
return req, nil
}
// Planning center meta data/information about request.
type PCMeta struct {
TotalCount uint64 `json:"total_count"`
Count uint64 `json:"count"`
Prev struct {
Offset uint64 `json:"offset"`
} `json:"prev"`
Next struct {
Offset uint64 `json:"offset"`
} `json:"next"`
CanOrderBy []string `json:"can_order_by"`
CanQueryBy []string `json:"can_query_by"`
CanInclude []string `json:"can_include"`
Parent struct {
Id string `json:"id"`
Type string `json:"type"`
} `json:"parent"`
}
// A dictionary for planning center response parsing.
type PCDict map[string]interface{}
// Common response error structure.
type PCError struct {
Status string `json:"status"`
Title string `json:"title"`
Detail string `json:"detail"`
}
// Basic PC response structure.
type PCResponse struct {
Links struct {
Self string `json:"self"`
Prev string `json:"prev"`
Next string `json:"next"`
} `json:"links"`
Data []PCDict `json:"data"`
Included []interface{} `json:"included"`
Meta PCMeta `json:"meta"`
Errors []PCError `json:"errors"`
}
// Parse a planning center reponse body.
func PCParseResponse(body io.Reader) (*PCResponse, error) {
// Decode JSON response.
res := new(PCResponse)
err := json.NewDecoder(body).Decode(res)
if err != nil {
return nil, err
}
// If an error was provided from the API, return it.
if len(res.Errors) != 0 {
return nil, fmt.Errorf(res.Errors[0].Detail)
}
// We expect result to be provided on a valid response.
if res.Data == nil {
return nil, fmt.Errorf("no data in response")
}
// A valid response was decoded, return it.
return res, nil
}
// Query Planning Center API and get data from all pages.
func PCGetAll(uri string) ([]PCDict, error) {
// The data array to store all found data.
var data []PCDict
// Set the first URL to the requested URL.
url := uri
// Make requests until the last page was loaded.
for {
// Make the request.
req, err := NewPCRequest(url)
if err != nil {
return nil, err
}
// Perform the request.
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Close body when done.
defer res.Body.Close()
// Parse the response.
response, err := PCParseResponse(res.Body)
if err != nil {
return nil, err
}
// Add data from response to global data array.
data = append(data, response.Data...)
// If no next link provided, stop here.
if response.Links.Next == "" {
break
}
// If next link provided, set it for the next request.
url = response.Links.Next
}
// Return all found data.
return data, nil
}
// Below are a bunch of helper functions.
// I would recommend using a tool like Insomnia to test API requests,
// then you will know what the data structure is like for an API request.
// Planning center does have some ok documentation available:
// https://developer.planning.center/docs/#/overview
// Get a string from a dictionary.
func (p PCDict) GetString(key string) string {
s, ok := p[key].(string)
if !ok {
return ""
}
return s
}
// Get a bool from a dictionary.
func (p PCDict) GetBool(key string) bool {
b, ok := p[key].(bool)
if !ok {
return false
}
return b
}
// Get an unsigned int from dictionary.
func (p PCDict) GetUint64(key string) uint64 {
s, ok := p[key].(string)
var i uint64
// Try parsing a string if its ok.
if ok {
i, _ = strconv.ParseUint(s, 10, 64)
} else {
// Otherwise, try converting to an integer.
i, ok = p[key].(uint64)
if !ok {
return 0
}
}
return i
}
// Get a dictionary from a dictionary.
func (p PCDict) GetDict(key string) PCDict {
d, ok := p[key].(map[string]interface{})
if !ok {
return make(map[string]interface{})
}
return d
}
// Standard date layouts.
const (
PCDateTimeLayout = "2006-01-02T15:04:05Z"
PCDateLayout = "2006-01-02"
)
// Get a date from a dictionary.
func (p PCDict) GetDate(key string) time.Time {
var t time.Time
var err error
s, ok := p[key].(string)
if ok {
// Try parsing with the time layout first.
t, err = time.Parse(PCDateTimeLayout, s)
if err != nil {
// If that fialed, try using the date layout.
t, _ = time.Parse(PCDateLayout, s)
}
}
return t
}