Merge pull request 'V0.1 release' (#11) from v0.1 into main
Security / security (push) Successful in 59s

Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
2026-06-02 20:49:07 -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