add examples and test VM

This commit is contained in:
2026-02-20 18:14:49 +01:00
parent df73fc25e6
commit 8868eb8736
12 changed files with 325 additions and 416 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.qcow2

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright © 2026 Kierán Meinhardt
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

231
README.md
View File

@@ -1,9 +1,8 @@
# Panoptikon Watch the world from NixOS # Panoptikon Watch the world from NixOS
A NixOS module for monitoring website content and command output changes.
![](./panoptikon.jpg) ![](./panoptikon.jpg)
A NixOS module for monitoring website content and command output changes.
## Overview ## Overview
Panoptikon is a generic command output and website watcher that periodically runs scripts and reports changes. It's designed to be flexible and can monitor anything from API endpoints to system metrics. Panoptikon is a generic command output and website watcher that periodically runs scripts and reports changes. It's designed to be flexible and can monitor anything from API endpoints to system metrics.
@@ -25,11 +24,6 @@ Add Panoptikon to your NixOS configuration:
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
imports = [
# Add the Panoptikon module
(pkgs.callPackage (builtins.fetchTarball https://github.com/kfm/panoptikon-library/archive/main.tar.gz) { }).nixosModules.default
];
# Enable Panoptikon service # Enable Panoptikon service
services.panoptikon.enable = true; services.panoptikon.enable = true;
@@ -51,21 +45,21 @@ Add Panoptikon to your NixOS configuration:
services.panoptikon.watchers = { services.panoptikon.watchers = {
# Monitor GitHub metadata # Monitor GitHub metadata
github-meta = { github-meta = {
script = pkgs.writers.writeDash "github-meta" ''' script = pkgs.writers.writeDash "github-meta" ''
${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq ${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
'''; '';
frequency = "*:0/5"; # Every 5 minutes frequency = "*:0/5"; # Every 5 minutes
reporters = [ reporters = [
# Report changes to Telegram # Report changes to Telegram
(pkgs.writers.writeDash "telegram-reporter" ''' (pkgs.writers.writeDash "telegram-reporter" ''
${pkgs.curl}/bin/curl -X POST https://api.telegram.org/bot''${TOKEN}/sendMessage \ ${pkgs.curl}/bin/curl -X POST https://api.telegram.org/bot''${TOKEN}/sendMessage \
-d chat_id=123456 \ -d chat_id=123456 \
-d text="$(cat)" -d text="$(cat)"
''') '')
# Also show desktop notifications # Also show desktop notifications
(pkgs.writers.writeDash "notify" ''' (pkgs.writers.writeDash "notify" ''
${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed." ${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed."
''') '')
]; ];
}; };
@@ -85,92 +79,24 @@ Add Panoptikon to your NixOS configuration:
# Monitor a local command # Monitor a local command
disk-space = { disk-space = {
script = pkgs.writers.writeDash "disk-space" ''' script = pkgs.writers.writeDash "disk-space" ''
df -h / | tail -1 | awk '{print $5 " used"}' df -h / | tail -1 | awk '{print $5 " used
'''; }'';
frequency = "*:0/30"; # Every 30 minutes frequency = "*:0/30"; # Every 30 minutes
reporters = [ reporters = [
# Log to systemd journal # Log to systemd journal
(pkgs.writers.writeDash "journal-log" ''' (pkgs.writers.writeDash "journal-log" ''
journalctl -t panoptikon-disk-space --since "1 hour ago" | tail -5 journalctl -t panoptikon-disk-space --since "1 hour ago" | tail -5
''') '')
]; ];
}; };
}; };
} }
``` ```
### Advanced Configuration
#### Using Secrets
```nix
{
services.panoptikon.watchers = {
private-api = {
script = pkgs.writers.writeDash "private-api" '''
${pkgs.curl}/bin/curl -sSL \
-H "Authorization: Bearer $API_TOKEN" \
https://api.example.com/data
''';
frequency = "hourly";
loadCredential = [ "API_TOKEN" ];
reporters = [
(pkgs.writers.writeDash "secure-reporter" '''
${pkgs.curl}/bin/curl -X POST https://secure.example.com/report \
-d "$(cat)"
''')
];
};
};
}
```
#### JSON API Monitoring
```nix
{
services.panoptikon.watchers = {
crypto-prices = {
script = pkgs.panoptikon.urlJSON {
jqScript = ".[0] | { name: .name, price: .quote.USD.price }";
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
frequency = "*:0/15"; # Every 15 minutes
reporters = [
(pkgs.writers.writeDash "price-alert" '''
price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
if (( $(echo "$price > 60000" | bc -l) )); then
${pkgs.libnotify}/bin/notify-send "Bitcoin price: $$price"
fi
''')
];
};
};
}
```
## Available Helper Scripts
Panoptikon provides several helper scripts in the overlay:
### URL Monitoring
- `pkgs.panoptikon.url`: Fetch and convert HTML to text
- `pkgs.panoptikon.urlSelector`: Extract specific HTML elements using CSS selectors
- `pkgs.panoptikon.urlJSON`: Fetch JSON and apply jq transformations
### Reporters
- `pkgs.panoptikon.kpaste-irc`: Report changes to IRC via kpaste
- `target`: IRC target (channel or user)
- `server`: IRC server (default: irc.r)
- `messagePrefix`: Prefix for messages (default: "change detected: ")
- `nick`: Nick to use (default: watcher name + "-watcher")
- `retiolumLink`: Use retiolum link (default: false)
## Service Management ## Service Management
### Systemd Integration ### systemd Integration
Each watcher gets its own systemd service and timer: Each watcher gets its own systemd service and timer:
@@ -190,11 +116,10 @@ systemctl start panoptikon-github-meta
### Timer Configuration ### Timer Configuration
Timers use systemd.timer syntax. Common examples: Timers use systemd timer syntax. Common examples:
- `*:0/5` - Every 5 minutes - `*:0/5` - Every 5 minutes
- `daily` - Once per day - `daily` - Once per day
- `Mon..Fri 9:00-17:00` - Weekdays during business hours
- `*:0/15` - Every 15 minutes - `*:0/15` - Every 15 minutes
- `weekly` - Once per week - `weekly` - Once per week
@@ -204,135 +129,13 @@ See [systemd.time(7)](https://www.freedesktop.org/software/systemd/man/systemd.t
- Watchers run as the `panoptikon` system user - Watchers run as the `panoptikon` system user
- Scripts are executed in `/var/lib/panoptikon` - Scripts are executed in `/var/lib/panoptikon`
- Use `loadCredential` to securely pass secrets - Use `LoadCredential=` to securely pass secrets
- Scripts should be written defensively (use `set -euo pipefail`) - Scripts should be written defensively (use `set -euo pipefail`)
## Troubleshooting ## Troubleshooting
### Common Issues
1. **No output**: Check if the script runs correctly manually
2. **Permission denied**: Ensure the panoptikon user can access required resources
3. **Network issues**: Add `network-online.target` as a dependency
4. **Rate limiting**: Add randomized delays using `RandomizedDelaySec`
### Debug Mode
Enable debug logging:
```nix
services.panoptikon.watchers.github-meta = {
# ...
script = ''
set -x # Enable debug output
${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
'';
};
```
## Examples ## Examples
### Monitor Cryptocurrency Prices See the [examples directory](./examples/) for complete configurations.
```nix Run `nix run .#panoptikon-vm` to start a VM with Panoptikon and example watchers pre-configured.
{
services.panoptikon.enable = true;
services.panoptikon.watchers = {
bitcoin-price = {
script = pkgs.panoptikon.urlJSON {
jqScript = ".[0] | { name: .name, price: .quote.USD.price, change: .quote.USD.percent_change_24h }";
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
frequency = "*:0/30";
reporters = [
(pkgs.writers.writeDash "btc-alert" '''
price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
change=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.change')
${pkgs.libnotify}/bin/notify-send \
"Bitcoin: $$price ($$change%)"
''')
];
};
};
}
```
### Monitor System Metrics
```nix
{
services.panoptikon.enable = true;
services.panoptikon.watchers = {
load-average = {
script = pkgs.writers.writeDash "load-average" '''
uptime | awk -F'load average:' '{print $2}' | awk '{print $1 " (1m), " $2 " (5m), " $3 " (15m)"}'
''';
frequency = "*:0/2"; # Every 2 minutes
reporters = [
(pkgs.writers.writeDash "load-alert" '''
load=$(echo "$(cat)" | awk '{print $1}' | tr -d ',')
if (( $(echo "$load > 2.0" | bc -l) )); then
${pkgs.libnotify}/bin/notify-send "High load: $$load"
fi
''')
];
};
};
}
```
### Monitor NixOS Updates
```nix
{
services.panoptikon.enable = true;
services.panoptikon.watchers = {
nixos-updates = {
script = pkgs.panoptikon.url "https://nixos.org/blog/";
frequency = "daily";
reporters = [
(pkgs.writers.writeDash "update-notify" '''
${pkgs.libnotify}/bin/notify-send "NixOS blog updated: $(cat | head -5)"
''')
];
};
};
}
```
## Development
### Adding Custom Reporters
Create a new reporter in the overlay:
```nix
final: prev: {
panoptikon = prev.panoptikon // {
my-custom-reporter = {
endpoint,
authToken ? "",
...
}:
prev.writers.writeDash "my-reporter" ''
${prev.curl}/bin/curl -X POST ''${endpoint} \
-H "Authorization: Bearer ''${authToken} \
-d "$(cat)"
'';
};
};
```
### Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests if applicable
4. Submit a pull request
## License
MIT License

View File

@@ -1,119 +0,0 @@
# Advanced Panoptikon configuration with secrets and custom reporters
{
# Load secrets from agenix
secrets = import ../../secrets { };
services.panoptikon.enable = true;
services.panoptikon.watchers = {
# Monitor a private API with authentication
private-api = {
script = pkgs.writers.writeDash "private-api" '''
set -euo pipefail
${pkgs.curl}/bin/curl -sSL \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
https://api.example.com/data
''';
frequency = "hourly";
loadCredential = [ "API_TOKEN" ];
reporters = [
# Custom reporter that sends to a webhook
(pkgs.writers.writeDash "webhook-reporter" '''
${pkgs.curl}/bin/curl -X POST \
-H "Content-Type: application/json" \
-d "{\"watcher\": \"$PANOPTIKON_WATCHER\", \"changes\": $(cat)}" \
https://hooks.example.com/panoptikon
''')
# Also log to systemd journal
(pkgs.writers.writeDash "journal-log" '''
journalctl -t panoptikon-private-api --since "1 hour ago" | tail -5
''')
];
};
# Monitor cryptocurrency prices with alerts
crypto-monitor = {
script = pkgs.panoptikon.urlJSON {
jqScript = ".[0] | {
name: .name,
price: .quote.USD.price,
change24h: .quote.USD.percent_change_24h,
marketCap: .quote.USD.market_cap
}";
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
frequency = "*:0/15";
reporters = [
(pkgs.writers.writeDash "btc-alert" '''
price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
change=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.change24h')
# Alert if price > $60,000 or change > 5%
if (( $(echo "$price > 60000" | bc -l) )) || (( $(echo "$change > 5" | bc -l) )); then
${pkgs.libnotify}/bin/notify-send \
"BTC Alert: $$price ($$change% change)"
fi
''')
# Log to file
(pkgs.writers.writeDash "price-logger" '''
echo "$(date): $(cat)" >> /var/log/panoptikon/btc-prices.log
''')
];
};
# Monitor system load with thresholds
system-health = {
script = pkgs.writers.writeDash "system-health" '''
set -euo pipefail
load=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | tr -d ',')
mem=$(free -m | awk 'NR==2{printf "%.1f%%", $3*100/$2 }')
disk=$(df / | awk 'NR==2{printf "%.1f%%", $5}')
echo "load: $$load, mem: $$mem, disk: $$disk"
''';
frequency = "*:0/5";
reporters = [
(pkgs.writers.writeDash "health-alert" '''
load=$(echo "$(cat)" | awk -F',' '{print $1}' | awk '{print $2}')
mem=$(echo "$(cat)" | awk -F',' '{print $2}' | awk '{print $2}')
disk=$(echo "$(cat)" | awk -F',' '{print $3}' | awk '{print $2}')
# Alert if load > 2.0, mem > 80%, or disk > 90%
if (( $(echo "$load > 2.0" | bc -l) )) || (( $(echo "${mem%%%} > 80" | bc -l) )) || (( $(echo "${disk%%%} > 90" | bc -l) )); then
${pkgs.libnotify}/bin/notify-send \
"System Alert: Load=$$load, Mem=$$mem, Disk=$$disk"
fi
''')
];
};
};
# Add monitoring user
users.extraUsers.panoptikon = {
isSystemUser = true;
createHome = true;
home = "/var/lib/panoptikon";
group = "panoptikon";
description = "Panoptikon monitoring service";
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK..." # Monitoring access key
];
};
# Configure log rotation
services.logrotate = {
enable = true;
config = {
rotate = 14;
compress = true;
delaycompress = true;
missingok = true;
notifempty = true;
create = "644 panoptikon panoptikon";
};
files = [
"/var/log/panoptikon/*.log"
];
};
}

18
examples/bitcoin.nix Normal file
View File

@@ -0,0 +1,18 @@
{ pkgs, ... }:
{
services.panoptikon.enable = true;
services.panoptikon.watchers = {
bitcoin-price = {
script = pkgs.panoptikonWatchers.json {
jqScript = ".[]|{name: .name, price: .current_price, change: .price_change_24h}";
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
frequency = "*:0/30";
reporters = [
(pkgs.panoptikonReporters.telegram {
chatId = "123";
tokenPath = pkgs.writeText "my-telegram-token.txt" "123:abc";
})
];
};
};
}

15
examples/nixos.nix Normal file
View File

@@ -0,0 +1,15 @@
{ pkgs, ... }:
{
services.panoptikon.enable = true;
services.panoptikon.watchers = {
nixos-updates = {
script = pkgs.panoptikonWatchers.html "https://nixos.org/blog/";
frequency = "daily";
reporters = [
(pkgs.panoptikonReporters.mail { recipient = "admin@example.org"; })
];
};
};
}

View File

@@ -1,30 +1,45 @@
# Simple Panoptikon configuration { pkgs, ... }:
{ {
services.panoptikon.enable = true; services.panoptikon.enable = true;
services.panoptikon.watchers = { services.panoptikon.watchers = {
# Monitor GitHub metadata every 5 minutes
github-meta = { github-meta = {
script = pkgs.writers.writeDash "github-meta" ''' script = pkgs.panoptikonWatchers.json { } "https://api.github.com/meta";
${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
''';
frequency = "*:0/5"; frequency = "*:0/5";
reporters = [ reporters = [
(pkgs.writers.writeDash "notify" ''' (pkgs.panoptikonReporters.wall { })
${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed." (pkgs.panoptikonReporters.irc {
''') target = "kmein";
})
];
};
cock-canary = {
script = pkgs.panoptikonWatchers.plain "https://cock.li/canary.asc.txt";
frequency = "daily";
reporters = [
(pkgs.panoptikonReporters.irc {
target = "kmein";
})
];
};
"4d2-canary" = {
script = pkgs.panoptikonWatchers.plain "https://4d2.org/canary.txt";
frequency = "daily";
reporters = [
(pkgs.panoptikonReporters.irc {
target = "kmein";
})
]; ];
}; };
# Monitor a website for news # Monitor a website for news
nixos-news = { nixos-news = {
script = pkgs.panoptikon.urlSelector "#news h2" "https://nixos.org/blog/"; script = pkgs.panoptikonWatchers.htmlSelector "article h2" "https://nixos.org/blog/";
frequency = "daily"; frequency = "daily";
reporters = [ reporters = [
(pkgs.writers.writeDash "news-alert" ''' (pkgs.panoptikonReporters.wall { })
${pkgs.libnotify}/bin/notify-send "New NixOS blog post: $(cat | head -1)"
''')
]; ];
}; };
}; };

16
examples/system.nix Normal file
View File

@@ -0,0 +1,16 @@
{ pkgs, ... }:
{
services.panoptikon.enable = true;
services.panoptikon.watchers = {
boot-space = {
script = pkgs.writers.writeDash "load-average" ''
${pkgs.coreutils}/bin/df -h /boot
'';
frequency = "*:0/2";
reporters = [
(pkgs.panoptikonReporters.wall { })
];
};
};
}

View File

@@ -1,13 +1,50 @@
{ {
description = "Panoptikon - Website and command output monitoring"; description = "Panoptikon Watch the world from NixOS";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
}; };
outputs = inputs: { outputs =
nixosModules.default = ./module.nix; inputs:
overlays.default = ./overlay.nix; let
eachSupportedSystem = inputs.nixpkgs.lib.genAttrs inputs.nixpkgs.lib.systems.flakeExposed;
inherit (inputs.nixpkgs) lib;
in
{
nixosModules.default = import nix/module.nix;
overlays.default = import nix/overlay.nix;
apps = eachSupportedSystem (
system:
let
nixosSystem = lib.nixosSystem {
system = "x86_64-linux";
modules = [
inputs.self.nixosModules.default
{
nixpkgs.overlays = [ inputs.self.overlay ];
}
{
virtualisation.vmVariant = {
virtualisation.graphics = false;
};
services.getty.autologinUser = "root";
system.stateVersion = lib.trivial.release;
}
./examples/simple.nix
./examples/bitcoin.nix
./examples/nixos.nix
./examples/system.nix
];
};
in
{
panoptikon-vm = {
type = "app";
program = lib.getExe nixosSystem.config.system.build.vm;
};
}
);
}; };
} }

View File

@@ -65,12 +65,13 @@
config = config =
let let
cfg = config.services.panoptikon; cfg = config.services.panoptikon;
stateDir = "/var/lib/panoptikon";
in in
lib.mkIf cfg.enable { lib.mkIf cfg.enable {
users.extraUsers.panoptikon = { users.extraUsers.panoptikon = {
isSystemUser = true; isSystemUser = true;
createHome = true; createHome = true;
home = "/var/lib/panoptikon"; home = stateDir;
group = "panoptikon"; group = "panoptikon";
}; };
@@ -85,37 +86,62 @@
systemd.services = lib.attrsets.mapAttrs' ( systemd.services = lib.attrsets.mapAttrs' (
watcherName: watcherOptions: watcherName: watcherOptions:
let
# Absolute paths for the state files
currentFile = "${stateDir}/${watcherName}";
oldFile = "${stateDir}/${watcherName}.old";
in
lib.nameValuePair "panoptikon-${watcherName}" { lib.nameValuePair "panoptikon-${watcherName}" {
enable = true; enable = true;
startAt = watcherOptions.frequency; startAt = watcherOptions.frequency;
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
User = "panoptikon"; User = "panoptikon";
Group = "panoptikon"; Group = "panoptikon";
WorkingDirectory = "/var/lib/panoptikon";
RestartSec = toString (60 * 60); # ISOLATION: Start in a fresh, empty directory for every run
RuntimeDirectory = "panoptikon/${watcherName}";
WorkingDirectory = "/run/panoptikon/${watcherName}";
# PERSISTENCE: Only allow write access to the specific state directory
ReadWritePaths = [ stateDir ];
# HARDENING: Prevent the process from seeing most of the system
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
Restart = "on-failure"; Restart = "on-failure";
LoadCredential = watcherOptions.loadCredential; LoadCredential = watcherOptions.loadCredential;
}; };
unitConfig = {
StartLimitIntervalSec = "300";
StartLimitBurst = "5";
};
environment.PANOPTIKON_WATCHER = watcherName; environment.PANOPTIKON_WATCHER = watcherName;
wants = [ "network-online.target" ];
script = '' script = ''
set -fux set -efu # Removed -x to keep tokens out of logs if they leak via env
${watcherOptions.script} > ${lib.escapeShellArg watcherName}
diff_output=$(${pkgs.diffutils}/bin/diff --new-file ${ # 1. Run watcher and save to the persistent state dir
lib.escapeShellArg (watcherName + ".old") ${watcherOptions.script} > "${currentFile}"
} ${lib.escapeShellArg watcherName} || :)
if [ -n "$diff_output" ] # 2. Ensure .old exists so diff doesn't crash on first run
then [ -f "${oldFile}" ] || touch "${oldFile}"
# 3. Compare
diff_output=$(${pkgs.diffutils}/bin/diff --new-file "${oldFile}" "${currentFile}" || :)
if [ -n "$diff_output" ]; then
# 4. Run reporters.
# They are running inside /run/panoptikon/${watcherName} (empty).
# They can't see or delete other .old files via relative paths.
${lib.strings.concatMapStringsSep "\n" ( ${lib.strings.concatMapStringsSep "\n" (
reporter: ''echo "$diff_output" | ${reporter} || :'' reporter: ''echo "$diff_output" | ${reporter} || :''
) watcherOptions.reporters} ) watcherOptions.reporters}
fi fi
mv ${lib.escapeShellArg watcherName} ${lib.escapeShellArg (watcherName + ".old")}
# 5. Rotate state
mv "${currentFile}" "${oldFile}"
''; '';
} }
) cfg.watchers; ) cfg.watchers;

125
nix/overlay.nix Normal file
View File

@@ -0,0 +1,125 @@
final: prev:
let
lib = prev.lib;
in
{
panoptikonReporters = (prev.panoptikonWatchers or { }) // {
kpaste-irc =
{
target,
retiolumLink ? false,
server ? "irc.r",
messagePrefix ? "change detected: ",
nick ? ''"$PANOPTIKON_WATCHER"-watcher'',
}:
prev.writers.writeDash "kpaste-irc-reporter" ''
KPASTE_CONTENT_TYPE=text/plain ${prev.kpaste}/bin/kpaste \
| ${prev.gnused}/bin/sed -n "${if retiolumLink then "2" else "3"}s/^/${messagePrefix}/p" \
| ${prev.nur.repos.mic92.ircsink}/bin/ircsink \
--nick ${nick} \
--server ${server} \
--target ${target}
'';
irc =
{
target, # e.g., "#mychannel"
server ? "irc.libera.chat",
port ? "6667",
nick ? "panoptikon-bot",
messagePrefix ? "change detected: ",
}:
prev.writers.writeDash "irc-reporter" ''
MESSAGE=$(cat | tr -d '\n\r')
# Use netcat to send raw IRC commands
(
echo "NICK ${nick}"
echo "USER ${nick} 8 * :Panoptikon Watcher"
sleep 2 # Give the server a moment to process login
echo "JOIN ${target}"
echo "PRIVMSG ${target} :${messagePrefix} $MESSAGE"
echo "QUIT"
) | ${prev.netcat}/bin/nc -w 10 ${server} ${port}
'';
telegram =
{
tokenPath, # Bot API Token
chatId,
messagePrefix ? "Change detected in $PANOPTIKON_WATCHER: ",
}:
prev.writers.writeDash "telegram-reporter" ''
if [ ! -f "${tokenPath}" ]; then
echo "Error: Token file ${tokenPath} not found" >&2
exit 1
fi
TOKEN=$(cat "${tokenPath}")
MESSAGE=$(cat)
${prev.curl}/bin/curl -s -X POST "https://api.telegram.org/bot''${TOKEN}/sendMessage" \
-d chat_id="${chatId}" \
-d text="${messagePrefix} $MESSAGE"
'';
matrix =
{
homeserver, # e.g., "https://matrix.org"
roomId, # e.g., "!roomid:matrix.org"
tokenPath,
messagePrefix ? "Change detected in $PANOPTIKON_WATCHER: ",
}:
prev.writers.writeDash "matrix-reporter" ''
if [ ! -f "${tokenPath}" ]; then
echo "Error: Token file ${tokenPath} not found" >&2
exit 1
fi
TOKEN=$(cat "${tokenPath}")
MESSAGE=$(cat)
TXN_ID=$(date +%s%N)
${prev.curl}/bin/curl -s -X PUT "${homeserver}/_matrix/client/r0/rooms/${lib.escapeShellArg roomId}/send/m.room.message/''${TXN_ID}?access_token=''${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"msgtype\": \"m.text\", \"body\": \"${messagePrefix} $MESSAGE\"}"
'';
wall =
{
messagePrefix ? "PANOPTIKON ALERT ($PANOPTIKON_WATCHER):",
}:
prev.writers.writeDash "wall-reporter" ''
MESSAGE=$(cat)
echo -e "${messagePrefix}\n$MESSAGE" | ${prev.util-linux}/bin/wall
'';
mail =
{
recipient, # e.g. "admin@example.com"
subjectPrefix ? "[Panoptikon] Change detected:",
}:
prev.writers.writeDash "mail-reporter" ''
MESSAGE=$(cat)
echo "$MESSAGE" | ${prev.mailutils}/bin/mail -s "${subjectPrefix} $PANOPTIKON_WATCHER" "${recipient}"
'';
};
panoptikonWatchers = (prev.panoptikonWatchers or { }) // {
plain =
address:
prev.writers.writeDash "watch-url" ''
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address}
'';
html =
address:
prev.writers.writeDash "watch-html" ''
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
| ${prev.python3Packages.html2text}/bin/html2text --decode-errors=ignore
'';
htmlSelector =
selector: address:
prev.writers.writeDash "watch-html-selector" ''
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
| ${prev.htmlq}/bin/htmlq ${lib.escapeShellArg selector} \
| ${prev.python3Packages.html2text}/bin/html2text
'';
json =
{
jqScript ? ".",
}:
address:
prev.writers.writeDash "watch-json" ''
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} | ${prev.jq}/bin/jq -f ${prev.writeText "script.jq" jqScript}
'';
};
}

View File

@@ -1,47 +0,0 @@
final: prev: {
panoptikon =
let
lib = prev.lib;
in
lib.recursiveUpdate prev.panoptikon {
url =
address:
prev.writers.writeDash "watch-url" ''
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
| ${prev.python3Packages.html2text}/bin/html2text --decode-errors=ignore
'';
urlSelector =
selector: address:
prev.writers.writeDash "watch-url-selector" ''
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
| ${prev.htmlq}/bin/htmlq ${lib.escapeShellArg selector} \
| ${prev.python3Packages.html2text}/bin/html2text
'';
urlJSON =
{
jqScript ? ".",
}:
address:
prev.writers.writeDash "watch-url-json" ''
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} | ${prev.jq}/bin/jq -f ${prev.writeText "script.jq" jqScript}
'';
# reporter scripts
kpaste-irc =
{
target,
retiolumLink ? false,
server ? "irc.r",
messagePrefix ? "change detected: ",
nick ? ''"$PANOPTIKON_WATCHER"-watcher'',
}:
prev.writers.writeDash "kpaste-irc-reporter" ''
KPASTE_CONTENT_TYPE=text/plain ${prev.kpaste}/bin/kpaste \
| ${prev.gnused}/bin/sed -n "${if retiolumLink then "2" else "3"}s/^/${messagePrefix}/p" \
| ${prev.nur.repos.mic92.ircsink}/bin/ircsink \
--nick ${nick} \
--server ${server} \
--target ${target}
'';
};
}