190 lines
5.5 KiB
Go
190 lines
5.5 KiB
Go
/*
|
|
Copyright (C) 2025 TheWanderingCrow
|
|
|
|
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 (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/joho/godotenv"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/smtp"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type BudgetClient struct {
|
|
baseUrl string
|
|
apiKey string
|
|
syncId string
|
|
fullUrl string
|
|
}
|
|
|
|
type BudgetMonthsResponse struct {
|
|
Data struct {
|
|
Month string `json:"month"`
|
|
IncomeAvailable int `json:"incomeAvailable"`
|
|
LastMonthOverspent int `json:"lastMonthOverspent"`
|
|
ForNextMonth int `json:"forNextMonth"`
|
|
TotalBudgeted int `json:"totalBudgeted"`
|
|
ToBudget int `json:"toBudget"`
|
|
FromLastMonth int `json:"fromLastMonth"`
|
|
TotalIncome int `json:"totalIncome"`
|
|
TotalSpent int `json:"totalSpent"`
|
|
TotalBalance int `json:"totalBalance"`
|
|
CategoryGroups []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
IsIncome bool `json:"is_income"`
|
|
Hidden bool `json:"hidden"`
|
|
Budgeted int `json:"budgeted"`
|
|
Spent int `json:"spent"`
|
|
Balance int `json:"balance"`
|
|
Categories []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
IsIncome bool `json:"is_income"`
|
|
Hidden bool `json:"hidden"`
|
|
GroupID string `json:"group_id"`
|
|
Budgeted int `json:"budgeted"`
|
|
Spent int `json:"spent"`
|
|
Balance int `json:"balance"`
|
|
Carryover bool `json:"carryover"`
|
|
} `json:"categories"`
|
|
} `json:"categoryGroups"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
func CreateBudgetClient(baseUrl string, apiKey string, syncId string) *BudgetClient {
|
|
fullUrl := baseUrl + "/v1/budgets/" + syncId
|
|
client := BudgetClient{baseUrl, apiKey, syncId, fullUrl}
|
|
return &client
|
|
}
|
|
|
|
// Make a call to the actualbudget API
|
|
//
|
|
// method is POST, GET, ect
|
|
// route is the route to call with a leading / ex. /accounts/banksync
|
|
// headers is a map of strings expecting "header" "value"
|
|
func (b BudgetClient) callApi(method string, route string, headers map[string]string) *http.Response {
|
|
var httpClient http.Client
|
|
req, err := http.NewRequest(method, b.fullUrl+route, http.NoBody)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
req.Header.Add("x-api-key", b.apiKey)
|
|
for header, value := range headers {
|
|
req.Header.Add(header, value)
|
|
}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// Triggers a bank sync, returns true if successful
|
|
func (b BudgetClient) BankSync() bool {
|
|
resp := b.callApi("POST", "/accounts/banksync", nil)
|
|
return resp.StatusCode == http.StatusOK
|
|
}
|
|
|
|
func (b BudgetClient) GetBudgetMonths() *BudgetMonthsResponse {
|
|
currentTime := time.Now()
|
|
year, month := currentTime.Year(), int(currentTime.Month())
|
|
budgetMonth := fmt.Sprintf("%v-%v", year, month)
|
|
resp := b.callApi("GET", "/months/"+budgetMonth, nil)
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Fatal("GetBudgetAmounts failed with: " + string(resp.Status))
|
|
}
|
|
var data bytes.Buffer
|
|
|
|
_, err := io.Copy(&data, resp.Body)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
|
|
var budgetMonths BudgetMonthsResponse
|
|
err = json.Unmarshal(data.Bytes(), &budgetMonths)
|
|
return &budgetMonths
|
|
|
|
}
|
|
|
|
func main() {
|
|
if os.Getenv("ENVIRONMENT") == "dev" {
|
|
err := godotenv.Load()
|
|
if err != nil {
|
|
log.Fatal("Error loading .env file")
|
|
}
|
|
}
|
|
var baseUrl = os.Getenv("BASE_URL")
|
|
var apiKey = os.Getenv("API_KEY")
|
|
var syncId = os.Getenv("SYNC_ID")
|
|
var smtpUsername = os.Getenv("SMTP_USERNAME")
|
|
var smtpPassword = os.Getenv("SMTP_PASSWORD")
|
|
var smtpHost = os.Getenv("SMTP_HOST")
|
|
var smtpRecipients = os.Getenv("SMTP_RECIPIENTS")
|
|
client := CreateBudgetClient(baseUrl, apiKey, syncId)
|
|
//if !client.BankSync() {
|
|
// log.Println("Bank Sync failed, information may not be up to date")
|
|
//}
|
|
budgetMonths := client.GetBudgetMonths()
|
|
|
|
subject := "Subject: Budget Report\n"
|
|
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
|
|
const bodyTemplate = `
|
|
<p>Good day,</p>
|
|
<p>Your unallocated budget total as of %s: <b>$%v</b></p>
|
|
|
|
<p>Category breakdown:</p>
|
|
`
|
|
var categories strings.Builder
|
|
|
|
for _, v := range budgetMonths.Data.CategoryGroups {
|
|
for _, v2 := range v.Categories {
|
|
if !v2.IsIncome {
|
|
bal := float32(v2.Balance)
|
|
bal /= 100
|
|
fmt.Fprintf(&categories, "<p>%v: <b>$%v</b></p>", v2.Name, bal)
|
|
}
|
|
}
|
|
}
|
|
categories.Write([]byte("<p>This is an automated report generated by <a href=\"https://git.wanderingcrow.net/TheWanderingCrow/actualbudget-report\">actualbudget-report</a>"))
|
|
|
|
currentTime := time.Now()
|
|
toBudget := float32(budgetMonths.Data.ToBudget)
|
|
toBudget /= 100
|
|
body := fmt.Sprintf(bodyTemplate, currentTime.Format(time.RFC850), toBudget)
|
|
message := []byte(subject + mime + body + categories.String())
|
|
|
|
var auth smtp.Auth
|
|
if os.Getenv("ENVIRONMENT") == "dev" {
|
|
auth = nil
|
|
} else {
|
|
auth = smtp.PlainAuth("", smtpUsername, smtpPassword, smtpHost)
|
|
}
|
|
err := smtp.SendMail(smtpHost, auth, smtpUsername, []string{smtpRecipients}, []byte(message))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
}
|