Compare commits
15 Commits
7a0506bb5f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 67814d960f | |||
| 03eb431beb | |||
| 74b2b2b8ab | |||
| 2d45156a08 | |||
| 075c6079c3 | |||
| 68be07e01d | |||
| a3c65ecdef | |||
| e343545254 | |||
| 76052a87e4 | |||
| d0ac1d8e4c | |||
| 9a1b61cfd1 | |||
| 1e01db0889 | |||
| d3d22892d2 | |||
| 505fe5fa7d | |||
| 7bce5cabc9 |
@@ -2,9 +2,15 @@ name: Security
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- testing
|
||||||
|
- "v*"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- main
|
||||||
|
- testing
|
||||||
|
- "v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -181,6 +187,8 @@ jobs:
|
|||||||
"Authorization": f"token {token}",
|
"Authorization": f"token {token}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
vulnerability_label_name = "vulnerability"
|
||||||
|
vulnerability_label_id = None
|
||||||
|
|
||||||
def load_json(path, fallback):
|
def load_json(path, fallback):
|
||||||
try:
|
try:
|
||||||
@@ -203,11 +211,47 @@ jobs:
|
|||||||
issues = json.load(response)
|
issues = json.load(response)
|
||||||
return any(issue.get("title") == title for issue in issues)
|
return any(issue.get("title") == title for issue in issues)
|
||||||
|
|
||||||
|
def ensure_vulnerability_label():
|
||||||
|
global vulnerability_label_id
|
||||||
|
|
||||||
|
if vulnerability_label_id is not None:
|
||||||
|
return vulnerability_label_id
|
||||||
|
|
||||||
|
labels_url = f"{api_url}/repos/{urllib.parse.quote(owner)}/{urllib.parse.quote(name)}/labels"
|
||||||
|
|
||||||
|
request = urllib.request.Request(labels_url, headers=headers)
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
labels = json.load(response)
|
||||||
|
|
||||||
|
for label in labels:
|
||||||
|
if label.get("name") == vulnerability_label_name:
|
||||||
|
vulnerability_label_id = label.get("id")
|
||||||
|
return vulnerability_label_id
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"name": vulnerability_label_name,
|
||||||
|
"color": "d73a4a",
|
||||||
|
"description": "Security vulnerability found by automated scans",
|
||||||
|
"exclusive": False,
|
||||||
|
"is_archived": False,
|
||||||
|
}).encode("utf-8")
|
||||||
|
request = urllib.request.Request(labels_url, data=payload, headers=headers, method="POST")
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
created = json.load(response)
|
||||||
|
|
||||||
|
vulnerability_label_id = created.get("id")
|
||||||
|
return vulnerability_label_id
|
||||||
|
|
||||||
def create_issue(title, body):
|
def create_issue(title, body):
|
||||||
if find_existing(title):
|
if find_existing(title):
|
||||||
print(f"Open issue already exists: {title}")
|
print(f"Open issue already exists: {title}")
|
||||||
return
|
return
|
||||||
payload = json.dumps({"title": title, "body": body}).encode("utf-8")
|
label_id = ensure_vulnerability_label()
|
||||||
|
payload = json.dumps({
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"labels": [label_id] if label_id is not None else [],
|
||||||
|
}).encode("utf-8")
|
||||||
request = urllib.request.Request(issues_url, data=payload, headers=headers, method="POST")
|
request = urllib.request.Request(issues_url, data=payload, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(request, timeout=30) as response:
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
created = json.load(response)
|
created = json.load(response)
|
||||||
|
|||||||
@@ -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
@@ -3,14 +3,21 @@ FROM mcr.microsoft.com/powershell:7.5-ubuntu-24.04
|
|||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN apt-get update \
|
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 dist-upgrade -y \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
USER 1000:1000
|
USER 1000:1000
|
||||||
|
|
||||||
WORKDIR /data
|
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
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
# donetick-notifier
|
# 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.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
A PowerShell-based notifier and webhook consumer for [Donetick](https://github.com/donetick/donetick) chores.
|
||||||
> **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.
|
The project now has two runtime paths:
|
||||||
|
|
||||||
## What it does
|
- 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`.
|
- Fetches chores from `https://<DONETICKHOST>:<DONETICKPORT>/eapi/v1/chore`.
|
||||||
- Authenticates to Donetick with the `secretkey` header.
|
- Authenticates to Donetick with the `secretkey` header.
|
||||||
- Sends one Apprise notification for overdue tasks, when any exist.
|
- Sends one notification for overdue chores when any exist.
|
||||||
- Sends one Apprise notification for tasks due today, when any exist.
|
- Sends one notification for chores due today when any exist.
|
||||||
- Runs continuously and sleeps until the next configured notification hour.
|
- 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
|
## Configuration
|
||||||
|
|
||||||
@@ -32,28 +40,25 @@ All configuration is provided through environment variables.
|
|||||||
| `DONETICKAPIKEY` | Yes | Donetick external API key. Sent as the `secretkey` header. |
|
| `DONETICKAPIKEY` | Yes | Donetick external API key. Sent as the `secretkey` header. |
|
||||||
| `APPRISEWEBHOOKURL` | Yes | Apprise webhook URL that accepts notification posts. |
|
| `APPRISEWEBHOOKURL` | Yes | Apprise webhook URL that accepts notification posts. |
|
||||||
| `APPRISEWEBHOOKTAG` | Yes | Apprise tag value to include with each notification. |
|
| `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. |
|
| `TZ` | No | Container timezone, such as `America/Chicago`. Recommended so notification hours match your local time. |
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
The published image is:
|
The published image is `docker.io/blinkfink182/donetick-notifier`.
|
||||||
|
|
||||||
```text
|
|
||||||
docker.io/blinkfink182/donetick-notifier
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name donetick-notifier \
|
--name donetick-notifier \
|
||||||
|
-p 8080:8080 \
|
||||||
-e DONETICKHOST=host.docker.internal \
|
-e DONETICKHOST=host.docker.internal \
|
||||||
-e DONETICKPORT=8787 \
|
-e DONETICKPORT=8787 \
|
||||||
-e DONETICKAPIKEY=your-donetick-api-key \
|
-e DONETICKAPIKEY=your-donetick-api-key \
|
||||||
-e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \
|
-e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \
|
||||||
-e APPRISEWEBHOOKTAG=all \
|
-e APPRISEWEBHOOKTAG=all \
|
||||||
-e NOTIFICATIONTIMES=8,12,17 \
|
-e JOB_SCHEDULE="0 8,17 * * *" \
|
||||||
-e TZ=America/Chicago \
|
-e TZ=America/Chicago \
|
||||||
docker.io/blinkfink182/donetick-notifier
|
docker.io/blinkfink182/donetick-notifier
|
||||||
```
|
```
|
||||||
@@ -65,13 +70,15 @@ services:
|
|||||||
donetick-notifier:
|
donetick-notifier:
|
||||||
container_name: donetick-notifier
|
container_name: donetick-notifier
|
||||||
image: docker.io/blinkfink182/donetick-notifier
|
image: docker.io/blinkfink182/donetick-notifier
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
environment:
|
environment:
|
||||||
- DONETICKHOST=host.docker.internal
|
- DONETICKHOST=host.docker.internal
|
||||||
- DONETICKPORT=8787
|
- DONETICKPORT=8787
|
||||||
- DONETICKAPIKEY=your-donetick-api-key
|
- DONETICKAPIKEY=your-donetick-api-key
|
||||||
- APPRISEWEBHOOKURL=https://apprise.example.com/notify/config
|
- APPRISEWEBHOOKURL=https://apprise.example.com/notify/config
|
||||||
- APPRISEWEBHOOKTAG=all
|
- APPRISEWEBHOOKTAG=all
|
||||||
- NOTIFICATIONTIMES=8,12,17
|
- JOB_SCHEDULE=0 8,17 * * *
|
||||||
- TZ=America/Chicago
|
- TZ=America/Chicago
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -81,19 +88,21 @@ Run it with:
|
|||||||
docker compose up -d
|
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
|
```yaml
|
||||||
services:
|
services:
|
||||||
donetick-notifier:
|
donetick-notifier:
|
||||||
image: docker.io/blinkfink182/donetick-notifier
|
image: docker.io/blinkfink182/donetick-notifier
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
environment:
|
environment:
|
||||||
- DONETICKHOST=donetick
|
- DONETICKHOST=donetick
|
||||||
- DONETICKPORT=8787
|
- DONETICKPORT=8787
|
||||||
- DONETICKAPIKEY=your-donetick-api-key
|
- DONETICKAPIKEY=your-donetick-api-key
|
||||||
- APPRISEWEBHOOKURL=http://apprise:8000/notify/config
|
- APPRISEWEBHOOKURL=http://apprise:8000/notify/config
|
||||||
- APPRISEWEBHOOKTAG=all
|
- APPRISEWEBHOOKTAG=all
|
||||||
- NOTIFICATIONTIMES=8,12,17
|
- JOB_SCHEDULE=0 8,17 * * *
|
||||||
- TZ=America/Chicago
|
- TZ=America/Chicago
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -106,12 +115,13 @@ docker build -t donetick-notifier .
|
|||||||
```sh
|
```sh
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name donetick-notifier \
|
--name donetick-notifier \
|
||||||
|
-p 8080:8080 \
|
||||||
-e DONETICKHOST=host.docker.internal \
|
-e DONETICKHOST=host.docker.internal \
|
||||||
-e DONETICKPORT=8787 \
|
-e DONETICKPORT=8787 \
|
||||||
-e DONETICKAPIKEY=your-donetick-api-key \
|
-e DONETICKAPIKEY=your-donetick-api-key \
|
||||||
-e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \
|
-e APPRISEWEBHOOKURL=https://apprise.example.com/notify/config \
|
||||||
-e APPRISEWEBHOOKTAG=all \
|
-e APPRISEWEBHOOKTAG=all \
|
||||||
-e NOTIFICATIONTIMES=8,12,17 \
|
-e JOB_SCHEDULE="0 8,17 * * *" \
|
||||||
-e TZ=America/Chicago \
|
-e TZ=America/Chicago \
|
||||||
donetick-notifier
|
donetick-notifier
|
||||||
```
|
```
|
||||||
@@ -120,34 +130,41 @@ docker run -d \
|
|||||||
|
|
||||||
PowerShell 7 or newer is recommended.
|
PowerShell 7 or newer is recommended.
|
||||||
|
|
||||||
|
Scheduled notifier:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:DONETICKHOST = "donetick.example.com"
|
$env:DONETICKHOST = "donetick.example.com"
|
||||||
$env:DONETICKPORT = "8787"
|
$env:DONETICKPORT = "8787"
|
||||||
$env:DONETICKAPIKEY = "your-donetick-api-key"
|
$env:DONETICKAPIKEY = "your-donetick-api-key"
|
||||||
$env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config"
|
$env:APPRISEWEBHOOKURL = "https://apprise.example.com/notify/config"
|
||||||
$env:APPRISEWEBHOOKTAG = "all"
|
$env:APPRISEWEBHOOKTAG = "all"
|
||||||
$env:NOTIFICATIONTIMES = "8,12,17"
|
|
||||||
|
|
||||||
pwsh ./Start-DoneTickNotifier.ps1
|
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
|
## CI/CD
|
||||||
|
|
||||||
This repository includes Gitea workflows for:
|
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.
|
- 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.
|
- 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 |
|
The `Donetick/` directory contains request examples and collection metadata used to document and exercise the Donetick API while developing the notifier.
|
||||||
| --- | --- |
|
|
||||||
| `main` | `latest` |
|
|
||||||
| refs beginning with `v` | matching ref name, such as `v1.0.0` |
|
|
||||||
| all other refs | `test` |
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,41 +1,8 @@
|
|||||||
[string] $dtHost = $ENV:DONETICKHOST
|
[string] $dtHost = $ENV:DONETICKHOST
|
||||||
[string] $dtPort = $ENV:DONETICKPORT
|
[string] $dtPort = $ENV:DONETICKPORT
|
||||||
[string] $dtAPIKey = $ENV:DONETICKAPIKEY
|
[string] $dtAPIKey = $ENV:DONETICKAPIKEY
|
||||||
[string] $appriseWebhookURL = $ENV:APPRISEWEBHOOKURL
|
|
||||||
[string] $appriseWebhookTag = $ENV:APPRISEWEBHOOKTAG
|
|
||||||
[int[]] $notificationTimes = $ENV:NOTIFICATIONTIMES -split ","
|
|
||||||
|
|
||||||
|
Import-Module ./AppriseNotification.psm1
|
||||||
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
|
function Get-Chores
|
||||||
{
|
{
|
||||||
@@ -51,11 +18,6 @@ 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")
|
$today = (Get-Date "23:59:59")
|
||||||
$chores = Get-Chores
|
$chores = Get-Chores
|
||||||
|
|
||||||
@@ -76,7 +38,6 @@ while ($true) {
|
|||||||
{
|
{
|
||||||
write-host "$($chore.name) $dueDate is due today!"
|
write-host "$($chore.name) $dueDate is due today!"
|
||||||
$todaysTasks += $chore
|
$todaysTasks += $chore
|
||||||
# Send-Notification -title "TASK DUE TODAY" -content "$($chore.Name) is due today!"
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -92,9 +53,9 @@ while ($true) {
|
|||||||
$content = "The following tasks are overdue!`n"
|
$content = "The following tasks are overdue!`n"
|
||||||
foreach($overdueTask in $overdueTasks)
|
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)
|
if ($todaysTasks.Count -ne 0)
|
||||||
@@ -103,35 +64,7 @@ while ($true) {
|
|||||||
$content = "The following tasks are due today!`n"
|
$content = "The following tasks are due today!`n"
|
||||||
foreach($task in $todaysTasks)
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Finding next notification time..."
|
|
||||||
Write-Host "Current time: $(Get-Date)"
|
|
||||||
$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 * 60 # hours * mins * seconds
|
|
||||||
Write-Host "Sleeping for $sleepTime seconds"
|
|
||||||
Start-Sleep -Seconds $sleepTime
|
|
||||||
continue # leave loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($nextNotificationTomorrow)
|
|
||||||
{
|
|
||||||
$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
|
|
||||||
}
|
}
|
||||||
|
Send-AppriseNotification -title "TODAY'S TASKS" -content $content
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Default schedule (overridden at runtime by JOB_SCHEDULE env var)
|
||||||
|
0 * * * * pwsh /data/Start-DoneTickNotifier.ps1
|
||||||
+3
-1
@@ -3,6 +3,8 @@ services:
|
|||||||
donetick-notifier:
|
donetick-notifier:
|
||||||
container_name: donetick-notifier
|
container_name: donetick-notifier
|
||||||
image: docker.io/blinkfink182/donetick-notifier
|
image: docker.io/blinkfink182/donetick-notifier
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
environment:
|
environment:
|
||||||
# BELOW ARE ALL REQUIRED
|
# BELOW ARE ALL REQUIRED
|
||||||
- DONETICKHOST=host.docker.internal # donetick host
|
- DONETICKHOST=host.docker.internal # donetick host
|
||||||
@@ -10,5 +12,5 @@ services:
|
|||||||
- DONETICKAPIKEY=adminpass # donetick API key
|
- DONETICKAPIKEY=adminpass # donetick API key
|
||||||
- APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url
|
- APPRISEWEBHOOKURL=https://apprisehost/notify/config # apprise notification url
|
||||||
- APPRISEWEBHOOKTAG=all # apprise notification tag
|
- 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
|
- 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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user