Compare commits

..

15 Commits

55 changed files with 595 additions and 2565 deletions

View File

@ -141,7 +141,7 @@ steps:
commands:
- export "GOROOT=$(go env GOROOT)"
- apk --no-cache add build-base git
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.1
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.51.2
- ./mage-static check:all
when:
event: [ push, tag, pull_request ]
@ -506,7 +506,7 @@ steps:
# Build os packages and push it to our bucket
- name: build-os-packages-unstable
image: goreleaser/nfpm:v2.27.1
image: goreleaser/nfpm:v2.26.0
pull: always
commands:
- apk add git go
@ -522,7 +522,7 @@ steps:
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm:v2.27.1
image: goreleaser/nfpm:v2.26.0
pull: always
commands:
- apk add git go
@ -731,6 +731,6 @@ steps:
- failure
---
kind: signature
hmac: 166caa5ba66cd55bc0f1c5cb42be0a0a647fbadf66716778cf795fc084fc80fd
hmac: 7242860ad70556ffeb8fc804ce0ffa0d3d1aa8e0d9167ad476aa392d7e937d48
...

View File

@ -79,7 +79,6 @@ issues:
- path: pkg/routes/api/v1/docs.go
linters:
- goheader
- misspell
- text: "Missed string"
linters:
- goheader

View File

@ -3,7 +3,7 @@
# │─││ │││ │ │
# ┘─┘┘─┘┘┘─┘┘─┘
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.20.x AS builder
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.20.0 AS builder
RUN go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin

View File

@ -168,10 +168,6 @@ log:
events: "off"
# The log level for event log messages. Possible values (case-insensitive) are ERROR, INFO, DEBUG.
eventslevel: "info"
# Whether or not to log mail log messages. This will not log mail contents. Possible values are stdout, stderr, file or off to disable mail-related logging.
mail: "off"
# The log level for mail log messages. Possible values (case-insensitive) are ERROR, WARNING, INFO, DEBUG.
maillevel: "info"
ratelimit:
# whether or not to enable the rate limit

View File

@ -871,28 +871,6 @@ Full path: `log.eventslevel`
Environment path: `VIKUNJA_LOG_EVENTSLEVEL`
### mail
Whether or not to log mail log messages. This will not log mail contents. Possible values are stdout, stderr, file or off to disable mail-related logging.
Default: `off`
Full path: `log.mail`
Environment path: `VIKUNJA_LOG_MAIL`
### maillevel
The log level for mail log messages. Possible values (case-insensitive) are ERROR, WARNING, INFO, DEBUG.
Default: `info`
Full path: `log.maillevel`
Environment path: `VIKUNJA_LOG_MAILLEVEL`
---
## ratelimit

View File

@ -39,31 +39,30 @@ Vikunja currently supports the following properties:
* `PRIORITY`
* `CATEGORIES`
* `COMPLETED`
* `CREATED` (only Vikunja -> Client)
* `DUE`
* `DURATION`
* `DTSTAMP`
* `DTSTART`
* `LAST-MODIFIED` (only Vikunja -> Client)
* `RRULE` (Recurrence) (only Vikunja -> Client)
* `VALARM` (Reminders)
* `DURATION`
* `ORGANIZER`
* `RELATED-TO`
* `CREATED`
* `DTSTAMP`
* `LAST-MODIFIED`
* Recurrence
Vikunja **currently does not** support these properties:
* `ATTACH`
* `CLASS`
* `COMMENT`
* `CONTACT`
* `GEO`
* `LOCATION`
* `ORGANIZER` (disabled)
* `PERCENT-COMPLETE`
* `RECURRENCE-ID`
* `RELATED-TO`
* `RESOURCES`
* `SEQUENCE`
* `STATUS`
* `CONTACT`
* `RECURRENCE-ID`
* `URL`
* `SEQUENCE`
## Tested Clients

View File

@ -90,7 +90,6 @@ This document describes the different errors Vikunja can return.
| 4019 | 400 | Invalid task filter value. |
| 4020 | 400 | The provided attachment does not belong to that task. |
| 4021 | 400 | This user is already assigned to that task. |
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
## Namespace

18
go.mod
View File

