Compare commits

..

20 Commits

Author SHA1 Message Date
kelly 74b2b2b8ab Merge pull request 'V0.1 release' (#11) from v0.1 into main
Security / security (push) Successful in 59s
Reviewed-on: #11
2026-06-02 20:49:07 -05:00
kelly 2d45156a08 updated readme for v0.1 release
Security / security (push) Successful in 58s
Security / security (pull_request) Successful in 53s
2026-06-02 20:45:52 -05:00
kelly 075c6079c3 resolving #10
Security / security (push) Successful in 58s
completing #8
2026-06-02 20:35:10 -05:00
kelly 68be07e01d resolving #9
Security / security (push) Successful in 1m2s
2026-06-02 20:30:58 -05:00
kelly a3c65ecdef added detailed handling of event types
Security / security (push) Successful in 1m10s
2026-06-02 20:20:15 -05:00
kelly e343545254 simple handling of events
Security / security (push) Successful in 57s
2026-05-31 20:30:37 -05:00
kelly 76052a87e4 broke apprise notification function into its own module to be used by multiple scripts
Security / security (push) Successful in 58s
2026-05-31 20:23:49 -05:00
kelly d0ac1d8e4c timezone bugs, changed webhook listener to use + since posh doesn't allow 0.0.0.0
Security / security (push) Successful in 58s
2026-05-31 20:06:54 -05:00
kelly 9a1b61cfd1 corrected listening url from localhost to all adapters
Security / security (push) Successful in 1m2s
2026-05-31 19:59:34 -05:00
kelly 1e01db0889 more time zone corrections and removing the loop in the notifier script as it now runs via a cron schedule
Security / security (push) Successful in 1m1s
2026-05-31 19:52:09 -05:00
kelly d3d22892d2 updated dockerfile, example compose, and corrected typo
Security / security (push) Successful in 1m4s
2026-05-31 18:29:31 -05:00
kelly 505fe5fa7d correcting dockerfile errors
Security / security (push) Successful in 42s
2026-05-30 19:58:33 -05:00
kelly 7bce5cabc9 first attempt at a webhook consumer
Security / security (push) Failing after 1m1s
2026-05-30 19:54:46 -05:00
kelly 7a0506bb5f correctly changing task due timestamp to local time instead of leaving it in UTC
Security / security (push) Successful in 2m14s
fixes #7
2026-05-27 20:38:47 -05:00
kelly 55a14dbc4b updates to readme 2026-05-14 21:54:55 -05:00
kelly 3a39a39be8 allowing for setting timezone
Security / security (push) Successful in 2m5s
2026-05-14 21:20:00 -05:00
kelly 1dc15708b9 correcting notification times not being seen as ints
Security / security (push) Successful in 1m4s
2026-05-13 21:36:58 -05:00
kelly adf42df4a7 fixed a bug for when the next notification time is tomorrow
Security / security (push) Successful in 1m6s
2026-05-13 21:21:06 -05:00
kelly 998f639a55 enable looping so the script can handle scheduling
Security / security (push) Successful in 1m7s
2026-05-13 21:09:52 -05:00
kelly 90a2805339 first draft of readme 2026-05-13 13:52:05 -05:00
8 changed files with 389 additions and 43 deletions
+33
View File
@@ -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])"
}
}
+10 -3
View File
@@ -3,14 +3,21 @@ 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 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 ["Start-DoneTickNotifier.ps1", "/data/"]
COPY ["crontab", "/data/"]
COPY --chmod=755 ["main.sh", "/data/"]
COPY ["Start-DoneTickNotifier.ps1", "Start-DoneTickConsumer.ps1", "AppriseNotification.psm1", "/data/"]
ENTRYPOINT ["pwsh", "-Command", "/data/Start-DoneTickNotifier.ps1"]
ENTRYPOINT ["/data/main.sh"]
EXPOSE 8080
+168
View File
@@ -1,2 +1,170 @@
# donetick-notifier
> AI disclaimer: AI was used solely for creating this README, the Gitea pipelines, and the base of the webhook consumer.
A PowerShell-based notifier and webhook consumer for [Donetick](https://github.com/donetick/donetick) chores.
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://<DONETICKHOST>:<DONETICKPORT>/eapi/v1/chore`.
- Authenticates to Donetick with the `secretkey` header.
- 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.
## 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
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. |
| `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 `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 JOB_SCHEDULE="0 8,17 * * *" \
-e TZ=America/Chicago \
docker.io/blinkfink182/donetick-notifier
```
### Docker Compose
```yaml
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
- JOB_SCHEDULE=0 8,17 * * *
- TZ=America/Chicago
```
Run it with:
```sh
docker compose up -d
```
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
- JOB_SCHEDULE=0 8,17 * * *
- TZ=America/Chicago
```
## Build Locally
```sh
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 JOB_SCHEDULE="0 8,17 * * *" \
-e TZ=America/Chicago \
donetick-notifier
```
## Run Without Docker
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"
pwsh ./Start-DoneTickNotifier.ps1
```
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 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 `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 push, it also builds the image so Trivy can scan the resulting container.
## API References
The `Donetick/` directory contains request examples and collection metadata used to document and exercise the Donetick API while developing the notifier.
+152
View File
@@ -0,0 +1,152 @@
[string] $ListenUrl = "http://+:8080/"
Import-Module ./AppriseNotification.psm1
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: <empty>"
}
$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).ToLocalTime())"
}
"task.created" {
$notificationTitle = "Donetick Task Created"
if ($null -eq $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).ToLocalTime())"
}
}
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
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()
}
+6 -39
View File
@@ -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
{
@@ -60,7 +28,7 @@ foreach($chore in $chores)
{
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!"
@@ -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
{
@@ -86,9 +53,9 @@ 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")`n"
$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)
@@ -97,7 +64,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")`n"
$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
}
+2
View File
@@ -0,0 +1,2 @@
# Default schedule (overridden at runtime by JOB_SCHEDULE env var)
0 * * * * pwsh /data/Start-DoneTickNotifier.ps1
+5 -1
View File
@@ -3,10 +3,14 @@ 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
- DONETICKPORT=8787 # donetick port
- DONETICKAPIKEY=adminpass # donetick API key
- APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url
- APPRISEWEBHOOKTAG=all # apprise notification tag
- APPRISEWEBHOOKTAG=all # apprise notification tag
- TZ=America/Chicago # set timezone from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
- JOB_SCHEDULE=0 8,17 * * * # when to notify about overdue tasks or tasks due today
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -e
# Build the crontab dynamically from the env variable
echo "${JOB_SCHEDULE} pwsh /data/Start-DoneTickNotifier.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