From 90a2805339b526941d60f8a0bc935c07890644ab Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Wed, 13 May 2026 13:52:05 -0500 Subject: [PATCH 01/19] first draft of readme --- README.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/README.md b/README.md index 905bf57..420e109 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,136 @@ # donetick-notifier +A small PowerShell notifier for [Donetick](https://github.com/donetick/donetick) chores. + +The notifier checks the Donetick external API for chores, groups tasks that are overdue or due today, and sends summary notifications through an Apprise-compatible webhook. + +## What it does + +- Fetches chores from `https://:/eapi/v1/chore`. +- Authenticates to Donetick with the `secretkey` header. +- Sends one Apprise notification for overdue tasks, when any exist. +- Sends one Apprise notification for tasks due today, when any exist. +- Runs once and exits. + +Because the container runs once and exits, schedule it with cron, systemd timers, Kubernetes CronJobs, Gitea Actions, or another scheduler if you want recurring notifications. + +## Configuration + +All configuration is provided through environment variables. + +| Variable | Required | Description | +| --- | --- | --- | +| `DONETICKHOST` | Yes | Donetick host name or IP address. Do not include `https://`. | +| `DONETICKPORT` | Yes | Donetick HTTPS port. | +| `DONETICKAPIKEY` | Yes | Donetick external API key. Sent as the `secretkey` header. | +| `APPRISEWEBHOOKURL` | Yes | Apprise webhook URL that accepts notification posts. | +| `APPRISEWEBHOOKTAG` | Yes | Apprise tag value to include with each notification. | + +## Docker + +The published image is: + +```text +docker.io/blinkfink182/donetick-notifier +``` + +### Run Once + +```sh +docker run --rm \ + --name donetick-notifier \ + -e DONETICKHOST=host.docker.internal \ + -e DONETICKPORT=8787 \ + -e DONETICKAPIKEY=your-donetick-api-key \ + -e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \ + -e APPRISEWEBHOOKTAG=all \ + docker.io/blinkfink182/donetick-notifier:latest +``` + +### Docker Compose + +```yaml +services: + donetick-notifier: + container_name: donetick-notifier + image: docker.io/blinkfink182/donetick-notifier:latest + environment: + DONETICKHOST: host.docker.internal + DONETICKPORT: "8787" + DONETICKAPIKEY: your-donetick-api-key + APPRISEWEBHOOKURL: https://apprise.example.com/notify/config + APPRISEWEBHOOKTAG: all +``` + +Run it with: + +```sh +docker compose run --rm donetick-notifier +``` + +If Donetick and Apprise are running on the same Docker network, use the service names instead of `host.docker.internal`. + +```yaml +services: + donetick-notifier: + image: docker.io/blinkfink182/donetick-notifier:latest + environment: + DONETICKHOST: donetick + DONETICKPORT: "8787" + DONETICKAPIKEY: your-donetick-api-key + APPRISEWEBHOOKURL: http://apprise:8000/notify/config + APPRISEWEBHOOKTAG: all +``` + +## Build Locally + +```sh +docker build -t donetick-notifier . +``` + +```sh +docker run --rm \ + -e DONETICKHOST=host.docker.internal \ + -e DONETICKPORT=8787 \ + -e DONETICKAPIKEY=your-donetick-api-key \ + -e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \ + -e APPRISEWEBHOOKTAG=all \ + donetick-notifier +``` + +## Run Without Docker + +PowerShell 7 or newer is recommended. + +```powershell +$env:DONETICKHOST = "donetick.example.com" +$env:DONETICKPORT = "8787" +$env:DONETICKAPIKEY = "your-donetick-api-key" +$env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config" +$env:APPRISEWEBHOOKTAG = "all" + +pwsh ./Start-DoneTickNotifier.ps1 +``` + +## CI/CD + +This repository includes Gitea workflows for: + +- Building and pushing the Docker image. +- Running security checks with Gitleaks, Semgrep, and Trivy. +- Creating Gitea issues for security findings when configured with a `GITEA_TOKEN`. +- Sending Apprise notifications for Docker build success or failure. + +The Docker build workflow tags images as: + +| Branch or ref | Image tag | +| --- | --- | +| `main` | `latest` | +| refs beginning with `v` | matching ref name, such as `v1.0.0` | +| all other refs | `test` | + +## AI Assistance Disclosure + +OpenAI Codex was used to help create this README and the Gitea pipeline files in this repository. + +All released application code is written by the repository owner. From 998f639a55f1d6ae403c8219ea851b484d8b871a Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Wed, 13 May 2026 21:09:52 -0500 Subject: [PATCH 02/19] enable looping so the script can handle scheduling --- Start-DoneTickNotifier.ps1 | 96 ++++++++++++++++++++++---------------- docker-compose.yaml | 3 +- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index 4e8918e..9571bb1 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -3,6 +3,7 @@ [string] $dtAPIKey = $ENV:DONETICKAPIKEY [string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL [string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG +[int[]] $notificationTimes = $ENV:NOTIFICATIONTIMES | Sort-Object function Send-Notification @@ -50,54 +51,71 @@ function Get-Chores } } -$today = (Get-Date "23:59:59") -$chores = Get-Chores - -$overdueTasks = @() -$todaysTasks = @() - -foreach($chore in $chores) -{ - if ($chore.nextDueDate) +while ($true) { + $today = (Get-Date "23:59:59") + $chores = Get-Chores + + $overdueTasks = @() + $todaysTasks = @() + + foreach($chore in $chores) { - $dueDate = Get-Date $chore.nextDueDate - if (($dueDate - $today).Days -lt 0) #OVERDUE + if ($chore.nextDueDate) { - write-host "$($chore.name) $dueDate is overdue!" - $overdueTasks += $chore + $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" + } } - elseif (($dueDate - $today) -lt 1) #due today + + } + + 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) { - write-host "$($chore.name) $dueDate is due today!" - $todaysTasks += $chore - # Send-Notification -title "TASK DUE TODAY" -content "$($chore.Name) is due today!" + $content += "$($overdueTask.Name) was due $(Get-Date $overdueTask.nextDueDate -Format "MM/dd/yyyy HH:mm")`n" } - else + 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) { - write-host "$($chore.name) $dueDate is due in the future" + $content += "$($task.Name) is due $(Get-Date $task.nextDueDate -Format "MM/dd/yyyy HH:mm")`n" } + Send-Notification -title "TODAY'S TASKS" -content $content } -} - -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) + Write-Host "Finding next notification time..." + foreach($time in $notificationTimes) { - $content += "$($overdueTask.Name) was due $(Get-Date $overdueTask.nextDueDate -Format "MM/dd/yyyy HH:mm")`n" + $now = Get-Date + $diff = $now.Hour - $time + if ($diff -lt 0) # next notification time + { + Write-Host "Next notification time is $time`:00" + $sleepTime = ($diff * -1) * 60 + Write-Host "Sleeping for $sleepTime seconds" + Start-Sleep -Seconds $sleepTime + continue # leave loop + } } - 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 index ef401c0..98bb9e9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,4 +9,5 @@ services: - 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 + - APPRISEWEBHOOKTAG=all # apprise notification tag + - NOTIFICATIONTIMES=8,12,17 # hours when notifications will be sent \ No newline at end of file From adf42df4a743801c38f8da8503ec9aa93a226772 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Wed, 13 May 2026 21:21:06 -0500 Subject: [PATCH 03/19] fixed a bug for when the next notification time is tomorrow --- Start-DoneTickNotifier.ps1 | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index 9571bb1..e5623d4 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -105,17 +105,27 @@ while ($true) { } Write-Host "Finding next notification time..." + $nextNotificationTomorrow = $true foreach($time in $notificationTimes) { $now = Get-Date $diff = $now.Hour - $time if ($diff -lt 0) # next notification time { + $nextNotificationTomorrow = $false Write-Host "Next notification time is $time`:00" - $sleepTime = ($diff * -1) * 60 + $sleepTime = ($diff * -1) * 60 * 60 # hours * mins * seconds Write-Host "Sleeping for $sleepTime seconds" Start-Sleep -Seconds $sleepTime continue # leave loop } } + if ($nextNotificationTomorrow) + { + Write-Host "Next notification time is $time`:00" + $diff = 24 - $now.Hour + $notificationTimes[0] + $sleepTime = $diff * 60 * 60 # hours * mins * seconds + Write-Host "Sleeping for $sleepTime seconds" + Start-Sleep -Seconds $sleepTime + } } \ No newline at end of file From 1dc15708b956a11f8ce3031ac808b8047b851e40 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Wed, 13 May 2026 21:36:58 -0500 Subject: [PATCH 04/19] correcting notification times not being seen as ints --- Start-DoneTickNotifier.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index e5623d4..c39a512 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -3,7 +3,7 @@ [string] $dtAPIKey = $ENV:DONETICKAPIKEY [string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL [string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG -[int[]] $notificationTimes = $ENV:NOTIFICATIONTIMES | Sort-Object +[int[]] $notificationTimes = $ENV:NOTIFICATIONTIMES -split "," function Send-Notification @@ -51,6 +51,10 @@ function Get-Chores } } +if (-not $notificationTimes) { $notificationTimes = @(8) } +else { $notificationTimes = $notificationTimes | Sort-Object } +Write-Host "Notification times: $notificationTimes" + while ($true) { $today = (Get-Date "23:59:59") $chores = Get-Chores @@ -122,8 +126,9 @@ while ($true) { } if ($nextNotificationTomorrow) { + $time = $notificationTimes[0] Write-Host "Next notification time is $time`:00" - $diff = 24 - $now.Hour + $notificationTimes[0] + $diff = 24 - $now.Hour + $time $sleepTime = $diff * 60 * 60 # hours * mins * seconds Write-Host "Sleeping for $sleepTime seconds" Start-Sleep -Seconds $sleepTime From 3a39a39be8f3ad4c0d9ad7845c3ef50d9462009b Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Thu, 14 May 2026 21:20:00 -0500 Subject: [PATCH 05/19] allowing for setting timezone --- Dockerfile | 2 +- Start-DoneTickNotifier.ps1 | 1 + docker-compose.yaml | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e737354..737b04d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/powershell:7.5-ubuntu-24.04 USER root RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates \ + && apt-get install -y --no-install-recommends tzdata \ && apt-get dist-upgrade -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index c39a512..10a14ca 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -109,6 +109,7 @@ while ($true) { } Write-Host "Finding next notification time..." + Write-Host "Current time: $(Get-Date)" $nextNotificationTomorrow = $true foreach($time in $notificationTimes) { diff --git a/docker-compose.yaml b/docker-compose.yaml index 98bb9e9..702a23d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,4 +10,5 @@ services: - DONETICKAPIKEY=adminpass # donetick API key - APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url - APPRISEWEBHOOKTAG=all # apprise notification tag - - NOTIFICATIONTIMES=8,12,17 # hours when notifications will be sent \ No newline at end of file + - NOTIFICATIONTIMES=8,12,17 # hours when notifications will be sent + - TZ=America/Chicago # set timezone from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List \ No newline at end of file From 55a14dbc4bbb99be2f75822fffe581d20dd4ded1 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Thu, 14 May 2026 21:54:55 -0500 Subject: [PATCH 06/19] updates to readme --- README.md | 71 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 420e109..432aa04 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ A small PowerShell notifier for [Donetick](https://github.com/donetick/donetick) chores. +> [!IMPORTANT] +> **AI Assistance Disclosure** +> +> OpenAI Codex was used to help create this README and the Gitea pipeline files in this repository. +> +> All released application code is written by the repository owner. + The notifier checks the Donetick external API for chores, groups tasks that are overdue or due today, and sends summary notifications through an Apprise-compatible webhook. ## What it does @@ -10,9 +17,9 @@ The notifier checks the Donetick external API for chores, groups tasks that are - Authenticates to Donetick with the `secretkey` header. - Sends one Apprise notification for overdue tasks, when any exist. - Sends one Apprise notification for tasks due today, when any exist. -- Runs once and exits. +- Runs continuously and sleeps until the next configured notification hour. -Because the container runs once and exits, schedule it with cron, systemd timers, Kubernetes CronJobs, Gitea Actions, or another scheduler if you want recurring notifications. +The container is intended to stay running. Configure the notification hours with `NOTIFICATIONTIMES`; the script wakes up at those hours, checks chores, sends any needed notifications, then sleeps until the next configured hour. ## Configuration @@ -25,6 +32,8 @@ All configuration is provided through environment variables. | `DONETICKAPIKEY` | Yes | Donetick external API key. Sent as the `secretkey` header. | | `APPRISEWEBHOOKURL` | Yes | Apprise webhook URL that accepts notification posts. | | `APPRISEWEBHOOKTAG` | Yes | Apprise tag value to include with each notification. | +| `NOTIFICATIONTIMES` | No | Comma-separated list of 24-hour clock hours when notifications should be sent, such as `8,12,17`. Defaults to `8` when unset. | +| `TZ` | No | Container timezone, such as `America/Chicago`. Recommended so notification hours match your local time. | ## Docker @@ -34,17 +43,19 @@ The published image is: docker.io/blinkfink182/donetick-notifier ``` -### Run Once +### Run ```sh -docker run --rm \ +docker run -d \ --name donetick-notifier \ -e DONETICKHOST=host.docker.internal \ -e DONETICKPORT=8787 \ -e DONETICKAPIKEY=your-donetick-api-key \ -e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \ -e APPRISEWEBHOOKTAG=all \ - docker.io/blinkfink182/donetick-notifier:latest + -e NOTIFICATIONTIMES=8,12,17 \ + -e TZ=America/Chicago \ + docker.io/blinkfink182/donetick-notifier ``` ### Docker Compose @@ -53,19 +64,21 @@ docker run --rm \ services: donetick-notifier: container_name: donetick-notifier - image: docker.io/blinkfink182/donetick-notifier:latest + image: docker.io/blinkfink182/donetick-notifier environment: - DONETICKHOST: host.docker.internal - DONETICKPORT: "8787" - DONETICKAPIKEY: your-donetick-api-key - APPRISEWEBHOOKURL: https://apprise.example.com/notify/config - APPRISEWEBHOOKTAG: all + - DONETICKHOST=host.docker.internal + - DONETICKPORT=8787 + - DONETICKAPIKEY=your-donetick-api-key + - APPRISEWEBHOOKURL=https://apprise.example.com/notify/config + - APPRISEWEBHOOKTAG=all + - NOTIFICATIONTIMES=8,12,17 + - TZ=America/Chicago ``` Run it with: ```sh -docker compose run --rm donetick-notifier +docker compose up -d ``` If Donetick and Apprise are running on the same Docker network, use the service names instead of `host.docker.internal`. @@ -73,13 +86,15 @@ If Donetick and Apprise are running on the same Docker network, use the service ```yaml services: donetick-notifier: - image: docker.io/blinkfink182/donetick-notifier:latest + image: docker.io/blinkfink182/donetick-notifier environment: - DONETICKHOST: donetick - DONETICKPORT: "8787" - DONETICKAPIKEY: your-donetick-api-key - APPRISEWEBHOOKURL: http://apprise:8000/notify/config - APPRISEWEBHOOKTAG: all + - DONETICKHOST=donetick + - DONETICKPORT=8787 + - DONETICKAPIKEY=your-donetick-api-key + - APPRISEWEBHOOKURL=http://apprise:8000/notify/config + - APPRISEWEBHOOKTAG=all + - NOTIFICATIONTIMES=8,12,17 + - TZ=America/Chicago ``` ## Build Locally @@ -89,12 +104,15 @@ docker build -t donetick-notifier . ``` ```sh -docker run --rm \ +docker run -d \ + --name donetick-notifier \ -e DONETICKHOST=host.docker.internal \ -e DONETICKPORT=8787 \ -e DONETICKAPIKEY=your-donetick-api-key \ -e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \ -e APPRISEWEBHOOKTAG=all \ + -e NOTIFICATIONTIMES=8,12,17 \ + -e TZ=America/Chicago \ donetick-notifier ``` @@ -108,29 +126,28 @@ $env:DONETICKPORT = "8787" $env:DONETICKAPIKEY = "your-donetick-api-key" $env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config" $env:APPRISEWEBHOOKTAG = "all" +$env:NOTIFICATIONTIMES = "8,12,17" pwsh ./Start-DoneTickNotifier.ps1 ``` +The script runs continuously. Stop it with `Ctrl+C` when running interactively. + ## CI/CD This repository includes Gitea workflows for: -- Building and pushing the Docker image. +- Building and pushing the Docker image on demand. - Running security checks with Gitleaks, Semgrep, and Trivy. - Creating Gitea issues for security findings when configured with a `GITEA_TOKEN`. - Sending Apprise notifications for Docker build success or failure. -The Docker build workflow tags images as: +The security workflow runs on pushes, pull requests, and manual dispatch. On pushes, it also pushes the scanned Docker image when all checks pass. + +Docker images are tagged as: | Branch or ref | Image tag | | --- | --- | | `main` | `latest` | | refs beginning with `v` | matching ref name, such as `v1.0.0` | | all other refs | `test` | - -## AI Assistance Disclosure - -OpenAI Codex was used to help create this README and the Gitea pipeline files in this repository. - -All released application code is written by the repository owner. From 7a0506bb5f81c3d973acbbb4d3401a670f55c548 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Wed, 27 May 2026 20:38:47 -0500 Subject: [PATCH 07/19] correctly changing task due timestamp to local time instead of leaving it in UTC fixes #7 --- Start-DoneTickNotifier.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index 10a14ca..244bc85 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -66,7 +66,7 @@ while ($true) { { if ($chore.nextDueDate) { - $dueDate = Get-Date $chore.nextDueDate + $dueDate = (Get-Date $chore.nextDueDate).ToLocalTime() if (($dueDate - $today).Days -lt 0) #OVERDUE { write-host "$($chore.name) $dueDate is overdue!" From 7bce5cabc9476cb01f2e05d6f3bbe1fbedbffad8 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sat, 30 May 2026 19:54:46 -0500 Subject: [PATCH 08/19] first attempt at a webhook consumer --- Dockerfile | 12 +++- README.md | 7 --- Start-DoneTickConsumer.ps1 | 123 +++++++++++++++++++++++++++++++++++++ crontab | 2 + main.sh | 13 ++++ 5 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 Start-DoneTickConsumer.ps1 create mode 100644 crontab create mode 100644 main.sh diff --git a/Dockerfile b/Dockerfile index 737b04d..a66ba9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,22 @@ FROM mcr.microsoft.com/powershell:7.5-ubuntu-24.04 USER root RUN apt-get update \ - && apt-get install -y --no-install-recommends tzdata \ + && apt-get install -y --no-install-recommends tzdata wget \ + && wget -q https://github.com/aptible/supercronic/releases/latest/download/supercronic-linux-amd64 -O /usr/local/bin/supercronic \ + && chmod +x /usr/local/bin/supercronic \ && apt-get dist-upgrade -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* + USER 1000:1000 WORKDIR /data +COPY ["crontab", "/data/"] +COPY ["main.sh", "/data/"] COPY ["Start-DoneTickNotifier.ps1", "/data/"] +COPY ["Start-DoneTickConsumer.ps1", "/data/"] -ENTRYPOINT ["pwsh", "-Command", "/data/Start-DoneTickNotifier.ps1"] +RUN chmod +x /data/main.sh + +ENTRYPOINT ["main.sh"] diff --git a/README.md b/README.md index 432aa04..7504e6c 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,6 @@ A small PowerShell notifier for [Donetick](https://github.com/donetick/donetick) chores. -> [!IMPORTANT] -> **AI Assistance Disclosure** -> -> OpenAI Codex was used to help create this README and the Gitea pipeline files in this repository. -> -> All released application code is written by the repository owner. - The notifier checks the Donetick external API for chores, groups tasks that are overdue or due today, and sends summary notifications through an Apprise-compatible webhook. ## What it does diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 new file mode 100644 index 0000000..56d7c71 --- /dev/null +++ b/Start-DoneTickConsumer.ps1 @@ -0,0 +1,123 @@ +[string] $ListenUrl = "http://localhost:8080/" + +function Write-JsonResponse +{ + param( + [Parameter(Mandatory=$true)] + [System.Net.HttpListenerResponse] + $Response, + + [Parameter(Mandatory=$true)] + [int] + $StatusCode, + + [Parameter(Mandatory=$true)] + [hashtable] + $Body + ) + + $json = $Body | ConvertTo-Json -Depth 10 + $bytes = [System.Text.Encoding]::UTF8.GetBytes($json) + + $Response.StatusCode = $StatusCode + $Response.ContentType = "application/json" + $Response.ContentEncoding = [System.Text.Encoding]::UTF8 + $Response.ContentLength64 = $bytes.Length + $Response.OutputStream.Write($bytes, 0, $bytes.Length) + $Response.OutputStream.Close() +} + +function Read-RequestBody +{ + param( + [Parameter(Mandatory=$true)] + [System.Net.HttpListenerRequest] + $Request + ) + + $reader = [System.IO.StreamReader]::new($Request.InputStream, $Request.ContentEncoding) + try { + return $reader.ReadToEnd() + } + finally { + $reader.Close() + } +} + +function Receive-Webhook +{ + param( + [Parameter(Mandatory=$true)] + [System.Net.HttpListenerContext] + $Context + ) + + $request = $Context.Request + $response = $Context.Response + + if ($request.HttpMethod -ne "POST") { + Write-JsonResponse -Response $response -StatusCode 405 -Body @{ + ok = $false + error = "Only POST requests are supported." + } + return + } + + $rawBody = Read-RequestBody -Request $request + $payload = $null + + try { + if ($rawBody) { + $payload = $rawBody | ConvertFrom-Json + } + } + catch { + Write-JsonResponse -Response $response -StatusCode 400 -Body @{ + ok = $false + error = "Request body was not valid JSON." + } + return + } + + Write-Host "Webhook received at $(Get-Date -Format o)" + Write-Host "From: $($request.RemoteEndPoint)" + Write-Host "Path: $($request.Url.AbsolutePath)" + + if ($payload) { + Write-Host "Payload:" + Write-Host ($payload | ConvertTo-Json -Depth 20) + } + else { + Write-Host "Payload: " + } + + # Add your real webhook handling here. + # Example: inspect $payload.event, save data, or call another API. + + Write-JsonResponse -Response $response -StatusCode 200 -Body @{ + ok = $true + message = "Webhook accepted." + receivedAt = (Get-Date -Format o) + } +} + +$listener = [System.Net.HttpListener]::new() +$listener.Prefixes.Add($ListenUrl) + +try { + $listener.Start() + Write-Host "Webhook consumer listening on $ListenUrl" + Write-Host "Press Ctrl+C to stop." + + while ($listener.IsListening) { + $context = $listener.GetContext() + Receive-Webhook -Context $context + } +} +finally { + if ($listener.IsListening) { + $listener.Stop() + } + + $listener.Close() +} diff --git a/crontab b/crontab new file mode 100644 index 0000000..4c6e11a --- /dev/null +++ b/crontab @@ -0,0 +1,2 @@ +# Default schedule (overridden at runtime by JOB_SCHEDULE env var) +0 * * * * pwsh /data/Start-DoneTickNotifier.ps1 \ No newline at end of file diff --git a/main.sh b/main.sh new file mode 100644 index 0000000..c0226c2 --- /dev/null +++ b/main.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +# Build the crontab dynamically from the env variable +echo "${JOB_SCHEDULE} pwsh /data/Start-DoneTineNotifier.ps1" > /tmp/crontab-runtime + +echo "Cron schedule set to: ${JOB_SCHEDULE}" + +# Start supercronic in background using the generated crontab +supercronic /tmp/crontab-runtime & + +# Start the HTTP listener in foreground +exec pwsh /data/Start-DoneTickConsumer.ps1 \ No newline at end of file From 505fe5fa7dad8e988e74ec30c05f540e0e258449 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sat, 30 May 2026 19:58:33 -0500 Subject: [PATCH 09/19] correcting dockerfile errors --- Dockerfile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index a66ba9f..cda5c98 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,10 +15,7 @@ USER 1000:1000 WORKDIR /data COPY ["crontab", "/data/"] -COPY ["main.sh", "/data/"] -COPY ["Start-DoneTickNotifier.ps1", "/data/"] -COPY ["Start-DoneTickConsumer.ps1", "/data/"] +COPY --chmod=755 ["main.sh", "/data/"] +COPY ["Start-DoneTickNotifier.ps1", "Start-DoneTickConsumer.ps1", "/data/"] -RUN chmod +x /data/main.sh - -ENTRYPOINT ["main.sh"] +ENTRYPOINT ["/data/main.sh"] From d3d22892d2c1ecd5710a880a7a2e0c21c5e46fea Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sun, 31 May 2026 18:29:31 -0500 Subject: [PATCH 10/19] updated dockerfile, example compose, and corrected typo --- Dockerfile | 2 ++ docker-compose.yaml | 5 ++++- main.sh | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index cda5c98..87dae40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,3 +19,5 @@ COPY --chmod=755 ["main.sh", "/data/"] COPY ["Start-DoneTickNotifier.ps1", "Start-DoneTickConsumer.ps1", "/data/"] ENTRYPOINT ["/data/main.sh"] + +EXPOSE 8080 \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 702a23d..66d4cdf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,6 +3,8 @@ services: donetick-notifier: container_name: donetick-notifier image: docker.io/blinkfink182/donetick-notifier + ports: + - 8080:8080 environment: # BELOW ARE ALL REQUIRED - DONETICKHOST=host.docker.internal # donetick host @@ -11,4 +13,5 @@ services: - APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url - APPRISEWEBHOOKTAG=all # apprise notification tag - NOTIFICATIONTIMES=8,12,17 # hours when notifications will be sent - - TZ=America/Chicago # set timezone from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List \ No newline at end of file + - TZ=America/Chicago # set timezone from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List + - JOB_SCHEDULE=0 * * * * \ No newline at end of file diff --git a/main.sh b/main.sh index c0226c2..43e3a4f 100644 --- a/main.sh +++ b/main.sh @@ -2,7 +2,7 @@ set -e # Build the crontab dynamically from the env variable -echo "${JOB_SCHEDULE} pwsh /data/Start-DoneTineNotifier.ps1" > /tmp/crontab-runtime +echo "${JOB_SCHEDULE} pwsh /data/Start-DoneTickNotifier.ps1" > /tmp/crontab-runtime echo "Cron schedule set to: ${JOB_SCHEDULE}" From 1e01db0889bd1c845adb8151a93446be13ee0d43 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sun, 31 May 2026 19:52:09 -0500 Subject: [PATCH 11/19] more time zone corrections and removing the loop in the notifier script as it now runs via a cron schedule --- Start-DoneTickNotifier.ps1 | 106 +++++++++++++------------------------ docker-compose.yaml | 3 +- 2 files changed, 37 insertions(+), 72 deletions(-) diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index 244bc85..6a94eee 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -3,7 +3,6 @@ [string] $dtAPIKey = $ENV:DONETICKAPIKEY [string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL [string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG -[int[]] $notificationTimes = $ENV:NOTIFICATIONTIMES -split "," function Send-Notification @@ -51,87 +50,54 @@ function Get-Chores } } -if (-not $notificationTimes) { $notificationTimes = @(8) } -else { $notificationTimes = $notificationTimes | Sort-Object } -Write-Host "Notification times: $notificationTimes" +$today = (Get-Date "23:59:59") +$chores = Get-Chores -while ($true) { - $today = (Get-Date "23:59:59") - $chores = Get-Chores - - $overdueTasks = @() - $todaysTasks = @() - - foreach($chore in $chores) +$overdueTasks = @() +$todaysTasks = @() + +foreach($chore in $chores) +{ + if ($chore.nextDueDate) { - if ($chore.nextDueDate) + $dueDate = (Get-Date $chore.nextDueDate).ToLocalTime() + if (($dueDate - $today).Days -lt 0) #OVERDUE { - $dueDate = (Get-Date $chore.nextDueDate).ToLocalTime() - 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" - } + write-host "$($chore.name) $dueDate is overdue!" + $overdueTasks += $chore } - - } - - 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) + elseif (($dueDate - $today) -lt 1) #due today { - $content += "$($overdueTask.Name) was due $(Get-Date $overdueTask.nextDueDate -Format "MM/dd/yyyy HH:mm")`n" + write-host "$($chore.name) $dueDate is due today!" + $todaysTasks += $chore + # Send-Notification -title "TASK DUE TODAY" -content "$($chore.Name) is due today!" } - 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) + else { - $content += "$($task.Name) is due $(Get-Date $task.nextDueDate -Format "MM/dd/yyyy HH:mm")`n" + write-host "$($chore.name) $dueDate is due in the future" } - Send-Notification -title "TODAY'S TASKS" -content $content } - Write-Host "Finding next notification time..." - Write-Host "Current time: $(Get-Date)" - $nextNotificationTomorrow = $true - foreach($time in $notificationTimes) +} + +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) { - $now = Get-Date - $diff = $now.Hour - $time - if ($diff -lt 0) # next notification time - { - $nextNotificationTomorrow = $false - Write-Host "Next notification time is $time`:00" - $sleepTime = ($diff * -1) * 60 * 60 # hours * mins * seconds - Write-Host "Sleeping for $sleepTime seconds" - Start-Sleep -Seconds $sleepTime - continue # leave loop - } + $content += "$($overdueTask.Name) was due $((Get-Date $overdueTask.nextDueDate -Format "MM/dd/yyyy HH:mm").ToLocalTime())`n" } - if ($nextNotificationTomorrow) + 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) { - $time = $notificationTimes[0] - Write-Host "Next notification time is $time`:00" - $diff = 24 - $now.Hour + $time - $sleepTime = $diff * 60 * 60 # hours * mins * seconds - Write-Host "Sleeping for $sleepTime seconds" - Start-Sleep -Seconds $sleepTime + $content += "$($task.Name) is due $((Get-Date $task.nextDueDate -Format "MM/dd/yyyy HH:mm").ToLocalTime())`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 index 66d4cdf..609c08f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,6 +12,5 @@ services: - DONETICKAPIKEY=adminpass # donetick API key - APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url - APPRISEWEBHOOKTAG=all # apprise notification tag - - NOTIFICATIONTIMES=8,12,17 # hours when notifications will be sent - TZ=America/Chicago # set timezone from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List - - JOB_SCHEDULE=0 * * * * \ No newline at end of file + - JOB_SCHEDULE=0 8,17 * * * # when to notify about overdue tasks or tasks due today \ No newline at end of file From 9a1b61cfd1ff8e040ad8f073c614b5abb323efd7 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sun, 31 May 2026 19:59:34 -0500 Subject: [PATCH 12/19] corrected listening url from localhost to all adapters --- Start-DoneTickConsumer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 index 56d7c71..d9e8f45 100644 --- a/Start-DoneTickConsumer.ps1 +++ b/Start-DoneTickConsumer.ps1 @@ -1,4 +1,4 @@ -[string] $ListenUrl = "http://localhost:8080/" +[string] $ListenUrl = "http://0.0.0.0:8080/" function Write-JsonResponse { From d0ac1d8e4c7ac875279d4f6180196526ac487fb6 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sun, 31 May 2026 20:06:54 -0500 Subject: [PATCH 13/19] timezone bugs, changed webhook listener to use + since posh doesn't allow 0.0.0.0 --- Start-DoneTickConsumer.ps1 | 2 +- Start-DoneTickNotifier.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 index d9e8f45..49f5338 100644 --- a/Start-DoneTickConsumer.ps1 +++ b/Start-DoneTickConsumer.ps1 @@ -1,4 +1,4 @@ -[string] $ListenUrl = "http://0.0.0.0:8080/" +[string] $ListenUrl = "http://+:8080/" function Write-JsonResponse { diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index 6a94eee..2b670cd 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -86,7 +86,7 @@ if ($overdueTasks.Count -ne 0) $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").ToLocalTime())`n" + $content += "$($overdueTask.Name) was due $((Get-Date $overdueTask.nextDueDate).ToLocalTime())`n" } Send-Notification -title "OVERDUE TASKS" -content $content } @@ -97,7 +97,7 @@ if ($todaysTasks.Count -ne 0) $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").ToLocalTime())`n" + $content += "$($task.Name) is due $((Get-Date $task.nextDueDate).ToLocalTime())`n" } Send-Notification -title "TODAY'S TASKS" -content $content } \ No newline at end of file From 76052a87e412f154950804fe38791a8ffb6344a5 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sun, 31 May 2026 20:23:49 -0500 Subject: [PATCH 14/19] broke apprise notification function into its own module to be used by multiple scripts --- AppriseNotification.psm1 | 33 ++++++++++++++++++++++++++++++++ Dockerfile | 2 +- Start-DoneTickConsumer.ps1 | 5 +++-- Start-DoneTickNotifier.ps1 | 39 +++----------------------------------- 4 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 AppriseNotification.psm1 diff --git a/AppriseNotification.psm1 b/AppriseNotification.psm1 new file mode 100644 index 0000000..57921c8 --- /dev/null +++ b/AppriseNotification.psm1 @@ -0,0 +1,33 @@ +[string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL +[string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG + +function Send-AppriseNotification +{ + 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])" + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 87dae40..458b630 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ USER 1000:1000 WORKDIR /data COPY ["crontab", "/data/"] COPY --chmod=755 ["main.sh", "/data/"] -COPY ["Start-DoneTickNotifier.ps1", "Start-DoneTickConsumer.ps1", "/data/"] +COPY ["Start-DoneTickNotifier.ps1", "Start-DoneTickConsumer.ps1", "AppriseNotification.psm1", "/data/"] ENTRYPOINT ["/data/main.sh"] diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 index 49f5338..31ad7b4 100644 --- a/Start-DoneTickConsumer.ps1 +++ b/Start-DoneTickConsumer.ps1 @@ -1,5 +1,7 @@ [string] $ListenUrl = "http://+:8080/" +Import-Module ./AppriseNotification.psm1 + function Write-JsonResponse { param( @@ -91,8 +93,7 @@ function Receive-Webhook Write-Host "Payload: " } - # Add your real webhook handling here. - # Example: inspect $payload.event, save data, or call another API. + Send-AppriseNotification -title "Donetick Webhook Received" -content ($payload | ConvertTo-Json -Depth 20) Write-JsonResponse -Response $response -StatusCode 200 -Body @{ ok = $true diff --git a/Start-DoneTickNotifier.ps1 b/Start-DoneTickNotifier.ps1 index 2b670cd..db0f3c7 100644 --- a/Start-DoneTickNotifier.ps1 +++ b/Start-DoneTickNotifier.ps1 @@ -1,40 +1,8 @@ [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])" - } -} +Import-Module ./AppriseNotification.psm1 function Get-Chores { @@ -70,7 +38,6 @@ foreach($chore in $chores) { write-host "$($chore.name) $dueDate is due today!" $todaysTasks += $chore - # Send-Notification -title "TASK DUE TODAY" -content "$($chore.Name) is due today!" } else { @@ -88,7 +55,7 @@ if ($overdueTasks.Count -ne 0) { $content += "$($overdueTask.Name) was due $((Get-Date $overdueTask.nextDueDate).ToLocalTime())`n" } - Send-Notification -title "OVERDUE TASKS" -content $content + Send-AppriseNotification -title "OVERDUE TASKS" -content $content } if ($todaysTasks.Count -ne 0) @@ -99,5 +66,5 @@ if ($todaysTasks.Count -ne 0) { $content += "$($task.Name) is due $((Get-Date $task.nextDueDate).ToLocalTime())`n" } - Send-Notification -title "TODAY'S TASKS" -content $content + Send-AppriseNotification -title "TODAY'S TASKS" -content $content } \ No newline at end of file From e343545254e70cf1da8ac93a59efca05051bf33e Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Sun, 31 May 2026 20:30:37 -0500 Subject: [PATCH 15/19] simple handling of events --- Start-DoneTickConsumer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 index 31ad7b4..a91b22d 100644 --- a/Start-DoneTickConsumer.ps1 +++ b/Start-DoneTickConsumer.ps1 @@ -93,7 +93,7 @@ function Receive-Webhook Write-Host "Payload: " } - Send-AppriseNotification -title "Donetick Webhook Received" -content ($payload | ConvertTo-Json -Depth 20) + Send-AppriseNotification -title "Donetick Webhook Received" -content "$($payload.type) $($payload.data.chore.name)" Write-JsonResponse -Response $response -StatusCode 200 -Body @{ ok = $true From a3c65ecdef71b24805a3289d3e15bcb860e66f25 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Tue, 2 Jun 2026 20:20:15 -0500 Subject: [PATCH 16/19] added detailed handling of event types --- Start-DoneTickConsumer.ps1 | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 index a91b22d..71246ed 100644 --- a/Start-DoneTickConsumer.ps1 +++ b/Start-DoneTickConsumer.ps1 @@ -93,7 +93,35 @@ function Receive-Webhook Write-Host "Payload: " } - Send-AppriseNotification -title "Donetick Webhook Received" -content "$($payload.type) $($payload.data.chore.name)" + $notificationTitle = "" + $notificationContent = "" + switch($payload.type) + { + "task.completed" { + $notificationTitle = "Donetick Task Completed" + $notificationContent = "$($payload.data.chore.name) marked completed!" + } + "task.reminder" { + $notificationTitle = "Donetick Task Reminder" + $notificationContent = "$($payload.data.name) is due at $(Get-Date $payload.data.due_date)" + } + "task.created" { + $notificationTitle = "Donetick Task Created" + if ($null -ne $payload.data.chore.nextDueDate) + { + $notificationContent = "$($payload.data.chore.name) created" + } + else + { + $notificationContent = "$($payload.data.chore.name) created with due date of $(Get-Date $payload.data.chore.nextDueDate)" + } + } + default { + $notificationTitle = "Donetick Notification" + $notificationContent = "Donetick event of type $($payload.type) received" + } + } + Send-AppriseNotification -title $notificationTitle -content $notificationContent Write-JsonResponse -Response $response -StatusCode 200 -Body @{ ok = $true From 68be07e01d7119f9dea8434d830b78c2a89203a5 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Tue, 2 Jun 2026 20:30:58 -0500 Subject: [PATCH 17/19] resolving #9 --- Start-DoneTickConsumer.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 index 71246ed..48dcd1e 100644 --- a/Start-DoneTickConsumer.ps1 +++ b/Start-DoneTickConsumer.ps1 @@ -107,7 +107,7 @@ function Receive-Webhook } "task.created" { $notificationTitle = "Donetick Task Created" - if ($null -ne $payload.data.chore.nextDueDate) + if ($null -eq $payload.data.chore.nextDueDate) { $notificationContent = "$($payload.data.chore.name) created" } From 075c6079c37257d76ed35a6d324dcef3a11282e5 Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Tue, 2 Jun 2026 20:35:10 -0500 Subject: [PATCH 18/19] resolving #10 completing #8 --- Start-DoneTickConsumer.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Start-DoneTickConsumer.ps1 b/Start-DoneTickConsumer.ps1 index 48dcd1e..85cf29c 100644 --- a/Start-DoneTickConsumer.ps1 +++ b/Start-DoneTickConsumer.ps1 @@ -103,7 +103,7 @@ function Receive-Webhook } "task.reminder" { $notificationTitle = "Donetick Task Reminder" - $notificationContent = "$($payload.data.name) is due at $(Get-Date $payload.data.due_date)" + $notificationContent = "$($payload.data.name) is due at $((Get-Date $payload.data.due_date).ToLocalTime())" } "task.created" { $notificationTitle = "Donetick Task Created" @@ -113,7 +113,7 @@ function Receive-Webhook } else { - $notificationContent = "$($payload.data.chore.name) created with due date of $(Get-Date $payload.data.chore.nextDueDate)" + $notificationContent = "$($payload.data.chore.name) created with due date of $((Get-Date $payload.data.chore.nextDueDate).ToLocalTime())" } } default { From 2d45156a0809be16197002ebaae309542cb750fd Mon Sep 17 00:00:00 2001 From: Kelly Thomas Reardon Date: Tue, 2 Jun 2026 20:45:52 -0500 Subject: [PATCH 19/19] updated readme for v0.1 release --- README.md | 82 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 7504e6c..a4942e9 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,33 @@ # donetick-notifier -A small PowerShell notifier for [Donetick](https://github.com/donetick/donetick) chores. +> AI disclaimer: AI was used solely for creating this README, the Gitea pipelines, and the base of the webhook consumer. -The notifier checks the Donetick external API for chores, groups tasks that are overdue or due today, and sends summary notifications through an Apprise-compatible webhook. +A PowerShell-based notifier and webhook consumer for [Donetick](https://github.com/donetick/donetick) chores. -## What it does +The project now has two runtime paths: + +- A scheduled notifier that checks the Donetick external API for chores and sends Apprise-compatible summary notifications for overdue and due-today tasks. +- A webhook consumer that listens on port `8080`, accepts Donetick webhook payloads, and turns them into Apprise notifications. + +## Features - Fetches chores from `https://:/eapi/v1/chore`. - Authenticates to Donetick with the `secretkey` header. -- Sends one Apprise notification for overdue tasks, when any exist. -- Sends one Apprise notification for tasks due today, when any exist. -- Runs continuously and sleeps until the next configured notification hour. +- Sends one notification for overdue chores when any exist. +- Sends one notification for chores due today when any exist. +- Accepts webhook events for `task.completed`, `task.reminder`, and `task.created`. +- Falls back to a generic notification for unknown webhook event types. +- Runs the scheduler and webhook listener together in the Docker container. +- Includes Gitea pipelines for Docker image publishing and security scanning. -The container is intended to stay running. Configure the notification hours with `NOTIFICATIONTIMES`; the script wakes up at those hours, checks chores, sends any needed notifications, then sleeps until the next configured hour. +## How It Runs + +The container entrypoint is `main.sh`. + +- `supercronic` runs `Start-DoneTickNotifier.ps1` on the schedule defined by `JOB_SCHEDULE`. +- `Start-DoneTickConsumer.ps1` runs in the foreground and listens on `http://+:8080/`. + +The notifier and consumer both use `AppriseNotification.psm1` to send messages to the configured Apprise webhook. ## Configuration @@ -25,28 +40,25 @@ All configuration is provided through environment variables. | `DONETICKAPIKEY` | Yes | Donetick external API key. Sent as the `secretkey` header. | | `APPRISEWEBHOOKURL` | Yes | Apprise webhook URL that accepts notification posts. | | `APPRISEWEBHOOKTAG` | Yes | Apprise tag value to include with each notification. | -| `NOTIFICATIONTIMES` | No | Comma-separated list of 24-hour clock hours when notifications should be sent, such as `8,12,17`. Defaults to `8` when unset. | +| `JOB_SCHEDULE` | Yes for Docker | Cron expression used by the container scheduler, such as `0 8,17 * * *`. | | `TZ` | No | Container timezone, such as `America/Chicago`. Recommended so notification hours match your local time. | ## Docker -The published image is: - -```text -docker.io/blinkfink182/donetick-notifier -``` +The published image is `docker.io/blinkfink182/donetick-notifier`. ### Run ```sh docker run -d \ --name donetick-notifier \ + -p 8080:8080 \ -e DONETICKHOST=host.docker.internal \ -e DONETICKPORT=8787 \ -e DONETICKAPIKEY=your-donetick-api-key \ -e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \ -e APPRISEWEBHOOKTAG=all \ - -e NOTIFICATIONTIMES=8,12,17 \ + -e JOB_SCHEDULE="0 8,17 * * *" \ -e TZ=America/Chicago \ docker.io/blinkfink182/donetick-notifier ``` @@ -58,13 +70,15 @@ services: donetick-notifier: container_name: donetick-notifier image: docker.io/blinkfink182/donetick-notifier + ports: + - 8080:8080 environment: - DONETICKHOST=host.docker.internal - DONETICKPORT=8787 - DONETICKAPIKEY=your-donetick-api-key - APPRISEWEBHOOKURL=https://apprise.example.com/notify/config - APPRISEWEBHOOKTAG=all - - NOTIFICATIONTIMES=8,12,17 + - JOB_SCHEDULE=0 8,17 * * * - TZ=America/Chicago ``` @@ -74,19 +88,21 @@ Run it with: docker compose up -d ``` -If Donetick and Apprise are running on the same Docker network, use the service names instead of `host.docker.internal`. +If Donetick and Apprise are on the same Docker network, use service names instead of `host.docker.internal`. ```yaml services: donetick-notifier: image: docker.io/blinkfink182/donetick-notifier + ports: + - 8080:8080 environment: - DONETICKHOST=donetick - DONETICKPORT=8787 - DONETICKAPIKEY=your-donetick-api-key - APPRISEWEBHOOKURL=http://apprise:8000/notify/config - APPRISEWEBHOOKTAG=all - - NOTIFICATIONTIMES=8,12,17 + - JOB_SCHEDULE=0 8,17 * * * - TZ=America/Chicago ``` @@ -99,12 +115,13 @@ docker build -t donetick-notifier . ```sh docker run -d \ --name donetick-notifier \ + -p 8080:8080 \ -e DONETICKHOST=host.docker.internal \ -e DONETICKPORT=8787 \ -e DONETICKAPIKEY=your-donetick-api-key \ -e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \ -e APPRISEWEBHOOKTAG=all \ - -e NOTIFICATIONTIMES=8,12,17 \ + -e JOB_SCHEDULE="0 8,17 * * *" \ -e TZ=America/Chicago \ donetick-notifier ``` @@ -113,34 +130,41 @@ docker run -d \ PowerShell 7 or newer is recommended. +Scheduled notifier: + ```powershell $env:DONETICKHOST = "donetick.example.com" $env:DONETICKPORT = "8787" $env:DONETICKAPIKEY = "your-donetick-api-key" $env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config" $env:APPRISEWEBHOOKTAG = "all" -$env:NOTIFICATIONTIMES = "8,12,17" pwsh ./Start-DoneTickNotifier.ps1 ``` -The script runs continuously. Stop it with `Ctrl+C` when running interactively. +Webhook consumer: + +```powershell +$env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config" +$env:APPRISEWEBHOOKTAG = "all" + +pwsh ./Start-DoneTickConsumer.ps1 +``` + +The consumer listens on port `8080` by default. Stop either script with `Ctrl+C` when running interactively. ## CI/CD This repository includes Gitea workflows for: -- Building and pushing the Docker image on demand. +- Building and pushing the Docker image on manual dispatch. +- Tagging images as `latest` for `main`, as the ref name for `v*` tags, and as `test` for everything else. - Running security checks with Gitleaks, Semgrep, and Trivy. -- Creating Gitea issues for security findings when configured with a `GITEA_TOKEN`. +- Creating Gitea issues for security findings when `GITEA_TOKEN` is configured. - Sending Apprise notifications for Docker build success or failure. -The security workflow runs on pushes, pull requests, and manual dispatch. On pushes, it also pushes the scanned Docker image when all checks pass. +The security workflow runs on pushes, pull requests, and manual dispatch. On push, it also builds the image so Trivy can scan the resulting container. -Docker images are tagged as: +## API References -| Branch or ref | Image tag | -| --- | --- | -| `main` | `latest` | -| refs beginning with `v` | matching ref name, such as `v1.0.0` | -| all other refs | `test` | +The `Donetick/` directory contains request examples and collection metadata used to document and exercise the Donetick API while developing the notifier.