@ -21,7 +21,7 @@ require (
gitea.com/xorm/xorm-redis-cache v0.2.0
github.com/ThreeDotsLabs/watermill v1.2.0
github.com/adlio/trello v1.10.0
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
@ -31,15 +31,15 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
github.com/gabriel-vasile/mimetype v1.4.2
github.com/getsentry/sentry-go v0.20.0
github.com/getsentry/sentry-go v0.19.0
github.com/go-sql-driver/mysql v1.7.0
github.com/go-testfixtures/testfixtures/v3 v3.8.1
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.3.0
github.com/iancoleman/strcase v0.2.0
github.com/imdario/mergo v0.3.15
github.com/imdario/mergo v0.3.13
github.com/jinzhu/copier v0.3.5
github.com/labstack/echo-jwt/v4 v4.1.0
github.com/labstack/echo/v4 v4.10.2
@ -58,11 +58,11 @@ require (
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.2
github.com/swaggo/swag v1.8.12
github.com/swaggo/swag v1.8.10
github.com/tkuchiki/go-timezone v0.2.2
github.com/ulule/limiter/v3 v3.11.1
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae
github.com/wneessen/go-mail v0.3.9
github.com/wneessen/go-mail v0.3.8
github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.7.0
golang.org/x/image v0.6.0
@ -138,13 +138,13 @@ require (
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.29.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

22
go.sum
View File

@ -78,8 +78,6 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0 h1:VVPogIxPiZ6WK5G4Pve5VSQ4HEFiJ8GChpqRjo1gN2c=
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc h1:up1aDcTCZ3KrL2ukKxNqjMRx/CCaXyn9Wl6N7ea3EWc=
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
@ -181,8 +179,6 @@ github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkN
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
github.com/getsentry/sentry-go v0.19.0 h1:BcCH3CN5tXt5aML+gwmbFwVptLLQA+eT866fCO9wVOM=
github.com/getsentry/sentry-go v0.19.0/go.mod h1:y3+lGEFEFexZtpbG1GUE2WD/f9zGyKYwpEqryTOC/nE=
github.com/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhjHmQaQ=
github.com/getsentry/sentry-go v0.20.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
@ -221,8 +217,6 @@ github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWl
github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA=
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a h1:/5o1ejt5M0fNAN2lU1NBLtPzUSZru689EWJq01ptr+E=
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479 h1:KaCpc4e48emF9hYmMB9INyfpGJHAZxEAS9EqWFkpTig=
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@ -354,10 +348,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo=
github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@ -694,10 +684,6 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/swaggo/swag v1.8.11 h1:Fp1dNNtDvbCf+8kvehZbHQnlF6AxHGjmw6H/xAMrZfY=
github.com/swaggo/swag v1.8.11/go.mod h1:2GXgpNI9iy5OdsYWu8zXfRAGnOAPxYxTWTyM0XOTYZQ=
github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w=
github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=
@ -725,8 +711,6 @@ github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
github.com/wneessen/go-mail v0.3.8 h1:ja5D/o/RVwrtRIYFlrO7GmtcjDNeMakGQuwQRZYv0JM=
github.com/wneessen/go-mail v0.3.8/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E=
github.com/wneessen/go-mail v0.3.9 h1:Q4DbCk3htT5DtDWKeMgNXCiHc4bBY/vv/XQPT6XDXzc=
github.com/wneessen/go-mail v0.3.9/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -821,8 +805,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1056,8 +1038,6 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1168,8 +1148,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -405,7 +405,7 @@ func checkGolangCiLintInstalled() {
mg.Deps(initVars)
if err := exec.Command("golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
fmt.Println("Please manually install golangci-lint by running")
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.1")
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.51.2")
os.Exit(1)
}
}

View File

@ -31,6 +31,19 @@ import (
// DateFormat is the caldav date format
const DateFormat = `20060102T150405`
// Event holds a single caldav event
type Event struct {
Summary string
Description string
UID string
Alarms []Alarm
Color string
Timestamp time.Time
Start time.Time
End time.Time
}
// Todo holds a single VTODO
type Todo struct {
// Required
@ -52,7 +65,6 @@ type Todo struct {
Duration time.Duration
RepeatAfter int64
RepeatMode models.TaskRepeatMode
Alarms []Alarm
Created time.Time
Updated time.Time // last-mod
@ -61,8 +73,6 @@ type Todo struct {
// Alarm holds infos about an alarm from a caldav event
type Alarm struct {
Time time.Time
Duration time.Duration
RelativeTo models.ReminderRelation
Description string
}
@ -90,6 +100,58 @@ X-OUTLOOK-COLOR:` + color + `
X-FUNAMBOL-COLOR:` + color
}
// ParseEvents parses an array of caldav events and gives them back as string
func ParseEvents(config *Config, events []*Event) (caldavevents string) {
caldavevents += `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:` + config.Name + `
PRODID:-//` + config.ProdID + `//EN` + getCaldavColor(config.Color)
for _, e := range events {
if e.UID == "" {
e.UID = makeCalDavTimeFromTimeStamp(e.Timestamp) + utils.Sha256(e.Summary)
}
formattedDescription := ""
if e.Description != "" {
re := regexp.MustCompile(`\r?\n`)
formattedDescription = re.ReplaceAllString(e.Description, "\\n")
}
caldavevents += `
BEGIN:VEVENT
UID:` + e.UID + `
SUMMARY:` + e.Summary + getCaldavColor(e.Color) + `
DESCRIPTION:` + formattedDescription + `
DTSTAMP:` + makeCalDavTimeFromTimeStamp(e.Timestamp) + `
DTSTART:` + makeCalDavTimeFromTimeStamp(e.Start) + `
DTEND:` + makeCalDavTimeFromTimeStamp(e.End)
for _, a := range e.Alarms {
if a.Description == "" {
a.Description = e.Summary
}
caldavevents += `
BEGIN:VALARM
TRIGGER:` + calcAlarmDateFromReminder(e.Start, a.Time) + `
ACTION:DISPLAY
DESCRIPTION:` + a.Description + `
END:VALARM`
}
caldavevents += `
END:VEVENT`
}
caldavevents += `
END:VCALENDAR` // Need a line break
return
}
func formatDuration(duration time.Duration) string {
seconds := duration.Seconds() - duration.Minutes()*60
minutes := duration.Minutes() - duration.Hours()*60
@ -184,7 +246,7 @@ CATEGORIES:` + strings.Join(t.Categories, ",")
caldavtodos += `
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
caldavtodos += ParseAlarms(t.Alarms, t.Summary)
caldavtodos += `
END:VTODO`
}
@ -195,42 +257,19 @@ END:VCALENDAR` // Need a line break
return
}
func ParseAlarms(alarms []Alarm, taskDescription string) (caldavalarms string) {
for _, a := range alarms {
if a.Description == "" {
a.Description = taskDescription
}
caldavalarms += `
BEGIN:VALARM`
switch a.RelativeTo {
case models.ReminderRelationStartDate:
caldavalarms += `
TRIGGER;RELATED=START:` + makeCalDavDuration(a.Duration)
case models.ReminderRelationEndDate, models.ReminderRelationDueDate:
caldavalarms += `
TRIGGER;RELATED=END:` + makeCalDavDuration(a.Duration)
default:
caldavalarms += `
TRIGGER;VALUE=DATE-TIME:` + makeCalDavTimeFromTimeStamp(a.Time)
}
caldavalarms += `
ACTION:DISPLAY
DESCRIPTION:` + a.Description + `
END:VALARM`
}
return caldavalarms
}
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
return ts.In(time.UTC).Format(DateFormat) + "Z"
}
func makeCalDavDuration(duration time.Duration) (caldavtime string) {
if duration < 0 {
duration = duration.Abs()
caldavtime = "-"
func calcAlarmDateFromReminder(eventStart, reminder time.Time) (alarmTime string) {
diff := reminder.Sub(eventStart)
diffStr := strings.ToUpper(diff.String())
if diff < 0 {
alarmTime += `-`
// We append the - at the beginning of the caldav flag, that would get in the way if the minutes
// themselves are also containing it
diffStr = diffStr[1:]
}
caldavtime += "PT" + strings.ToUpper(duration.Truncate(time.Millisecond).String())
alarmTime += `PT` + diffStr
return
}

View File

@ -26,6 +26,275 @@ import (
"github.com/stretchr/testify/assert"
)
func TestParseEvents(t *testing.T) {
type args struct {
config *Config
events []*Event
}
tests := []struct {
name string
args args
wantCaldavevents string
}{
{
name: "Test caldavparsing without reminders",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
Color: "ffffff",
},
events: []*Event{
{
Summary: "Event #1",
Description: "Lorem Ipsum",
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
Color: "affffe",
},
{
Summary: "Event #2",
UID: "randommduidd",
Timestamp: time.Unix(1543726724, 0).In(config.GetTimeZone()),
Start: time.Unix(1543726724, 0).In(config.GetTimeZone()),
End: time.Unix(1543738724, 0).In(config.GetTimeZone()),
},
{
Summary: "Event #3 with empty uid",
UID: "20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83",
Timestamp: time.Unix(1543726824, 0).In(config.GetTimeZone()),
Start: time.Unix(1543726824, 0).In(config.GetTimeZone()),
End: time.Unix(1543727000, 0).In(config.GetTimeZone()),
},
},
},
wantCaldavevents: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
X-APPLE-CALENDAR-COLOR:#ffffffFF
X-OUTLOOK-COLOR:#ffffffFF
X-FUNAMBOL-COLOR:#ffffffFF
BEGIN:VEVENT
UID:randommduid
SUMMARY:Event #1
X-APPLE-CALENDAR-COLOR:#affffeFF
X-OUTLOOK-COLOR:#affffeFF
X-FUNAMBOL-COLOR:#affffeFF
DESCRIPTION:Lorem Ipsum
DTSTAMP:20181201T011204Z
DTSTART:20181201T011204Z
DTEND:20181201T013024Z
END:VEVENT
BEGIN:VEVENT
UID:randommduidd
SUMMARY:Event #2
DESCRIPTION:
DTSTAMP:20181202T045844Z
DTSTART:20181202T045844Z
DTEND:20181202T081844Z
END:VEVENT
BEGIN:VEVENT
UID:20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
SUMMARY:Event #3 with empty uid
DESCRIPTION:
DTSTAMP:20181202T050024Z
DTSTART:20181202T050024Z
DTEND:20181202T050320Z
END:VEVENT
END:VCALENDAR`,
},
{
name: "Test caldavparsing with reminders",
args: args{
config: &Config{
Name: "test2",
ProdID: "RandomProdID which is not random",
},
events: []*Event{
{
Summary: "Event #1",
Description: "Lorem Ipsum",
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
Alarms: []Alarm{
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
{Time: time.Unix(1543626024, 0)},
},
},
{
Summary: "Event #2",
UID: "randommduidd",
Timestamp: time.Unix(1543726724, 0).In(config.GetTimeZone()),
Start: time.Unix(1543726724, 0).In(config.GetTimeZone()),
End: time.Unix(1543738724, 0).In(config.GetTimeZone()),
Alarms: []Alarm{
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
{Time: time.Unix(1543626024, 0).In(config.GetTimeZone())},
},
},
{
Summary: "Event #3 with empty uid",
Timestamp: time.Unix(1543726824, 0).In(config.GetTimeZone()),
Start: time.Unix(1543726824, 0).In(config.GetTimeZone()),
End: time.Unix(1543727000, 0).In(config.GetTimeZone()),
Alarms: []Alarm{
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
{Time: time.Unix(1543626024, 0).In(config.GetTimeZone())},
{Time: time.Unix(1543826824, 0).In(config.GetTimeZone())},
},
},
{
Summary: "Event #4 without any",
Timestamp: time.Unix(1543726824, 0),
Start: time.Unix(1543726824, 0),
End: time.Unix(1543727000, 0),
},
},
},
wantCaldavevents: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test2
PRODID:-//RandomProdID which is not random//EN
BEGIN:VEVENT
UID:randommduid
SUMMARY:Event #1
DESCRIPTION:Lorem Ipsum
DTSTAMP:20181201T011204Z
DTSTART:20181201T011204Z
DTEND:20181201T013024Z
BEGIN:VALARM
TRIGGER:-PT3M20S
ACTION:DISPLAY
DESCRIPTION:Event #1
END:VALARM
BEGIN:VALARM
TRIGGER:-PT8M20S
ACTION:DISPLAY
DESCRIPTION:Event #1
END:VALARM
BEGIN:VALARM
TRIGGER:-PT11M40S
ACTION:DISPLAY
DESCRIPTION:Event #1
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:randommduidd
SUMMARY:Event #2
DESCRIPTION:
DTSTAMP:20181202T045844Z
DTSTART:20181202T045844Z
DTEND:20181202T081844Z
BEGIN:VALARM
TRIGGER:-PT27H50M0S
ACTION:DISPLAY
DESCRIPTION:Event #2
END:VALARM
BEGIN:VALARM
TRIGGER:-PT27H55M0S
ACTION:DISPLAY
DESCRIPTION:Event #2
END:VALARM
BEGIN:VALARM
TRIGGER:-PT27H58M20S
ACTION:DISPLAY
DESCRIPTION:Event #2
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:20181202T050024Z2aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
SUMMARY:Event #3 with empty uid
DESCRIPTION:
DTSTAMP:20181202T050024Z
DTSTART:20181202T050024Z
DTEND:20181202T050320Z
BEGIN:VALARM
TRIGGER:-PT27H51M40S
ACTION:DISPLAY
DESCRIPTION:Event #3 with empty uid
END:VALARM
BEGIN:VALARM
TRIGGER:-PT27H56M40S
ACTION:DISPLAY
DESCRIPTION:Event #3 with empty uid
END:VALARM
BEGIN:VALARM
TRIGGER:-PT28H0M0S
ACTION:DISPLAY
DESCRIPTION:Event #3 with empty uid
END:VALARM
BEGIN:VALARM
TRIGGER:PT27H46M40S
ACTION:DISPLAY
DESCRIPTION:Event #3 with empty uid
END:VALARM
END:VEVENT
BEGIN:VEVENT
UID:20181202T050024Zae7548ce9556df85038abe90dc674d4741a61ce74d1cf
SUMMARY:Event #4 without any
DESCRIPTION:
DTSTAMP:20181202T050024Z
DTSTART:20181202T050024Z
DTEND:20181202T050320Z
END:VEVENT
END:VCALENDAR`,
},
{
name: "Test caldavparsing with multiline description",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
},
events: []*Event{
{
Summary: "Event #1",
Description: `Lorem Ipsum
Dolor sit amet`,
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
},
},
},
wantCaldavevents: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VEVENT
UID:randommduid
SUMMARY:Event #1
DESCRIPTION:Lorem Ipsum\nDolor sit amet
DTSTAMP:20181201T011204Z
DTSTART:20181201T011204Z
DTEND:20181201T013024Z
END:VEVENT
END:VCALENDAR`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotCaldavevents := ParseEvents(tt.args.config, tt.args.events)
assert.Equal(t, gotCaldavevents, tt.wantCaldavevents)
})
}
}
func TestParseTodos(t *testing.T) {
type args struct {
config *Config
@ -251,88 +520,13 @@ X-FUNAMBOL-COLOR:#affffeFF
CATEGORIES:label1,label2
LAST-MODIFIED:00010101T000000Z
END:VTODO
END:VCALENDAR`,
},
{
name: "with alarm",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
},
todos: []*Todo{
{
Summary: "Todo #1",
UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Alarms: []Alarm{
{
Time: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
{
Time: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Description: "alarm description",
},
{
Duration: -2 * time.Hour,
RelativeTo: "due_date",
},
{
Duration: 1 * time.Hour,
RelativeTo: "start_date",
},
{
Duration: time.Duration(0),
RelativeTo: "end_date",
},
},
},
},
},
wantCaldavtasks: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
LAST-MODIFIED:00010101T000000Z
BEGIN:VALARM
TRIGGER;VALUE=DATE-TIME:20181201T011204Z
ACTION:DISPLAY
DESCRIPTION:Todo #1
END:VALARM
BEGIN:VALARM
TRIGGER;VALUE=DATE-TIME:20181201T011204Z
ACTION:DISPLAY
DESCRIPTION:alarm description
END:VALARM
BEGIN:VALARM
TRIGGER;RELATED=END:-PT2H0M0S
ACTION:DISPLAY
DESCRIPTION:Todo #1
END:VALARM
BEGIN:VALARM
TRIGGER;RELATED=START:PT1H0M0S
ACTION:DISPLAY
DESCRIPTION:Todo #1
END:VALARM
BEGIN:VALARM
TRIGGER;RELATED=END:PT0S
ACTION:DISPLAY
DESCRIPTION:Todo #1
END:VALARM
END:VTODO
END:VCALENDAR`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotCaldavtasks := ParseTodos(tt.args.config, tt.args.todos)
assert.Equal(t, tt.wantCaldavtasks, gotCaldavtasks)
assert.Equal(t, gotCaldavtasks, tt.wantCaldavtasks)
})
}
}

