Define my YNAB personal access token as an environment variable. Prefix Monzo environment variables with "monzo_" to more easily differentiate between Monzo credentials and YNAB credentials.
		
			
				
	
	
		
			125 lines
		
	
	
	
		
			4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			125 lines
		
	
	
	
		
			4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Exporting Monzo transactions to my YouNeedABudget.com (i.e. YNAB)
 | 
						|
// account. YNAB unfortunately doesn't currently offer an Monzo integration. As
 | 
						|
// a workaround and a practical excuse to learn Go, I decided to write one
 | 
						|
// myself.
 | 
						|
//
 | 
						|
// This job is going to run N times per 24 hours. Monzo offers webhooks for
 | 
						|
// reacting to certain types of events. I don't expect I'll need realtime data
 | 
						|
// for my YNAB integration. That may change, however, so it's worth noting.
 | 
						|
 | 
						|
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"log"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"os/exec"
 | 
						|
)
 | 
						|
 | 
						|
////////////////////////////////////////////////////////////////////////////////
 | 
						|
// Constants
 | 
						|
////////////////////////////////////////////////////////////////////////////////
 | 
						|
 | 
						|
var (
 | 
						|
	clientId     = os.Getenv("monzo_client_id")
 | 
						|
	clientSecret = os.Getenv("monzo_client_secret")
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	redirectURI = "http://localhost:8080/authorization-code"
 | 
						|
	// TODO(wpcarro): Consider generating a random string for the state when the
 | 
						|
	// application starts instead of hardcoding it here.
 | 
						|
	state = "xyz123"
 | 
						|
)
 | 
						|
 | 
						|
////////////////////////////////////////////////////////////////////////////////
 | 
						|
// Business Logic
 | 
						|
////////////////////////////////////////////////////////////////////////////////
 | 
						|
 | 
						|
// This is the response returned from Monzo when we exchange our authorization
 | 
						|
// code for an access token. While Monzo returns additional fields, I'm only
 | 
						|
// interested in AccessToken and RefreshToken.
 | 
						|
type accessTokenResponse struct {
 | 
						|
	AccessToken  string `json:"access_token"`
 | 
						|
	RefreshToken string `json:"refresh_token"`
 | 
						|
}
 | 
						|
 | 
						|
// TODO(wpcarro): Replace http.PostForm and other similar calls with
 | 
						|
// client.postForm. The default http.Get and other methods doesn't timeout, so
 | 
						|
// it's better to create a configured client with a value for the timeout.
 | 
						|
 | 
						|
func getAccessToken(code string) {
 | 
						|
	res, err := http.PostForm("https://api.monzo.com/oauth2/token", url.Values{
 | 
						|
		"grant_type":    {"authorization_code"},
 | 
						|
		"client_id":     {clientId},
 | 
						|
		"client_secret": {clientSecret},
 | 
						|
		"redirect_uri":  {redirectURI},
 | 
						|
		"code":          {code},
 | 
						|
	})
 | 
						|
	failOn(err)
 | 
						|
	defer res.Body.Close()
 | 
						|
 | 
						|
	payload := accessTokenResponse{}
 | 
						|
	json.NewDecoder(res.Body).Decode(&payload)
 | 
						|
 | 
						|
	log.Printf("Access token: %s\n", payload.AccessToken)
 | 
						|
	log.Printf("Refresh token: %s\n", payload.AccessToken)
 | 
						|
}
 | 
						|
 | 
						|
func listenHttp(sigint chan os.Signal) {
 | 
						|
	// Use a go-routine to listen for interrupt signals to shutdown our HTTP
 | 
						|
	// server.
 | 
						|
	go func() {
 | 
						|
		<-sigint
 | 
						|
		// TODO(wpcarro): Do we need context here? I took this example from the
 | 
						|
		// example on golang.org.
 | 
						|
		log.Println("Warning: I should be shutting down and closing the connection here, but I'm not.")
 | 
						|
		close(sigint)
 | 
						|
	}()
 | 
						|
 | 
						|
	log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
						|
		// 1. Get authorization code from Monzo.
 | 
						|
		if req.URL.Path == "/authorization-code" {
 | 
						|
			params := req.URL.Query()
 | 
						|
			reqState := params["state"][0]
 | 
						|
			code := params["code"][0]
 | 
						|
 | 
						|
			if reqState != state {
 | 
						|
				log.Fatalf("Value for state returned by Monzo does not equal our state. %s != %s", reqState, state)
 | 
						|
			}
 | 
						|
 | 
						|
			// TODO(wpcarro): Add a more interesting authorization confirmation
 | 
						|
			// screen -- or even nothing at all.
 | 
						|
			fmt.Fprintf(w, "Authorized!")
 | 
						|
 | 
						|
			// Exchange the authorization code for an access token.
 | 
						|
			getAccessToken(code)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		log.Printf("Unhandled request: %v\n", *req)
 | 
						|
	})))
 | 
						|
}
 | 
						|
 | 
						|
// Open a web browser to allow the user to authorize this application.
 | 
						|
// TODO(wpcarro): Prefer using an environment variable for the web browser
 | 
						|
// instead of assuming it will be google-chrome.
 | 
						|
func authorizeClient() {
 | 
						|
	url := fmt.Sprintf("https://auth.monzo.com/?client_id=%s&redirect_uri=%s&response_type=code&state=%s", clientId, redirectURI, state)
 | 
						|
	exec.Command("google-chrome", url).Start()
 | 
						|
}
 | 
						|
 | 
						|
func main() {
 | 
						|
	sigint := make(chan os.Signal, 1)
 | 
						|
	// TODO(wpcarro): Remove state here. I'm using as a hack to prevent my
 | 
						|
	// program from halting before I'd like it to. Once I'm more comfortable
 | 
						|
	// using channels, this should be a trivial change.
 | 
						|
	state := make(chan bool)
 | 
						|
 | 
						|
	authorizeClient()
 | 
						|
	listenHttp(sigint)
 | 
						|
	<-state
 | 
						|
}
 |