diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml new file mode 100644 index 0000000..0bbd5cf --- /dev/null +++ b/.gitea/workflows/docker-build.yaml @@ -0,0 +1,83 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - "**" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # - name: Run Trivy vulnerability scanner in repo mode + # uses: aquasecurity/trivy-action@v0.36.0 + # with: + # scan-type: 'fs' + # ignore-unfixed: true + # format: 'sarif' + # output: 'trivy-results.sarif' + # severity: 'CRITICAL' + + # - name: Upload Trivy scan results to GitHub Security tab + # uses: github/codeql-action/upload-sarif@v4 + # with: + # sarif_file: 'trivy-results.sarif' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Compute image tag + id: tag + run: | + BRANCH="${{ gitea.ref_name }}" + + if [ "$BRANCH" = "main" ]; then + TAG="latest" + elif [[ "$BRANCH" == v* ]]; then + TAG="$BRANCH" + else + TAG="test" + fi + + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: blinkfink182/donetick-notifier:${{ steps.tag.outputs.tag }} + + - name: Notify Apprise (success) + if: success() + run: | + curl -X POST \ + -H "Content-Type: application/json" \ + -d "{ + \"tags\": \"all\", + \"title\": \"Gitea Build Succeeded\", + \"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nImage tag built successfully\" + }" \ + ${{ secrets.APPRISE_URL }} + + - name: Notify Apprise (failure) + if: failure() + run: | + curl -X POST \ + -H "Content-Type: application/json" \ + -d "{ + \"tags\": \"all\", + \"title\": \"Gitea Build Failed\", + \"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nCheck logs in Gitea\" + }" \ + ${{ secrets.APPRISE_URL }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9daf30a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.postmate/postmate-envs.json +.vscode/launch.json +.env.ps1 +debug.ps1 +.postmate/postmate-history.json +.postmate/postmate-envs.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e106e53 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "trivy.secretScanning": true +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bed4acb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/powershell + +USER 1000:1000 + +WORKDIR /data +COPY ["Start-DoneTickNotifier.ps1", "/data/"] + +ENTRYPOINT ["pwsh", "-Command", "/data/Start-DoneTickNotifier.ps1"] \ No newline at end of file diff --git a/Donetick/get-chore.yml b/Donetick/get-chore.yml new file mode 100644 index 0000000..dcfb225 --- /dev/null +++ b/Donetick/get-chore.yml @@ -0,0 +1,20 @@ +info: + name: Get-Chore + type: http + seq: 1 +http: + method: GET + url: "{{baseUrl}}/eapi/v1/chore" + headers: [] + params: [] +settings: + encodeUrl: true + timeout: 30000 + followRedirects: true + maxRedirects: 5 +runtime: + auth: + type: apikey + key: secretkey + value: "{{secretkey}}" + placement: header diff --git a/Donetick/opencollection.yml b/Donetick/opencollection.yml new file mode 100644 index 0000000..8e0e4d6 --- /dev/null +++ b/Donetick/opencollection.yml @@ -0,0 +1,13 @@ +opencollection: 1.0.0 +info: + name: Donetick + version: 1.0.0 +config: + environments: + - name: Production + variables: + - name: baseUrl + value: https://todo.ktr32.org + - name: secretkey + secret: true +items: [] diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 new file mode 100644 index 0000000..4e8918e --- /dev/null +++ b/Start-DoneTickNotifier.ps1 @@ -0,0 +1,103 @@ +[string] $dtHost = $ENV:DONETICKHOST +[string] $dtPort = $ENV:DONETICKPORT +[string] $dtAPIKey = $ENV:DONETICKAPIKEY +[string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL +[string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG + + +function Send-Notification +{ + param( + # Title of the notification + [Parameter(Mandatory=$true)] + [string] + $title, + + # Content of the notification + [Parameter(Mandatory=$true)] + [string] + $content + ) + + $headers = @{ + "Content-Type"="application/json" + } + $body = @{ + title=$title + body=$content + tags="$appriseWebhookTag" + } + $json = $body | ConvertTo-Json + try { + Invoke-WebRequest -URI $appriseWebhookURL -Method post -Body $json -Headers $headers + } + catch { + Write-Host "Notification Error Encountered: $($global:Error[0])" + } +} + +function Get-Chores +{ + try { + $headers = @{ + secretkey = $dtAPIKey + } + $results = Invoke-WebRequest -Uri "https://$dtHost`:$dtPort/eapi/v1/chore" -Method Get -Headers $headers + return ($results.Content | ConvertFrom-Json) + } + catch { + Write-Error "Error fetching chores: $($global:Error[0])" + } +} + +$today = (Get-Date "23:59:59") +$chores = Get-Chores + +$overdueTasks = @() +$todaysTasks = @() + +foreach($chore in $chores) +{ + if ($chore.nextDueDate) + { + $dueDate = Get-Date $chore.nextDueDate + if (($dueDate - $today).Days -lt 0) #OVERDUE + { + write-host "$($chore.name) $dueDate is overdue!" + $overdueTasks += $chore + } + elseif (($dueDate - $today) -lt 1) #due today + { + write-host "$($chore.name) $dueDate is due today!" + $todaysTasks += $chore + # Send-Notification -title "TASK DUE TODAY" -content "$($chore.Name) is due today!" + } + else + { + write-host "$($chore.name) $dueDate is due in the future" + } + } + +} + +if ($overdueTasks.Count -ne 0) +{ + Write-Host "Sending a notification for $($overdueTasks.Count) overdue tasks" + $content = "The following tasks are overdue!`n" + foreach($overdueTask in $overdueTasks) + { + $content += "$($overdueTask.Name) was due $(Get-Date $overdueTask.nextDueDate -Format "MM/dd/yyyy HH:mm")`n" + } + Send-Notification -title "OVERDUE TASKS" -content $content +} + +if ($todaysTasks.Count -ne 0) +{ + Write-Host "Sending a notification for $($todaysTasks.Count) tasks due today" + $content = "The following tasks are due today!`n" + foreach($task in $todaysTasks) + { + $content += "$($task.Name) is due $(Get-Date $task.nextDueDate -Format "MM/dd/yyyy HH:mm")`n" + } + Send-Notification -title "TODAY'S TASKS" -content $content +} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..ef401c0 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ +# SAMPLE DOCKER COMPOSE +services: + donetick-notifier: + container_name: donetick-notifier + image: docker.io/blinkfink182/donetick-notifier + environment: + # BELOW ARE ALL REQUIRED + - DONETICKHOST=host.docker.internal # donetick host + - DONETICKPORT=8787 # donetick port + - DONETICKAPIKEY=adminpass # donetick API key + - APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url + - APPRISEWEBHOOKTAG=all # apprise notification tag \ No newline at end of file diff --git a/ref/chore-schema.json b/ref/chore-schema.json new file mode 100644 index 0000000..3d492bf --- /dev/null +++ b/ref/chore-schema.json @@ -0,0 +1,152 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "frequencyType": { + "type": "string" + }, + "frequency": { + "type": "integer" + }, + "frequencyMetadata": { + "type": "object", + "properties": { + "unit": { + "type": "string" + }, + "time": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "weekPattern": { + "type": "string" + } + }, + "required": [ + "unit", + "time", + "timezone", + "weekPattern" + ] + }, + "nextDueDate": { + "type": [ + "null", + "string" + ] + }, + "isRolling": { + "type": "boolean" + }, + "assignedTo": { + "type": [ + "integer", + "null" + ] + }, + "assignees": { + "type": "array" + }, + "assignStrategy": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "notification": { + "type": "boolean" + }, + "notificationMetadata": { + "type": "object", + "properties": { + "templates": { + "type": "array" + } + }, + "required": [ + "templates" + ] + }, + "labels": { + "type": "null" + }, + "labelsV2": { + "type": "array" + }, + "circleId": { + "type": "integer" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "createdBy": { + "type": "integer" + }, + "updatedBy": { + "type": "integer" + }, + "thingChore": { + "type": "null" + }, + "status": { + "type": "integer" + }, + "priority": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "requireApproval": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "completionWindow": { + "type": "integer" + } + }, + "required": [ + "id", + "name", + "frequencyType", + "frequency", + "frequencyMetadata", + "nextDueDate", + "isRolling", + "assignedTo", + "assignees", + "assignStrategy", + "isActive", + "notification", + "notificationMetadata", + "labels", + "labelsV2", + "circleId", + "createdAt", + "updatedAt", + "createdBy", + "updatedBy", + "thingChore", + "status", + "priority", + "description", + "requireApproval", + "isPrivate", + "completionWindow" + ] + } +} \ No newline at end of file