View File

@ -17,15 +17,12 @@
package caldav
import (
"errors"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/utils"
ics "github.com/arran4/golang-ical"
)
@ -41,14 +38,6 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
for _, label := range t.Labels {
categories = append(categories, label.Title)
}
var alarms []Alarm
for _, reminder := range t.Reminders {
alarms = append(alarms, Alarm{
Time: reminder.Reminder,
Duration: time.Duration(reminder.RelativePeriod) * time.Second,
RelativeTo: reminder.RelativeTo,
})
}
caldavtodos = append(caldavtodos, &Todo{
Timestamp: t.Updated,
@ -67,7 +56,6 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
Duration: duration,
RepeatAfter: t.RepeatAfter,
RepeatMode: t.RepeatMode,
Alarms: alarms,
})
}
@ -84,20 +72,17 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
if err != nil {
return nil, err
}
vTodo, ok := parsed.Components[0].(*ics.VTodo)
if !ok {
return nil, errors.New("VTODO element not found")
}
// We put the vTodo details in a map to be able to handle them more easily
task := make(map[string]ics.IANAProperty)
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
task[c.IANAToken] = c
// We put the task details in a map to be able to handle them more easily
task := make(map[string]string)
for _, c := range parsed.Components[0].UnknownPropertiesIANAProperties() {
task[c.IANAToken] = c.Value
}
// Parse the priority
var priority int64
if _, ok := task["PRIORITY"]; ok {
priorityParsed, err := strconv.ParseInt(task["PRIORITY"].Value, 10, 64)
priorityParsed, err := strconv.ParseInt(task["PRIORITY"], 10, 64)
if err != nil {
return nil, err
}
@ -106,14 +91,14 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
}
// Parse the enddate
duration, _ := time.ParseDuration(task["DURATION"].Value)
duration, _ := time.ParseDuration(task["DURATION"])
description := strings.ReplaceAll(task["DESCRIPTION"].Value, "\\,", ",")
description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",")
description = strings.ReplaceAll(description, "\\n", "\n")
var labels []*models.Label
if val, ok := task["CATEGORIES"]; ok {
categories := strings.Split(val.Value, ",")
categories := strings.Split(val, ",")
labels = make([]*models.Label, 0, len(categories))
for _, category := range categories {
labels = append(labels, &models.Label{
@ -123,8 +108,8 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
}
vTask = &models.Task{
UID: task["UID"].Value,
Title: task["SUMMARY"].Value,
UID: task["UID"],
Title: task["SUMMARY"],
Description: description,
Priority: priority,
Labels: labels,
@ -134,7 +119,7 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
}
if task["STATUS"].Value == "COMPLETED" {
if task["STATUS"] == "COMPLETED" {
vTask.Done = true
}
@ -142,66 +127,11 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
vTask.EndDate = vTask.StartDate.Add(duration)
}
for _, vAlarm := range vTodo.SubComponents() {
if vAlarm, ok := vAlarm.(*ics.VAlarm); ok {
vTask = parseVAlarm(vAlarm, vTask)
}
}
return
}
func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) *models.Task {
for _, property := range vAlarm.UnknownPropertiesIANAProperties() {
if property.IANAToken != "TRIGGER" {
continue
}
if contains(property.ICalParameters["VALUE"], "DATE-TIME") {
// Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
Reminder: caldavTimeToTimestamp(property),
})
continue
}
duration := utils.ParseISO8601Duration(property.Value)
if contains(property.ICalParameters["RELATED"], "END") {
// Example: TRIGGER;RELATED=END:-P2D
if vTask.EndDate.IsZero() {
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
RelativePeriod: int64(duration.Seconds()),
RelativeTo: models.ReminderRelationDueDate})
} else {
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
RelativePeriod: int64(duration.Seconds()),
RelativeTo: models.ReminderRelationEndDate})
}
continue
}
// Example: TRIGGER;RELATED=START:-P2D
// Example: TRIGGER:-PT60M
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
RelativePeriod: int64(duration.Seconds()),
RelativeTo: models.ReminderRelationStartDate})
}
return vTask
}
func contains(array []string, str string) bool {
for _, value := range array {
if value == str {
return true
}
}
return false
}
// https://tools.ietf.org/html/rfc5545#section-3.3.5
func caldavTimeToTimestamp(ianaProperty ics.IANAProperty) time.Time {
tstring := ianaProperty.Value
func caldavTimeToTimestamp(tstring string) time.Time {
if tstring == "" {
return time.Time{}
}
@ -216,24 +146,7 @@ func caldavTimeToTimestamp(ianaProperty ics.IANAProperty) time.Time {
format = `20060102`
}
var t time.Time
var err error
tzParameter := ianaProperty.ICalParameters["TZID"]
if len(tzParameter) > 0 {
loc, err := time.LoadLocation(tzParameter[0])
if err != nil {
log.Warningf("Error while parsing caldav timezone %s: %s", tzParameter[0], err)
} else {
t, err = time.ParseInLocation(format, tstring, loc)
if err != nil {
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s at location %s", tstring, loc, err)
} else {
t = t.In(config.GetTimeZone())
return t
}
}
}
t, err = time.Parse(format, tstring)
t, err := time.Parse(format, tstring)
if err != nil {
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err)
return time.Time{}

View File

@ -118,177 +118,6 @@ END:VCALENDAR`,
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
{
name: "With alarm (time trigger)",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011204
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
BEGIN:VALARM
TRIGGER;VALUE=DATE-TIME:20181201T011210Z
ACTION:DISPLAY
END:VALARM
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "Todo #1",
UID: "randomuid",
Description: "Lorem Ipsum",
Reminders: []*models.TaskReminder{
{
Reminder: time.Date(2018, 12, 1, 1, 12, 10, 0, config.GetTimeZone()),
},
},
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
{
name: "With alarm (relative trigger)",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011204
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
DTSTART:20230228T170000Z
DUE:20230304T150000Z
BEGIN:VALARM
TRIGGER:PT0S
ACTION:DISPLAY
END:VALARM
BEGIN:VALARM
TRIGGER;VALUE=DURATION:-PT60M
ACTION:DISPLAY
END:VALARM
BEGIN:VALARM
TRIGGER:-PT61M
ACTION:DISPLAY
END:VALARM
BEGIN:VALARM
TRIGGER;RELATED=START:-P1D
ACTION:DISPLAY
END:VALARM
BEGIN:VALARM
TRIGGER;RELATED=END:-PT30M
ACTION:DISPLAY
END:VALARM
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "Todo #1",
UID: "randomuid",
Description: "Lorem Ipsum",
StartDate: time.Date(2023, 2, 28, 17, 0, 0, 0, config.GetTimeZone()),
DueDate: time.Date(2023, 3, 4, 15, 0, 0, 0, config.GetTimeZone()),
Reminders: []*models.TaskReminder{
{
RelativeTo: models.ReminderRelationStartDate,
RelativePeriod: 0,
},
{
RelativeTo: models.ReminderRelationStartDate,
RelativePeriod: -3600,
},
{
RelativeTo: models.ReminderRelationStartDate,
RelativePeriod: -3660,
},
{
RelativeTo: models.ReminderRelationStartDate,
RelativePeriod: -86400,
},
{
RelativeTo: models.ReminderRelationDueDate,
RelativePeriod: -1800,
},
},
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
{
name: "example task from tasks.org app",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-130102//EN
BEGIN:VTODO
DTSTAMP:20230402T074158Z
UID:4290517349243274514
CREATED:20230402T060451Z
LAST-MODIFIED:20230402T074154Z
SUMMARY:Test with tasks.org
PRIORITY:9
CATEGORIES:Vikunja
X-APPLE-SORT-ORDER:697384109
DUE;TZID=Europe/Berlin:20230402T170001
DTSTART;TZID=Europe/Berlin:20230401T090000
BEGIN:VALARM
TRIGGER;RELATED=END:PT0S
ACTION:DISPLAY
DESCRIPTION:Default Tasks.org description
END:VALARM
BEGIN:VALARM
TRIGGER;VALUE=DATE-TIME:20230402T100000Z
ACTION:DISPLAY
DESCRIPTION:Default Tasks.org description
END:VALARM
END:VTODO
BEGIN:VTIMEZONE
TZID:Europe/Berlin
LAST-MODIFIED:20220816T024022Z
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR`,
},
wantVTask: &models.Task{
Updated: time.Date(2023, 4, 2, 7, 41, 58, 0, config.GetTimeZone()),
UID: "4290517349243274514",
Title: "Test with tasks.org",
Priority: 1,
Labels: []*models.Label{
{
Title: "Vikunja",
},
},
DueDate: time.Date(2023, 4, 2, 15, 0, 1, 0, config.GetTimeZone()),
StartDate: time.Date(2023, 4, 1, 7, 0, 0, 0, config.GetTimeZone()),
Reminders: []*models.TaskReminder{
{
RelativeTo: models.ReminderRelationDueDate,
RelativePeriod: 0,
},
{
Reminder: time.Date(2023, 4, 2, 10, 0, 0, 0, config.GetTimeZone()),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -298,7 +127,7 @@ END:VCALENDAR`,
return
}
if diff, equal := messagediff.PrettyDiff(got, tt.wantVTask); !equal {
t.Errorf("ParseTaskFromVTODO()\n gotVTask = %v\n want %v\n diff = %s", got, tt.wantVTask, diff)
t.Errorf("ParseTaskFromVTODO() gotVTask = %v, want %v, diff = %s", got, tt.wantVTask, diff)
}
})
}
@ -346,16 +175,6 @@ func TestGetCaldavTodosForTasks(t *testing.T) {
Title: "label2",
},
},
Reminders: []*models.TaskReminder{
{
Reminder: time.Unix(1543626730, 0).In(config.GetTimeZone()),
},
{
Reminder: time.Unix(1543626731, 0).In(config.GetTimeZone()),
RelativePeriod: -3600,
RelativeTo: models.ReminderRelationDueDate,
},
},
},
},
},
@ -381,16 +200,6 @@ PRIORITY:3
RRULE:FREQ=SECONDLY;INTERVAL=86400
CATEGORIES:label1,label2
LAST-MODIFIED:20181201T011205Z
BEGIN:VALARM
TRIGGER;VALUE=DATE-TIME:20181201T011210Z
ACTION:DISPLAY
DESCRIPTION:Task 1
END:VALARM
BEGIN:VALARM
TRIGGER;RELATED=END:-PT1H0M0S
ACTION:DISPLAY
DESCRIPTION:Task 1
END:VALARM
END:VTODO
END:VCALENDAR`,
},

View File

@ -118,8 +118,6 @@ const (
LogPath Key = `log.path`
LogEvents Key = `log.events`
LogEventsLevel Key = `log.eventslevel`
LogMail Key = `log.mail`
LogMailLevel Key = `log.maillevel`
RateLimitEnabled Key = `ratelimit.enabled`
RateLimitKind Key = `ratelimit.kind`
@ -353,8 +351,6 @@ func InitDefaultConfig() {
LogPath.setDefault(ServiceRootpath.GetString() + "/logs")
LogEvents.setDefault("off")
LogEventsLevel.setDefault("INFO")
LogMail.setDefault("off")
LogMailLevel.setDefault("INFO")
// Rate Limit
RateLimitEnabled.setDefault(false)
RateLimitKind.setDefault("user")

View File

@ -12,7 +12,3 @@
task_id: 2
reminder: 2018-12-01 01:13:44
created: 2018-12-01 01:12:04
- id: 4
task_id: 39
reminder: 2023-03-04 15:00:00
created: 2018-12-01 01:12:04

View File

@ -37,10 +37,6 @@ SUMMARY:Caldav Task 1
CATEGORIES:tag1,tag2,tag3
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
BEGIN:VALARM
TRIGGER;VALUE=DATE-TIME:20230304T150000Z
ACTION:DISPLAY
END:VALARM
END:VTODO
END:VCALENDAR`
@ -69,9 +65,5 @@ func TestCaldav(t *testing.T) {
assert.Contains(t, rec.Body.String(), "DUE:20230301T150000Z")
assert.Contains(t, rec.Body.String(), "PRIORITY:3")
assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4")
assert.Contains(t, rec.Body.String(), "BEGIN:VALARM")
assert.Contains(t, rec.Body.String(), "TRIGGER;VALUE=DATE-TIME:20230304T150000Z")
assert.Contains(t, rec.Body.String(), "ACTION:DISPLAY")
assert.Contains(t, rec.Body.String(), "END:VALARM")
})
}

View File

@ -1,87 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package log
import (
"strings"
"time"
"code.vikunja.io/api/pkg/config"
"github.com/op/go-logging"
"xorm.io/xorm/log"
)
type MailLogger struct {
logger *logging.Logger
level log.LogLevel
}
const mailFormat = `%{color}%{time:` + time.RFC3339Nano + `}: %{level}` + "\t" + `▶ [MAIL] %{id:03x}%{color:reset} %{message}`
const mailLogModule = `vikunja_mail`
func NewMailLogger() *MailLogger {
lvl := strings.ToUpper(config.LogMailLevel.GetString())
level, err := logging.LogLevel(lvl)
if err != nil {
Criticalf("Error setting database log level: %s", err.Error())
}
mailLogger := &MailLogger{
logger: logging.MustGetLogger(mailLogModule),
}
logBackend := logging.NewLogBackend(GetLogWriter("mail"), "", 0)
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(mailFormat+"\n"))
backendLeveled := logging.AddModuleLevel(backend)
backendLeveled.SetLevel(level, mailLogModule)
mailLogger.logger.SetBackend(backendLeveled)
switch level {
case logging.CRITICAL:
case logging.ERROR:
mailLogger.level = log.LOG_ERR
case logging.WARNING:
mailLogger.level = log.LOG_WARNING
case logging.NOTICE:
case logging.INFO:
mailLogger.level = log.LOG_INFO
case logging.DEBUG:
mailLogger.level = log.LOG_DEBUG
default:
mailLogger.level = log.LOG_OFF
}
return mailLogger
}
func (m *MailLogger) Errorf(format string, v ...interface{}) {
m.logger.Errorf(format, v...)
}
func (m *MailLogger) Warnf(format string, v ...interface{}) {
m.logger.Warningf(format, v...)
}
func (m *MailLogger) Infof(format string, v ...interface{}) {
m.logger.Infof(format, v...)
}
func (m *MailLogger) Debugf(format string, v ...interface{}) {
m.logger.Debugf(format, v...)
}

View File

@ -23,6 +23,6 @@ import (
// NoopBackend doesn't log anything. Used in cases where we want to disable logging completely.
type NoopBackend struct{}
func (n *NoopBackend) Log(_ logging.Level, _ int, _ *logging.Record) error {
func (n *NoopBackend) Log(level logging.Level, i int, record *logging.Record) error {
return nil
}

View File

@ -91,6 +91,6 @@ func (w *WatermillLogger) Trace(msg string, fields watermill.LogFields) {
w.logger.Debugf("%s, %s", msg, concatFields(fields))
}
func (w *WatermillLogger) With(_ watermill.LogFields) watermill.LoggerAdapter {
func (w *WatermillLogger) With(fields watermill.LogFields) watermill.LoggerAdapter {
return w
}

View File

@ -56,8 +56,6 @@ func getClient() (*mail.Client, error) {
ServerName: config.MailerHost.GetString(),
}),
mail.WithTimeout((config.MailerQueueTimeout.GetDuration() + 3) * time.Second), // 3s more for us to close before mail server timeout
mail.WithLogger(log.NewMailLogger()),
mail.WithDebugLog(),
}
if config.MailerForceSSL.GetBool() {

View File

@ -109,7 +109,10 @@ func Rollback(migrationID string) {
// MigrateTo executes all migrations up to a certain point
func MigrateTo(migrationID string, x *xorm.Engine) error {
m := initMigration(x)
return m.MigrateTo(migrationID)
if err := m.MigrateTo(migrationID); err != nil {
return err
}
return nil
}
// Deletes a column from a table. All arguments are strings, to let them be standalone and not depending on any struct.

View File

@ -898,7 +898,7 @@ func (err ErrReminderRelativeToMissing) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeReminderRelativeToMissing,
Message: "Please provide what the reminder date is relative to",
Message: "Relative reminder without relative_to",
}
}

View File

@ -281,7 +281,7 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/buckets/{bucketID} [post]
func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
doneBucket, err := getDoneBucketForProject(s, b.ProjectID)
if err != nil {
return err
@ -320,7 +320,7 @@ func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/buckets/{bucketID} [delete]
func (b *Bucket) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
// Prevent removing the last bucket
total, err := s.Where("project_id = ?", b.ProjectID).Count(&Bucket{})

View File

@ -123,7 +123,7 @@ func (l *Label) Update(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [delete]
func (l *Label) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (l *Label) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.ID(l.ID).Delete(&Label{})
return err
}
@ -175,7 +175,7 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
// @Failure 404 {object} web.HTTPError "Label not found"
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [get]
func (l *Label) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
label, err := getLabelByIDSimple(s, l.ID)
if err != nil {
return

View File

@ -40,7 +40,7 @@ func (l *Label) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
// CanCreate checks if the user can create a label
// Currently a dummy.
func (l *Label) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
func (l *Label) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}

View File

@ -64,7 +64,7 @@ func (LabelTask) TableName() string {
// @Failure 404 {object} web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels/{label} [delete]
func (lt *LabelTask) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.Delete(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
return err
}
@ -84,7 +84,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The label does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [put]
func (lt *LabelTask) Create(s *xorm.Session, _ web.Auth) (err error) {
func (lt *LabelTask) Create(s *xorm.Session, a web.Auth) (err error) {
// Check if the label is already added
exists, err := s.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
if err != nil {
@ -118,7 +118,7 @@ func (lt *LabelTask) Create(s *xorm.Session, _ web.Auth) (err error) {
// @Success 200 {array} models.Label "The labels"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [get]
func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page int, _ int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user has the right to see the task
task := Task{ID: lt.TaskID}
canRead, _, err := task.CanRead(s, a)

View File

@ -169,7 +169,7 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{project}/shares/{share} [get]
func (share *LinkSharing) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
exists, err := s.Where("id = ?", share.ID).Get(share)
if err != nil {
return err
@ -269,7 +269,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
// @Failure 404 {object} web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{project}/shares/{share} [delete]
func (share *LinkSharing) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (share *LinkSharing) Delete(s *xorm.Session, a web.Auth) (err error) {
_, err = s.Where("id = ?", share.ID).Delete(share)
return
}

View File

@ -75,7 +75,7 @@ func (s *IncreaseTaskCounter) Name() string {
}
// Handle is executed when the event IncreaseTaskCounter listens on is fired
func (s *IncreaseTaskCounter) Handle(_ *message.Message) (err error) {
func (s *IncreaseTaskCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.TaskCountKey, 1)
}
@ -89,7 +89,7 @@ func (s *DecreaseTaskCounter) Name() string {
}
// Handle is executed when the event DecreaseTaskCounter listens on is fired
func (s *DecreaseTaskCounter) Handle(_ *message.Message) (err error) {
func (s *DecreaseTaskCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.TaskCountKey, 1)
}
@ -480,7 +480,7 @@ func (s *IncreaseProjectCounter) Name() string {
return "project.counter.increase"
}
func (s *IncreaseProjectCounter) Handle(_ *message.Message) (err error) {
func (s *IncreaseProjectCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.ProjectCountKey, 1)
}
@ -491,7 +491,7 @@ func (s *DecreaseProjectCounter) Name() string {
return "project.counter.decrease"
}
func (s *DecreaseProjectCounter) Handle(_ *message.Message) (err error) {
func (s *DecreaseProjectCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.ProjectCountKey, 1)
}
@ -553,7 +553,7 @@ func (s *IncreaseNamespaceCounter) Name() string {
}
// Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired
func (s *IncreaseNamespaceCounter) Handle(_ *message.Message) (err error) {
func (s *IncreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.NamespaceCountKey, 1)
}
@ -566,8 +566,8 @@ func (s *DecreaseNamespaceCounter) Name() string {
return "namespace.counter.decrease"
}
// Handle is executed when the event DecreaseNamespaceCounter listens on is fired
func (s *DecreaseNamespaceCounter) Handle(_ *message.Message) (err error) {
// Hanlde is executed when the event DecreaseNamespaceCounter listens on is fired
func (s *DecreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.NamespaceCountKey, 1)
}
@ -583,8 +583,8 @@ func (s *IncreaseTeamCounter) Name() string {
return "team.counter.increase"
}
// Handle is executed when the event IncreaseTeamCounter listens on is fired
func (s *IncreaseTeamCounter) Handle(_ *message.Message) (err error) {
// Hanlde is executed when the event IncreaseTeamCounter listens on is fired
func (s *IncreaseTeamCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.TeamCountKey, 1)
}
@ -597,8 +597,8 @@ func (s *DecreaseTeamCounter) Name() string {
return "team.counter.decrease"
}
// Handle is executed when the event DecreaseTeamCounter listens on is fired
func (s *DecreaseTeamCounter) Handle(_ *message.Message) (err error) {
// Hanlde is executed when the event DecreaseTeamCounter listens on is fired
func (s *DecreaseTeamCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.TeamCountKey, 1)
}

View File

@ -50,7 +50,7 @@ func (n *Namespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
}
// CanCreate checks if the user can create a new namespace
func (n *Namespace) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
func (n *Namespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}

View File

@ -124,7 +124,7 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
func (tn *TeamNamespace) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (tn *TeamNamespace) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the team exists
_, err = GetTeamByID(s, tn.TeamID)
@ -229,7 +229,7 @@ func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, pag
// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
func (tn *TeamNamespace) Update(s *xorm.Session, _ web.Auth) (err error) {
func (tn *TeamNamespace) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := tn.Right.isValid(); err != nil {

View File

@ -133,7 +133,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/users/{userID} [delete]
func (nu *NamespaceUser) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (nu *NamespaceUser) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the user exists
user, err := user2.GetUserByUsername(s, nu.Username)
@ -229,7 +229,7 @@ func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, pag
// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces/{namespaceID}/users/{userID} [post]
func (nu *NamespaceUser) Update(s *xorm.Session, _ web.Auth) (err error) {
func (nu *NamespaceUser) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := nu.Right.isValid(); err != nil {

View File

@ -47,7 +47,7 @@ type DatabaseNotifications struct {
// @Failure 403 {object} web.HTTPError "Link shares cannot have notifications."
// @Failure 500 {object} models.Message "Internal error"
// @Router /notifications [get]
func (d *DatabaseNotifications) ReadAll(s *xorm.Session, a web.Auth, _ string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) {
func (d *DatabaseNotifications) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) {
if _, is := a.(*LinkSharing); is {
return nil, 0, 0, ErrGenericForbidden{}
}
@ -79,6 +79,6 @@ func (d *DatabaseNotifications) CanUpdate(s *xorm.Session, a web.Auth) (bool, er
// @Failure 404 {object} web.HTTPError "The notification does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /notifications/{id} [post]
func (d *DatabaseNotifications) Update(s *xorm.Session, _ web.Auth) (err error) {
func (d *DatabaseNotifications) Update(s *xorm.Session, a web.Auth) (err error) {
return notifications.MarkNotificationAsRead(s, &d.DatabaseNotification, d.Read)
}

View File

@ -135,7 +135,7 @@ func (tl *TeamProject) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "Team or project does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/teams/{teamID} [delete]
func (tl *TeamProject) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (tl *TeamProject) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the team exists
_, err = GetTeamByID(s, tl.TeamID)
@ -247,7 +247,7 @@ func (tl *TeamProject) ReadAll(s *xorm.Session, a web.Auth, search string, page
// @Failure 404 {object} web.HTTPError "Team or project does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/teams/{teamID} [post]
func (tl *TeamProject) Update(s *xorm.Session, _ web.Auth) (err error) {
func (tl *TeamProject) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := tl.Right.isValid(); err != nil {

View File

@ -142,7 +142,7 @@ func (lu *ProjectUser) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "user or project does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/users/{userID} [delete]
func (lu *ProjectUser) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (lu *ProjectUser) Delete(s *xorm.Session, a web.Auth) (err error) {
// Check if the user exists
u, err := user.GetUserByUsername(s, lu.Username)
@ -244,7 +244,7 @@ func (lu *ProjectUser) ReadAll(s *xorm.Session, a web.Auth, search string, page
// @Failure 404 {object} web.HTTPError "User or project does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/users/{userID} [post]
func (lu *ProjectUser) Update(s *xorm.Session, _ web.Auth) (err error) {
func (lu *ProjectUser) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if the right is valid
if err := lu.Right.isValid(); err != nil {

View File

@ -149,7 +149,7 @@ func getSavedFilterSimpleByID(s *xorm.Session, id int64) (sf *SavedFilter, err e
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
// @Failure 500 {object} models.Message "Internal error"
// @Router /filters/{id} [get]
func (sf *SavedFilter) ReadOne(s *xorm.Session, _ web.Auth) error {
func (sf *SavedFilter) ReadOne(s *xorm.Session, a web.Auth) error {
// s already contains almost the full saved filter from the rights check, we only need to add the user
u, err := user.GetUserByID(s, sf.OwnerID)
sf.Owner = u
@ -169,7 +169,7 @@ func (sf *SavedFilter) ReadOne(s *xorm.Session, _ web.Auth) error {
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /filters/{id} [post]
func (sf *SavedFilter) Update(s *xorm.Session, _ web.Auth) error {
func (sf *SavedFilter) Update(s *xorm.Session, a web.Auth) error {
origFilter, err := getSavedFilterSimpleByID(s, sf.ID)
if err != nil {
return err
@ -204,7 +204,7 @@ func (sf *SavedFilter) Update(s *xorm.Session, _ web.Auth) error {
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /filters/{id} [delete]
func (sf *SavedFilter) Delete(s *xorm.Session, _ web.Auth) error {
func (sf *SavedFilter) Delete(s *xorm.Session, a web.Auth) error {
_, err := s.
Where("id = ?", sf.ID).
Delete(sf)

View File

@ -40,7 +40,7 @@ func (sf *SavedFilter) CanUpdate(s *xorm.Session, auth web.Auth) (bool, error) {
}
// CanCreate checks if a user has the right to update a saved filter
func (sf *SavedFilter) CanCreate(_ *xorm.Session, auth web.Auth) (bool, error) {
func (sf *SavedFilter) CanCreate(s *xorm.Session, auth web.Auth) (bool, error) {
if _, is := auth.(*LinkSharing); is {
return false, nil
}

View File

@ -94,7 +94,7 @@ func (ta *TaskAttachment) NewAttachment(s *xorm.Session, f io.ReadCloser, realna
}
// ReadOne returns a task attachment
func (ta *TaskAttachment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
func (ta *TaskAttachment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
exists, err := s.Where("id = ?", ta.ID).Get(ta)
if err != nil {
return
@ -127,7 +127,7 @@ func (ta *TaskAttachment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 404 {object} models.Message "The task does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id}/attachments [get]
func (ta *TaskAttachment) ReadAll(s *xorm.Session, _ web.Auth, _ string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
func (ta *TaskAttachment) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
attachments := []*TaskAttachment{}
limit, start := getLimitFromPageIndex(page, perPage)

View File

@ -101,7 +101,7 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [delete]
func (tc *TaskComment) Delete(s *xorm.Session, _ web.Auth) error {
func (tc *TaskComment) Delete(s *xorm.Session, a web.Auth) error {
deleted, err := s.
ID(tc.ID).
NoAutoCondition().
@ -135,7 +135,7 @@ func (tc *TaskComment) Delete(s *xorm.Session, _ web.Auth) error {
// @Failure 404 {object} web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [post]
func (tc *TaskComment) Update(s *xorm.Session, _ web.Auth) error {
func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
updated, err := s.
ID(tc.ID).
Cols("comment").
@ -192,7 +192,7 @@ func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
// @Failure 404 {object} web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [get]
func (tc *TaskComment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
err = getTaskCommentSimple(s, tc)
if err != nil {
return err

View File

@ -198,7 +198,7 @@ type taskOptions struct {
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/all [get]
func (t *Task) ReadAll(_ *xorm.Session, _ web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, totalItems int64, err error) {
func (t *Task) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
return nil, 0, 0, nil
}
@ -1513,9 +1513,6 @@ func (t *Task) overwriteRemindersWithReminderDates(reminderDates []time.Time) {
func updateRelativeReminderDates(task *Task) (err error) {
for _, reminder := range task.Reminders {
relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second
if reminder.RelativeTo != "" {
reminder.Reminder = time.Time{}
}
switch reminder.RelativeTo {
case ReminderRelationDueDate:
if !task.DueDate.IsZero() {
@ -1534,8 +1531,8 @@ func updateRelativeReminderDates(task *Task) (err error) {
err = ErrReminderRelativeToMissing{
TaskID: task.ID,
}
return err
}
return err
}
}
return nil

View File

@ -88,7 +88,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
// @Success 200 {object} models.Message "The user was successfully removed from the team."
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id}/members/{userID} [delete]
func (tm *TeamMember) Delete(s *xorm.Session, _ web.Auth) (err error) {
func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) {
total, err := s.Where("team_id = ?", tm.TeamID).Count(&TeamMember{})
if err != nil {
@ -120,7 +120,7 @@ func (tm *TeamMember) Delete(s *xorm.Session, _ web.Auth) (err error) {
// @Success 200 {object} models.Message "The member right was successfully changed."
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id}/members/{userID}/admin [post]
func (tm *TeamMember) Update(s *xorm.Session, _ web.Auth) (err error) {
func (tm *TeamMember) Update(s *xorm.Session, a web.Auth) (err error) {
// Find the numeric user id
user, err := user2.GetUserByUsername(s, tm.Username)
if err != nil {

View File

@ -184,7 +184,7 @@ func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
// @Failure 403 {object} web.HTTPError "The user does not have access to the team"
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id} [get]
func (t *Team) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
func (t *Team) ReadOne(s *xorm.Session, a web.Auth) (err error) {
team, err := GetTeamByID(s, t.ID)
if team != nil {
*t = *team
@ -338,7 +338,7 @@ func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
// @Failure 400 {object} web.HTTPError "Invalid team object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /teams/{id} [post]
func (t *Team) Update(s *xorm.Session, _ web.Auth) (err error) {
func (t *Team) Update(s *xorm.Session, a web.Auth) (err error) {
// Check if we have a name
if t.Name == "" {
return ErrTeamNameCannotBeEmpty{}

View File

@ -22,7 +22,7 @@ import (
)
// CanCreate checks if the user can create a new team
func (t *Team) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
func (t *Team) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}

View File

@ -40,6 +40,6 @@ const defaultAvatar string = `<?xml version="1.0" encoding="UTF-8"?>
</svg>`
// GetAvatar implements getting the avatar method
func (p *Provider) GetAvatar(_ *user.User, _ int64) (avatar []byte, mimeType string, err error) {
func (p *Provider) GetAvatar(user *user.User, size int64) (avatar []byte, mimeType string, err error) {
return []byte(defaultAvatar), "image/svg+xml", nil
}

View File

@ -151,7 +151,7 @@ func getUnsplashPhotoInfoByID(photoID string) (photo *Photo, err error) {
// @Success 200 {array} background.Image "An array with photos"
// @Failure 500 {object} models.Message "Internal error"
// @Router /backgrounds/unsplash/search [get]
func (p *Provider) Search(_ *xorm.Session, search string, page int64) (result []*background.Image, err error) {
func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []*background.Image, err error) {
// If we don't have a search query, return results from the unsplash featured collection
if search == "" {

View File

@ -32,7 +32,7 @@ type Provider struct {
}
// Search is only used to implement the interface
func (p *Provider) Search(_ *xorm.Session, _ string, _ int64) (result []*background.Image, err error) {
func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []*background.Image, err error) {
return
}
@ -52,7 +52,7 @@ func (p *Provider) Search(_ *xorm.Session, _ string, _ int64) (result []*backgro
// @Failure 404 {object} models.Message "The project does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/backgrounds/upload [put]
func (p *Provider) Set(s *xorm.Session, img *background.Image, project *models.Project, _ web.Auth) (err error) {
func (p *Provider) Set(s *xorm.Session, img *background.Image, project *models.Project, auth web.Auth) (err error) {
// Remove the old background if one exists
err = project.DeleteBackgroundFileIfExists()
if err != nil {

View File

@ -20,7 +20,9 @@ import (
"encoding/csv"
"errors"
"io"
"regexp"
"sort"
"strconv"
"strings"
"time"
@ -28,7 +30,6 @@ import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"github.com/gocarina/gocsv"
)
@ -74,6 +75,36 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
return err
}
// Copied from https://stackoverflow.com/a/57617885
var durationRegex = regexp.MustCompile(`P([\d\.]+Y)?([\d\.]+M)?([\d\.]+D)?T?([\d\.]+H)?([\d\.]+M)?([\d\.]+?S)?`)
// ParseDuration converts a ISO8601 duration into a time.Duration
func parseDuration(str string) time.Duration {
matches := durationRegex.FindStringSubmatch(str)
if len(matches) == 0 {
return 0
}
years := parseDurationPart(matches[1], time.Hour*24*365)
months := parseDurationPart(matches[2], time.Hour*24*30)
days := parseDurationPart(matches[3], time.Hour*24)
hours := parseDurationPart(matches[4], time.Hour)
minutes := parseDurationPart(matches[5], time.Second*60)
seconds := parseDurationPart(matches[6], time.Second)
return years + months + days + hours + minutes + seconds
}
func parseDurationPart(value string, unit time.Duration) time.Duration {
if len(value) != 0 {
if parsed, err := strconv.ParseFloat(value[:len(value)-1], 64); err == nil {
return time.Duration(float64(unit) * parsed)
}
}
return 0
}
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.NamespaceWithProjectsAndTasks) {
namespace := &models.NamespaceWithProjectsAndTasks{
Namespace: models.Namespace{
@ -200,7 +231,7 @@ func (m *Migrator) Migrate(user *user.User, file io.ReaderAt, size int64) error
task.IsChecklist = true
}
reminder := utils.ParseISO8601Duration(task.ReminderDuration)
reminder := parseDuration(task.ReminderDuration)
if reminder > 0 {
task.Reminder = reminder
}

View File

@ -141,12 +141,7 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
comment.ID = 0
}
for _, attachment := range t.Attachments {
attachmentFile, exists := storedFiles[attachment.File.ID]
if !exists {
log.Debugf(logPrefix+"Could not find attachment file %d for attachment %d", attachment.File.ID, attachment.ID)
continue
}
af, err := attachmentFile.Open()
af, err := storedFiles[attachment.File.ID].Open()
if err != nil {
return fmt.Errorf("could not open attachment %d for reading: %w", attachment.ID, err)
}

File diff suppressed because one or more lines are too long

View File

@ -51,7 +51,7 @@ type VikunjaCaldavProjectStorage struct {
}
// GetResources returns either all projects, links to the principal, or only one project, depending on the request
func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, _ bool) ([]data.Resource, error) {
func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren bool) ([]data.Resource, error) {
// It looks like we need to have the same handler for returning both the calendar home set and the user principal
// Since the client seems to ignore the whatever is being returned in the first request and just makes a second one
@ -166,7 +166,7 @@ func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) ([]
}
// GetResourcesByFilters fetches a project of resources with a filter
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByFilters(rpath string, _ *data.ResourceFilter) ([]data.Resource, error) {
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByFilters(rpath string, filters *data.ResourceFilter) ([]data.Resource, error) {
// If we already have a project saved, that means the user is making a REPORT request to find out if
// anything changed, in that case we need to return all tasks.
@ -359,7 +359,7 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
}
// DeleteResource deletes a resource
func (vcls *VikunjaCaldavProjectStorage) DeleteResource(_ string) error {
func (vcls *VikunjaCaldavProjectStorage) DeleteResource(rpath string) error {
if vcls.task != nil {
s := db.NewSession()
defer s.Close()
@ -411,13 +411,13 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
return err
}
labelMap := make(map[string]*models.Label)
labelMap := make(map[int64]*models.Label)
for _, l := range existingLabels {
labelMap[l.Title] = &l.Label
labelMap[l.ID] = &l.Label
}
for _, label := range labels {
if l, has := labelMap[label.Title]; has {
if l, has := labelMap[label.ID]; has {
*label = *l
continue
}

View File

@ -39,7 +39,7 @@ func (s *IncreaseUserCounter) Name() string {
return "increase.user.counter"
}
// Handle is executed when the event IncreaseUserCounter listens on is fired
func (s *IncreaseUserCounter) Handle(_ *message.Message) (err error) {
// Hanlde is executed when the event IncreaseUserCounter listens on is fired
func (s *IncreaseUserCounter) Handle(msg *message.Message) (err error) {
return keyvalue.IncrBy(metrics.UserCountKey, 1)
}

View File

@ -1,57 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package utils
import (
"regexp"
"strconv"
"time"
)
// ParseISO8601Duration converts a ISO8601 duration into a time.Duration
func ParseISO8601Duration(str string) time.Duration {
matches := durationRegex.FindStringSubmatch(str)
if len(matches) == 0 {
return 0
}
years := parseDurationPart(matches[2], time.Hour*24*365)
months := parseDurationPart(matches[3], time.Hour*24*30)
days := parseDurationPart(matches[4], time.Hour*24)
hours := parseDurationPart(matches[5], time.Hour)
minutes := parseDurationPart(matches[6], time.Second*60)
seconds := parseDurationPart(matches[7], time.Second)
duration := years + months + days + hours + minutes + seconds
if matches[1] == "-" {
return -duration
}
return duration
}
var durationRegex = regexp.MustCompile(`([-+])?P([\d\.]+Y)?([\d\.]+M)?([\d\.]+D)?T?([\d\.]+H)?([\d\.]+M)?([\d\.]+?S)?`)
func parseDurationPart(value string, unit time.Duration) time.Duration {
if len(value) != 0 {
if parsed, err := strconv.ParseFloat(value[:len(value)-1], 64); err == nil {
return time.Duration(float64(unit) * parsed)
}
}
return 0
}

View File

@ -1,39 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee 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 Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package utils
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestParseISO8601Duration(t *testing.T) {
t.Run("full example", func(t *testing.T) {
dur := ParseISO8601Duration("P1DT1H1M1S")
expected, _ := time.ParseDuration("25h1m1s")
assert.Equal(t, expected, dur)
})
t.Run("negative duration", func(t *testing.T) {
dur := ParseISO8601Duration("-P1DT1H1M1S")
expected, _ := time.ParseDuration("-25h1m1s")
assert.Equal(t, expected, dur)
})
}