all repos — cameraman @ 83d7e0c51439a2c4916cea870c2f687961610154

initial commit
BiRabittoh andronacomarco@gmail.com
Wed, 29 May 2024 16:03:19 +0200
commit

83d7e0c51439a2c4916cea870c2f687961610154

A .env.example

@@ -0,0 +1,3 @@

+TELEGRAM_BOT_TOKEN=your:bot-token +TELEGRAM_CHAT_ID=your_chat_id +TELEGRAM_THREAD_ID=your_thread_id
A .github/workflows/publish.yaml

@@ -0,0 +1,48 @@

+# +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `main`. +on: + push: + branches: ['main'] + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v3 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }}
A .gitignore

@@ -0,0 +1,2 @@

+.env +*.db
A Dockerfile

@@ -0,0 +1,32 @@

+# syntax=docker/dockerfile:1 + +FROM golang:alpine AS builder + +WORKDIR /build + +# Install gcc and musl-dev +RUN apk add --no-cache gcc musl-dev + +# Download Go modules +COPY go.mod go.sum ./ +RUN go mod download + +# Transfer source code +COPY templates ./templates +COPY *.go ./ + +# Build +RUN CGO_ENABLED=1 go build -ldflags='-s -w' -trimpath -o /dist/app +RUN ldd /dist/app | tr -s [:blank:] '\n' | grep ^/ | xargs -I % install -D % /dist/% +RUN ln -s ld-musl-x86_64.so.1 /dist/lib/libc.musl-x86_64.so.1 + +# Test +FROM builder AS run-test-stage +RUN go test -v ./... + +FROM scratch AS build-release-stage + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /dist / + +ENTRYPOINT ["/app"]
A LICENSE

@@ -0,0 +1,21 @@

+MIT License + +Copyright (c) 2024 Marco Andronaco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
A README.md

@@ -0,0 +1,27 @@

+# Cameraman +A simple calendar to remind yourself about yearly occurrences. It can send notifications via Telegram API. + +## Instructions + +First of all, you should set your Telegram parameters if you want notifications (you probably do). +``` +cp .env.example .env +nano .env +``` + +### Docker +If you want to use the latest image, just do: +``` +docker-compose up -d +``` + +Otherwise, you can build it yourself: +``` +docker-compose up -d --build +``` + +### Test and debug locally +``` +go test -v ./... +go run . +```
A docker-compose.yaml

@@ -0,0 +1,15 @@

+services: + app: + build: . + image: ghcr.io/birabittoh/cameraman:main + container_name: cameraman + restart: unless-stopped + ports: + - 3000:3000 + env_file: + - .env + volumes: + - /etc/localtime:/etc/localtime:ro + - data:/data +volumes: + data:
A go.mod

@@ -0,0 +1,40 @@

