123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- package gersemi
- import (
- "bytes"
- "encoding/json"
- "errors"
- "fmt"
- "log"
- "net/http"
- "regexp"
- "strings"
- "time"
- "apiote.xyz/p/asgard/idavollr"
- "apiote.xyz/p/asgard/jotunheim"
- "apiote.xyz/p/gott/v2"
- _ "github.com/emersion/go-message/charset"
- )
- type InvalidMessageError struct{}
- func (InvalidMessageError) Error() string {
- return "message does not match withdrawal or deposit regex"
- }
- type Transaction interface {
- IsTransaction()
- }
- type TransactionData struct {
- Type string `json:"type"`
- Date string `json:"date"` //yyyy-mm-ddT00:00:00+00:00
- Amount string `json:"amount"`
- Description string `json:"description"`
- }
- type Withdrawal struct {
- TransactionData
- SourceID string `json:"source_id"`
- DestinationName string `json:"destination_name"`
- }
- func (w Withdrawal) IsTransaction() {}
- type Deposit struct {
- TransactionData
- SourceName string `json:"source_name"`
- DestinationID string `json:"destination_id"`
- }
- func (w Deposit) IsTransaction() {}
- type GersemiRequestBody struct {
- Transactions []Transaction `json:"transactions"`
- }
- type GersemiMailbox struct {
- idavollr.Mailbox
- hc *http.Client
- withdrawalRegexes []*regexp.Regexp
- depositRegexes []*regexp.Regexp
- }
- type GersemiImapMessage struct {
- idavollr.ImapMessage
- hc *http.Client
- withdrawalRegexes []*regexp.Regexp
- depositRegexes []*regexp.Regexp
- config jotunheim.Config
- src, dst string
- title string
- amount string
- day, month, year string
- requestBody GersemiRequestBody
- requestBodyBytes []byte
- request *http.Request
- response *http.Response
- }
- func Gersemi(config jotunheim.Config) error {
- timeout, _ := time.ParseDuration("60s")
- mailbox := &GersemiMailbox{
- Mailbox: idavollr.Mailbox{
- MboxName: config.Gersemi.ImapInbox,
- ImapAdr: config.Gersemi.ImapAddress,
- ImapUser: config.Gersemi.ImapUsername,
- ImapPass: config.Gersemi.ImapPassword,
- Conf: config,
- },
- hc: &http.Client{
- Timeout: timeout,
- },
- }
- mailbox.SetupChannels()
- r := gott.R[idavollr.AbstractMailbox]{
- S: mailbox,
- }.
- Bind(prepareRegexes).
- Bind(idavollr.Connect).
- Tee(idavollr.Login).
- Bind(idavollr.SelectInbox).
- Tee(idavollr.CheckEmptyBox).
- Map(idavollr.FetchMessages).
- Tee(createTransactions).
- Tee(idavollr.CheckFetchError).
- Recover(idavollr.IgnoreEmptyBox).
- Recover(idavollr.Disconnect)
- return r.E
- }
- func prepareRegexes(am idavollr.AbstractMailbox) (idavollr.AbstractMailbox, error) {
- m := am.(*GersemiMailbox)
- for i, wr := range m.Config().Gersemi.WithdrawalRegexes {
- re, err := regexp.Compile(wr)
- if err != nil {
- return m, fmt.Errorf("while compiling withdrawal regex %d: %w", i, err)
- }
- m.withdrawalRegexes = append(m.withdrawalRegexes, re)
- }
- for i, dr := range m.Config().Gersemi.DepositRegexes {
- re, err := regexp.Compile(dr)
- if err != nil {
- return m, fmt.Errorf("while compiling deposit regex %d: %w", i, err)
- }
- m.depositRegexes = append(m.depositRegexes, re)
- }
- return m, nil
- }
- func createTransactions(am idavollr.AbstractMailbox) error {
- m := am.(*GersemiMailbox)
- for msg := range m.Messages() {
- imapMessage := idavollr.ImapMessage{
- Msg: msg,
- Sect: m.Section(),
- Mimetype: m.Config().Gersemi.MessageMime,
- }
- imapMessage.SetClient(m.Client())
- r := gott.R[idavollr.AbstractImapMessage]{
- S: &GersemiImapMessage{
- ImapMessage: imapMessage,
- config: m.Config(),
- withdrawalRegexes: m.withdrawalRegexes,
- depositRegexes: m.depositRegexes,
- hc: m.hc,
- },
- }.
- Bind(idavollr.ReadMessageBody).
- Bind(idavollr.ParseMimeMessage).
- Bind(idavollr.GetBody).
- Bind(idavollr.ReadBody).
- Bind(createTransaction).
- Bind(marshalBody).
- Bind(createRequest).
- Bind(doRequest).
- Bind(handleHttpError).
- Tee(moveMessage).
- Recover(ignoreInvalidMessage).
- Recover(idavollr.RecoverMalformedMessage).
- Recover(idavollr.RecoverErroredMessages)
- if r.E != nil {
- log.Printf("while processing message %s: %v", msg.Envelope.Subject, r.E)
- }
- }
- return nil
- }
- func matchRegex(m *GersemiImapMessage, match []string, groupNames []string) *GersemiImapMessage {
- for groupIdx, group := range match {
- names := groupNames[groupIdx]
- for _, name := range strings.Split(names, "_") {
- switch name {
- case "TITLE":
- m.title = regexp.MustCompile(" +").ReplaceAllString(group, " ")
- case "SRC":
- m.src = group
- case "DST":
- m.dst = regexp.MustCompile(" +").ReplaceAllString(group, " ")
- case "AMOUNTC":
- m.amount = group
- m.amount = strings.Replace(m.amount, ",", ".", -1)
- case "AMOUNT":
- m.amount = group
- case "DAY":
- m.day = group
- case "MONTH":
- m.month = group
- case "YEAR":
- m.year = group
- }
- }
- }
- return m
- }
- func createWithdrawal(m *GersemiImapMessage) *GersemiImapMessage {
- for _, regex := range m.withdrawalRegexes {
- groupNames := regex.SubexpNames()
- matches := regex.FindAllStringSubmatch(string(m.MessageBody()), -1)
- if matches == nil {
- continue
- }
- match := matches[0]
- m = matchRegex(m, match, groupNames)
- transaction := Withdrawal{
- TransactionData: TransactionData{
- Type: "withdrawal",
- Date: m.year + "-" + m.month + "-" + m.day + "T06:00:00+00:00",
- Amount: m.amount,
- Description: m.title,
- },
- SourceID: m.config.Gersemi.Accounts[m.src],
- DestinationName: m.dst,
- }
- body := GersemiRequestBody{
- Transactions: []Transaction{transaction},
- }
- m.requestBody = body
- return m
- }
- return nil
- }
- func createDeposit(m *GersemiImapMessage) *GersemiImapMessage {
- for _, regex := range m.depositRegexes {
- groupNames := regex.SubexpNames()
- matches := regex.FindAllStringSubmatch(string(m.MessageBody()), -1)
- if matches == nil {
- continue
- }
- match := matches[0]
- m = matchRegex(m, match, groupNames)
- transaction := Deposit{
- TransactionData: TransactionData{
- Type: "deposit",
- Date: m.year + "-" + m.month + "-" + m.day + "T06:00:00+00:00",
- Amount: m.amount,
- Description: m.title,
- },
- SourceName: m.src,
- DestinationID: m.config.Gersemi.Accounts[m.dst],
- }
- body := GersemiRequestBody{
- Transactions: []Transaction{transaction},
- }
- m.requestBody = body
- return m
- }
- return nil
- }
- func createTransaction(am idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- m := am.(*GersemiImapMessage)
- m.src = m.config.Gersemi.DefaultSource
- result := createWithdrawal(m)
- if result != nil {
- return result, nil
- }
- result = createDeposit(m)
- if result != nil {
- return result, nil
- }
- return m, InvalidMessageError{}
- }
- func marshalBody(am idavollr.AbstractImapMessage) (_ idavollr.AbstractImapMessage, err error) {
- m := am.(*GersemiImapMessage)
- m.requestBodyBytes, err = json.Marshal(m.requestBody)
- return m, err
- }
- func createRequest(am idavollr.AbstractImapMessage) (_ idavollr.AbstractImapMessage, err error) {
- m := am.(*GersemiImapMessage)
- m.request, err = http.NewRequest("POST", m.config.Gersemi.Firefly+"/api/v1/transactions", bytes.NewReader(m.requestBodyBytes))
- m.request.Header.Add("Authorization", "Bearer "+m.config.Gersemi.FireflyToken)
- m.request.Header.Add("Accept", "application/vnd.api+json")
- m.request.Header.Add("Content-Type", "application/json")
- return m, err
- }
- func doRequest(am idavollr.AbstractImapMessage) (_ idavollr.AbstractImapMessage, err error) {
- m := am.(*GersemiImapMessage)
- m.response, err = m.hc.Do(m.request)
- return m, err
- }
- func handleHttpError(am idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
- m := am.(*GersemiImapMessage)
- if m.response.StatusCode != 200 {
- return m, fmt.Errorf(m.response.Status)
- }
- return m, nil
- }
- func moveMessage(am idavollr.AbstractImapMessage) error { // TODO collect messages out of loop and move all
- m := am.(*GersemiImapMessage)
- return idavollr.MoveMsg(m.Client(), m.Msg, m.config.Gersemi.DoneFolder)
- }
- func ignoreInvalidMessage(s idavollr.AbstractImapMessage, e error) (idavollr.AbstractImapMessage, error) {
- var invalidMessageErr InvalidMessageError
- if errors.As(e, &invalidMessageErr) {
- log.Println(e.Error())
- return s, nil
- }
- return s, e
- }
|