+module github.com/BiRabittoh/cameraman + +go 1.22.3 + +require ( + github.com/bytedance/sonic v1.11.7 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.4 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-resty/resty/v2 v2.13.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/sqlite v1.5.5 // indirect + gorm.io/gorm v1.25.10 // indirect +)
A go.sum

@@ -0,0 +1,133 @@

+github.com/bytedance/sonic v1.11.7 h1:k/l9p1hZpNIMJSk37wL9ltkcpqLfIho1vYthi4xT2t4= +github.com/bytedance/sonic v1.11.7/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= +github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= +github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= +gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
A handlers.go

@@ -0,0 +1,90 @@

+package main + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func validateDate(month, day int) error { + if month < 1 || month > 12 { + return errors.New("invalid month: must be between 1 and 12") + } + + if day < 1 || day > 31 { + return errors.New("invalid day: must be between 1 and 31") + } + + // Construct a date and use time package to validate + dateStr := fmt.Sprintf("2023-%02d-%02d", month, day) + _, err := time.Parse("2006-01-02", dateStr) + if err != nil { + return errors.New("invalid day for the given month") + } + + return nil +} + +func addOccurrence(c *gin.Context) { + var input Occurrence + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Validate date + if err := validateDate(input.Month, input.Day); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var occurrence Occurrence + if input.ID != 0 { + if err := db.First(&occurrence, input.ID).Error; err == nil { + // Update existing record with new values + if input.Month != 0 { + occurrence.Month = input.Month + } + if input.Day != 0 { + occurrence.Day = input.Day + } + if input.Name != "" { + occurrence.Name = input.Name + } + if input.Description != "" { + occurrence.Description = input.Description + } + occurrence.Notify = input.Notify + db.Save(&occurrence) + c.JSON(http.StatusOK, occurrence) + return + } + } + + // Create a new record if no existing record is found + occurrence = input + db.Create(&occurrence) + c.JSON(http.StatusOK, occurrence) +} + +func getOccurrences(c *gin.Context) { + var occurrences []Occurrence + db.Find(&occurrences) + c.JSON(http.StatusOK, occurrences) +} + +func deleteOccurrence(c *gin.Context) { + id := c.Param("id") + var occurrence Occurrence + + if err := db.First(&occurrence, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Occurrence not found"}) + return + } + + db.Delete(&occurrence) + c.JSON(http.StatusOK, gin.H{"message": "Occurrence deleted"}) +}
A main.go

@@ -0,0 +1,72 @@

+package main + +import ( + "log" + "os" + "path" + "time" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// Occurrence represents a scheduled event +type Occurrence struct { + ID uint `gorm:"primaryKey" json:"id"` + Month int `json:"month"` + Day int `json:"day"` + Name string `json:"name"` + Description string `json:"description"` + Notify bool `json:"notify"` + Notified bool `json:"notified"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` +} + +var db *gorm.DB + +const ( + dataDir = "data" + dbFile = "occurrences.db" +) + +func initDB() { + if _, err := os.Stat(dataDir); os.IsNotExist(err) { + err := os.Mkdir(dataDir, os.ModePerm) + if err != nil { + log.Fatal("Failed to create directory:", err) + } + } + + var err error + db, err = gorm.Open(sqlite.Open(path.Join(dataDir, dbFile)), &gorm.Config{}) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + + db.AutoMigrate(&Occurrence{}) +} + +func loadEnv() { + if err := godotenv.Load(); err != nil { + log.Println("Error loading .env file") + } +} + +func main() { + loadEnv() + initDB() + ParseTemplates() + + go CheckOccurrences() + + router := gin.Default() + router.POST("/occurrences", addOccurrence) + router.GET("/occurrences", getOccurrences) + router.DELETE("/occurrences/:id", deleteOccurrence) + router.GET("/", ShowIndexPage) + + router.Run(":3000") +}
A notify.go

@@ -0,0 +1,86 @@

+package main + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/go-resty/resty/v2" +) + +var notificationWindow = 3 + +func notifyTelegram(occurrence Occurrence) { + client := resty.New() + telegramToken := os.Getenv("TELEGRAM_BOT_TOKEN") + chatID := os.Getenv("TELEGRAM_CHAT_ID") + threadID := os.Getenv("TELEGRAM_THREAD_ID") + + message := fmt.Sprintf("*Giorno %02d/%02d*:\n\n_%s_\n%s", + occurrence.Day, occurrence.Month, occurrence.Name, occurrence.Description) + + url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", telegramToken) + + // Create the payload + payload := map[string]interface{}{ + "message_thread_id": threadID, + "chat_id": chatID, + "text": message, + "parse_mode": "markdown", + } + + // Send the POST request + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). + SetBody(payload). + Post(url) + + if err != nil { + log.Printf("Failed to send notification: %v", err) + return + } + log.Printf("Notification sent: %s, Response: %s", message, resp) +} + +func resetNotifications() { + if err := db.Model(&Occurrence{}).Where("notified = ?", true).Update("notified", false).Error; err != nil { + log.Printf("Failed to reset notifications: %v", err) + } else { + log.Println("Notifications have been reset for the new year.") + } +} + +func CheckOccurrences() { + const sleepDuration = 12 * time.Hour + + for { + now := time.Now() + var occurrences []Occurrence + endWindow := now.AddDate(0, 0, notificationWindow) + + db.Where("notified = ? AND ((month = ? AND day >= ?) OR (month = ? AND day <= ?))", + false, now.Month(), now.Day(), endWindow.Month(), endWindow.Day()).Find(&occurrences) + + for _, occurrence := range occurrences { + occurrenceDate := time.Date(now.Year(), time.Month(occurrence.Month), occurrence.Day, 0, 0, 0, 0, time.Local) + if occurrenceDate.Before(now) || occurrenceDate.After(endWindow) { + continue + } + + if occurrence.Notify { + notifyTelegram(occurrence) + occurrence.Notified = true + db.Save(&occurrence) + } + } + + // Check if New Year's Eve is within the next sleep cycle + nextCheck := now.Add(sleepDuration) + if now.Month() == 12 && now.Day() == 31 || (nextCheck.Month() == 1 && nextCheck.Day() == 1) { + resetNotifications() + } + + time.Sleep(sleepDuration) + } +}
A templates/index.html

@@ -0,0 +1,267 @@

+<!DOCTYPE html> +<html> +<head> + <title>Ricorrenze</title> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"> + <style> + body { + font-family: Arial, sans-serif; + } + table { + width: 100%; + border-collapse: collapse; + } + table, th, td { + border: 1px solid black; + } + th, td { + padding: 10px; + text-align: left; + } + th { + background-color: #f2f2f2; + } + .actions { + display: flex; + gap: 10px; + } + .actions i { + cursor: pointer; + } + .add-row-button { + margin-top: 20px; + background-color: #4CAF50; + border: none; + color: white; + padding: 10px 20px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + transition-duration: 0.4s; + cursor: pointer; + border-radius: 12px; + } + .add-row-button:hover { + background-color: #45a049; + } + </style> + <script> + function submitForm(event) { + event.preventDefault(); + + const name = document.getElementById('name').value; + const description = document.getElementById('description').value; + const month = document.getElementById('month').value; + const day = document.getElementById('day').value; + const notify = document.getElementById('notify').checked; + + const data = { + name: name, + description: description, + month: parseInt(month), + day: parseInt(day), + notify: notify + }; + + fetch('/occurrences', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + .then(response => { + if (response.ok) { + window.location.reload(); + } else { + alert('Failed to add occurrence'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to add occurrence'); + }); + } + + function deleteOccurrence(id) { + if (confirm('Are you sure you want to delete this occurrence?')) { + fetch(`/occurrences/${id}`, { + method: 'DELETE' + }) + .then(response => { + if (response.ok) { + window.location.reload(); + } else { + alert('Failed to delete occurrence'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to delete occurrence'); + }); + } + } + + function createInputFields(id, name, description, day, month, notify, isNew) { + return ` + <td>${isNew ? '' : id}</td> + <td><input type="text" value="${name || ''}" id="${isNew ? 'new' : 'edit'}-name-${id}"></td> + <td><input type="text" value="${description || ''}" id="${isNew ? 'new' : 'edit'}-description-${id}"></td> + <td><input type="number" value="${day || ''}" id="${isNew ? 'new' : 'edit'}-day-${id}" min="1" max="31"> / <input type="number" value="${month || ''}" id="${isNew ? 'new' : 'edit'}-month-${id}" min="1" max="12"></td> + <td><input type="checkbox" id="${isNew ? 'new' : 'edit'}-notify-${id}" ${notify ? 'checked' : ''}></td> + <td class="actions"> + <i class="fas fa-save" title="Save" onclick="${isNew ? 'saveNewOccurrence()' : `saveOccurrence(${id})`}"></i> + <i class="fas fa-times" title="Cancel" onclick="${isNew ? 'cancelNewOccurrence()' : `cancelEdit(${id}, '${name}', '${description}', ${day}, ${month}, ${notify})`}"></i> + </td> + `; + } + + function editOccurrence(id) { + const row = document.getElementById(`occurrence-${id}`); + const cells = row.getElementsByTagName('td'); + + const name = cells[1].innerText; + const description = cells[2].innerText; + const [day, month] = cells[3].innerText.split('/'); + const notify = cells[4].getElementsByTagName('input')[0].checked; + + row.innerHTML = createInputFields(id, name, description, day, month, notify, false); + } + + function cancelEdit(id, name, description, day, month, notify) { + const row = document.getElementById(`occurrence-${id}`); + row.innerHTML = ` + <td>${id}</td> + <td>${name}</td> + <td>${description}</td> + <td>${day}/${month}</td> + <td><input type="checkbox" ${notify ? 'checked' : ''} disabled></td> + <td class="actions"> + <i class="fas fa-edit" title="Edit" onclick="editOccurrence(${id})"></i> + <i class="fas fa-trash-alt" title="Delete" onclick="deleteOccurrence(${id})"></i> + </td> + `; + } + + function saveOccurrence(id) { + const name = document.getElementById(`edit-name-${id}`).value; + const description = document.getElementById(`edit-description-${id}`).value; + const day = parseInt(document.getElementById(`edit-day-${id}`).value); + const month = parseInt(document.getElementById(`edit-month-${id}`).value); + const notify = document.getElementById(`edit-notify-${id}`).checked; + + const updatedData = { + id: id, + name: name, + description: description, + month: month, + day: day, + notify: notify + }; + + fetch('/occurrences', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updatedData) + }) + .then(response => { + if (response.ok) { + window.location.reload(); + } else { + alert('Failed to update occurrence'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to update occurrence'); + }); + } + + function addNewOccurrenceRow() { + const table = document.querySelector('table'); + const newRow = table.insertRow(-1); + newRow.id = 'new-occurrence'; + newRow.innerHTML = createInputFields('new', '', '', '', '', true, true); + } + + function saveNewOccurrence() { + const name = document.getElementById('new-name-new').value; + const description = document.getElementById('new-description-new').value; + const day = parseInt(document.getElementById('new-day-new').value); + const month = parseInt(document.getElementById('new-month-new').value); + const notify = document.getElementById('new-notify-new').checked; + + const newData = { + name: name, + description: description, + month: month, + day: day, + notify: notify + }; + + fetch('/occurrences', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(newData) + }) + .then(response => { + if (response.ok) { + window.location.reload(); + } else { + alert('Failed to add occurrence'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('Failed to add occurrence'); + }); + } + + function cancelNewOccurrence() { + const newRow = document.getElementById('new-occurrence'); + newRow.parentNode.removeChild(newRow); + } + </script> +</head> +<body> + <h1>Ricorrenze</h1> + <table> + <tr> + <th>ID</th> + <th>Nome</th> + <th>Descrizione</th> + <th>Data (gg/mm)</th> + <th>Notifica</th> + <th>Azioni</th> + </tr> + {{ range .Occurrences }} + <tr id="occurrence-{{ .ID }}"> + <td>{{ .ID }}</td> + <td>{{ .Name }}</td> + <td>{{ .Description }}</td> + <td>{{ padZero .Day }}/{{ padZero .Month }}</td> + <td><input type="checkbox" {{if .Notify}}checked{{end}} disabled></td> + <td class="actions"> + <i class="fas fa-edit" title="Edit" onclick="editOccurrence('{{ .ID }}')"></i> + <i class="fas fa-trash-alt" title="Delete" onclick="deleteOccurrence('{{ .ID }}')"></i> + </td> + </tr> + {{ else }} + <tr> + <td colspan="6">Nessuna ricorrenza.</td> + </tr> + {{ end }} + </table> + <div style="margin-top: 10px; text-align: center;"> + <button class="add-row-button" onclick="addNewOccurrenceRow()"><i class="fas fa-plus"></i> Aggiungi</button> + </div> + + +</body> +</html>
A ui.go

@@ -0,0 +1,45 @@

+package main + +import ( + "embed" + "fmt" + "html/template" + "log" + + "github.com/gin-gonic/gin" +) + +func padZero(i int) string { + return fmt.Sprintf("%02d", i) +} + +var ( + //go:embed templates/index.html + templates embed.FS + indexTemplate *template.Template + funcMap = template.FuncMap{"padZero": padZero} +) + +func ParseTemplates() { + var err error + indexTemplate, err = template.New("index.html").Funcs(funcMap).ParseFS(templates, "templates/index.html") + if err != nil { + log.Fatal("Could not parse index template") + return + } +} + +func ShowIndexPage(c *gin.Context) { + var occurrences []Occurrence + db.Find(&occurrences) + + data := struct { + Occurrences []Occurrence + }{ + Occurrences: occurrences, + } + + if indexTemplate.Execute(c.Writer, data) != nil { + c.String(500, "Internal Server Error") + } +}