⛳ alerts for any ctfd instance via ntfy

Compare changes

Choose any two refs to compare.

+5 -5
.direnv/bin/nix-direnv-reload
···
#!/usr/bin/env bash
set -e
-
if [[ ! -d "/home/kierank/Projects/ctfd-alerts" ]]; then
+
if [[ ! -d "/home/kierank/code/personal/ctfd-alerts" ]]; then
echo "Cannot find source directory; Did you move it?"
-
echo "(Looking for "/home/kierank/Projects/ctfd-alerts")"
+
echo "(Looking for "/home/kierank/code/personal/ctfd-alerts")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
-
_nix_direnv_force_reload=1 direnv exec "/home/kierank/Projects/ctfd-alerts" true
+
_nix_direnv_force_reload=1 direnv exec "/home/kierank/code/personal/ctfd-alerts" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
-
touch "/home/kierank/Projects/ctfd-alerts/.envrc"
+
touch "/home/kierank/code/personal/ctfd-alerts/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
-
touch -r "/home/kierank/Projects/ctfd-alerts/.envrc" "/home/kierank/Projects/ctfd-alerts/.direnv"/*.rc
+
touch -r "/home/kierank/code/personal/ctfd-alerts/.envrc" "/home/kierank/code/personal/ctfd-alerts/.direnv"/*.rc
.github/images/out.gif

This is a binary file and will not be displayed.

+75
.github/workflows/release.yml
···
+
name: Release
+
+
on:
+
release:
+
types: [created]
+
+
jobs:
+
build-and-upload:
+
runs-on: ubuntu-latest
+
permissions:
+
contents: write
+
+
steps:
+
- name: Checkout repository
+
uses: actions/checkout@v4
+
+
- name: Install Nix
+
uses: DeterminateSystems/nix-installer-action@main
+
+
- name: Configure Nix cache
+
uses: DeterminateSystems/magic-nix-cache-action@main
+
+
- name: Build for all platforms
+
run: |
+
nix develop -c ctfd-alerts-build
+
+
- name: Upload Linux AMD64 binary
+
uses: actions/upload-release-asset@v1
+
env:
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
with:
+
upload_url: ${{ github.event.release.upload_url }}
+
asset_path: ./bin/ctfd-alerts-linux-amd64
+
asset_name: ctfd-alerts-linux-amd64
+
asset_content_type: application/octet-stream
+
+
- name: Upload Linux ARM64 binary
+
uses: actions/upload-release-asset@v1
+
env:
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
with:
+
upload_url: ${{ github.event.release.upload_url }}
+
asset_path: ./bin/ctfd-alerts-linux-arm64
+
asset_name: ctfd-alerts-linux-arm64
+
asset_content_type: application/octet-stream
+
+
- name: Upload macOS AMD64 binary
+
uses: actions/upload-release-asset@v1
+
env:
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
with:
+
upload_url: ${{ github.event.release.upload_url }}
+
asset_path: ./bin/ctfd-alerts-darwin-amd64
+
asset_name: ctfd-alerts-darwin-amd64
+
asset_content_type: application/octet-stream
+
+
- name: Upload macOS ARM64 binary
+
uses: actions/upload-release-asset@v1
+
env:
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
with:
+
upload_url: ${{ github.event.release.upload_url }}
+
asset_path: ./bin/ctfd-alerts-darwin-arm64
+
asset_name: ctfd-alerts-darwin-arm64
+
asset_content_type: application/octet-stream
+
+
- name: Upload Windows AMD64 binary
+
uses: actions/upload-release-asset@v1
+
env:
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
with:
+
upload_url: ${{ github.event.release.upload_url }}
+
asset_path: ./bin/ctfd-alerts-windows-amd64.exe
+
asset_name: ctfd-alerts-windows-amd64.exe
+
asset_content_type: application/octet-stream
+1
.gitignore
···
.envrc
config.toml
cache.json
+
vendor
+25 -2
README.md
···
Sends alerts for any arbitrary [CTFd](https://ctfd.io/) instance via [ntfy](https://ntfy.sh/)
+
![vhs gif of the command being run](https://github.com/taciturnaxolotl/ctfd-alerts/blob/main/.github/images/out.gif?raw=true)
+
## Install
You can download a pre-built binary from the releases or you can use the following options
···
# Go
go install github.com/taciturnaxolotl/ctfd-alerts@latest
```
+
+
If you need a systemd service file there is one in `ctfd-alerts.service`
### Nix
···
```nix
# In your flake.nix
{
-
inputs.akami.url = "github:taciturnaxolotl/ctfd-alerts";
+
inputs.ctfd-alerts.url = "github:taciturnaxolotl/ctfd-alerts";
-
outputs = { self, nixpkgs, akami, ... }: {
+
outputs = { self, nixpkgs, ctfd-alerts, ... }: {
# Access the package as:
# ctfd-alerts.packages.${system}.default
};
}
+
```
+
+
## Config
+
+
The config for the bot is quite simple. Create a `config.toml` file in the same directory as the binary (or link to the config location with `-c ./path/to/config/config.toml`) with the following format:
+
+
```toml
+
debug = true
+
interval = 100 # defaults to 300 if unset
+
user = "echo_kieran"
+
+
[ctfd]
+
api_base = "http://163.11.237.79/api/v1"
+
api_key = "ctfd_10698fd44950bf7556bc3f5e1012832dae5bddcffb1fe82191d8dd3be3641393"
+
+
[ntfy]
+
api_base = "https://ntfy.sh/"
+
acess_token = ""
+
topic = "youralert"
```
Written in go. If you have any suggestions or issues feel free to open an issue on my [tangled](https://tangled.sh/@dunkirk.sh/ctfd-alerts) knot
+15
cassette.tape
···
+
Output .github/images/out.gif
+
Set Shell zsh
+
Set Width 1400
+
Set Height 900
+
Require ctfd-alerts-dev
+
Sleep 1s
+
Type "ctfd-alerts-dev"
+
Enter
+
Sleep 3s
+
Type "ctfd-alerts-dev status"
+
Enter
+
Sleep 4s
+
Type "ctfd-alerts-dev serve"
+
Enter
+
Sleep 5s
+249
cmd/serve/serve.go
···
+
package serve
+
+
import (
+
"encoding/json"
+
"fmt"
+
"log"
+
"os"
+
"os/signal"
+
"path/filepath"
+
"reflect"
+
"syscall"
+
"time"
+
+
"github.com/spf13/cobra"
+
"github.com/taciturnaxolotl/ctfd-alerts/clients"
+
)
+
+
type MonitorState struct {
+
LastScoreboard *clients.ScoreboardResponse `json:"last_scoreboard"`
+
LastChallenges *clients.ChallengeListResponse `json:"last_challenges"`
+
UserPosition int `json:"user_position"`
+
}
+
+
func getCacheFilePath() string {
+
return filepath.Join(".", "cache.json")
+
}
+
+
func loadStateFromCache() *MonitorState {
+
cachePath := getCacheFilePath()
+
data, err := os.ReadFile(cachePath)
+
if err != nil {
+
log.Printf("No cache file found or error reading cache: %v", err)
+
return &MonitorState{}
+
}
+
+
var state MonitorState
+
if err := json.Unmarshal(data, &state); err != nil {
+
log.Printf("Error parsing cache file: %v", err)
+
return &MonitorState{}
+
}
+
+
log.Printf("Loaded state from cache: %s", cachePath)
+
return &state
+
}
+
+
func saveStateToCache(state *MonitorState) error {
+
cachePath := getCacheFilePath()
+
data, err := json.MarshalIndent(state, "", " ")
+
if err != nil {
+
return fmt.Errorf("error marshaling state: %v", err)
+
}
+
+
if err := os.WriteFile(cachePath, data, 0644); err != nil {
+
return fmt.Errorf("error writing cache file: %v", err)
+
}
+
+
return nil
+
}
+
+
var ServeCmd = &cobra.Command{
+
Use: "serve",
+
Short: "Run monitoring server",
+
Long: "Continuously monitors CTFd for leaderboard changes and new challenges, sending alerts when events occur",
+
Run: runServer,
+
}
+
+
func runServer(cmd *cobra.Command, args []string) {
+
ctx := cmd.Context()
+
+
// Get CTFd client from context
+
ctfdClient, ok := ctx.Value("ctfd_client").(clients.CTFdClient)
+
if !ok {
+
log.Fatal("CTFd client not found in context")
+
}
+
+
// Get config from context
+
config := ctx.Value("config")
+
+
// Use reflection to access config fields
+
configValue := reflect.ValueOf(config).Elem()
+
userField := configValue.FieldByName("User").String()
+
intervalField := int(configValue.FieldByName("MonitorInterval").Int())
+
+
ntfyConfigField := configValue.FieldByName("NtfyConfig")
+
ntfyTopic := ntfyConfigField.FieldByName("Topic").String()
+
ntfyApiBase := ntfyConfigField.FieldByName("ApiBase").String()
+
ntfyAccessToken := ntfyConfigField.FieldByName("AccessToken").String()
+
+
// Create ntfy client
+
ntfyClient := clients.NewNtfyClient(ntfyTopic, ntfyApiBase, ntfyAccessToken)
+
+
// Initialize monitoring state - try to load from cache first
+
state := loadStateFromCache()
+
+
// If cache is empty or we want fresh data, get initial state from API
+
if state.LastScoreboard == nil || state.LastChallenges == nil {
+
log.Println("No cached state found, fetching initial state from API...")
+
if err := updateState(ctfdClient, state, userField); err != nil {
+
log.Printf("Error getting initial state: %v", err)
+
}
+
} else {
+
log.Println("Using cached state")
+
// Still update user position in case it changed
+
if state.LastScoreboard != nil {
+
state.UserPosition = findUserPosition(state.LastScoreboard, userField)
+
}
+
}
+
+
log.Printf("Starting monitoring server (interval: %d seconds)", intervalField)
+
log.Printf("Monitoring user: %s", userField)
+
+
// Set up signal handling for graceful shutdown
+
sigChan := make(chan os.Signal, 1)
+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+
// Main monitoring loop
+
ticker := time.NewTicker(time.Duration(intervalField) * time.Second)
+
defer ticker.Stop()
+
+
for {
+
select {
+
case <-ticker.C:
+
if err := monitorAndAlert(ctfdClient, ntfyClient, state, userField); err != nil {
+
log.Printf("Error during monitoring: %v", err)
+
} else {
+
// Save state to cache after successful monitoring
+
if err := saveStateToCache(state); err != nil {
+
log.Printf("Error saving state to cache: %v", err)
+
}
+
}
+
case <-sigChan:
+
log.Println("Received shutdown signal, saving state and stopping server...")
+
if err := saveStateToCache(state); err != nil {
+
log.Printf("Error saving final state to cache: %v", err)
+
} else {
+
log.Printf("State saved to cache: %s", getCacheFilePath())
+
}
+
return
+
}
+
}
+
}
+
+
func updateState(client clients.CTFdClient, state *MonitorState, username string) error {
+
// Get scoreboard
+
scoreboard, err := client.GetScoreboard()
+
if err != nil {
+
return fmt.Errorf("failed to get scoreboard: %v", err)
+
}
+
state.LastScoreboard = scoreboard
+
+
// Find user position
+
state.UserPosition = findUserPosition(scoreboard, username)
+
+
// Get challenges
+
challenges, err := client.GetChallengeList()
+
if err != nil {
+
return fmt.Errorf("failed to get challenges: %v", err)
+
}
+
state.LastChallenges = challenges
+
+
return nil
+
}
+
+
func monitorAndAlert(client clients.CTFdClient, ntfy *clients.NtfyClient, state *MonitorState, username string) error {
+
// Get current scoreboard
+
currentScoreboard, err := client.GetScoreboard()
+
if err != nil {
+
return fmt.Errorf("failed to get scoreboard: %v", err)
+
}
+
+
// Get current challenges
+
currentChallenges, err := client.GetChallengeList()
+
if err != nil {
+
return fmt.Errorf("failed to get challenges: %v", err)
+
}
+
+
// Check for leaderboard bypass
+
if state.LastScoreboard != nil {
+
currentPosition := findUserPosition(currentScoreboard, username)
+
if currentPosition > state.UserPosition && state.UserPosition > 0 {
+
// User was bypassed
+
msg := ntfy.NewMessage(fmt.Sprintf("🏆 You've been bypassed on the leaderboard! New position: #%d (was #%d)", currentPosition, state.UserPosition))
+
msg.Title = "CTFd Leaderboard Alert"
+
msg.Tags = []string{"warning", "leaderboard"}
+
msg.Priority = 4
+
+
if err := ntfy.SendMessage(msg); err != nil {
+
log.Printf("Failed to send bypass alert: %v", err)
+
} else {
+
log.Printf("Sent bypass alert: %s -> %d", username, currentPosition)
+
}
+
}
+
state.UserPosition = currentPosition
+
}
+
+
// Check for new challenges
+
if state.LastChallenges != nil {
+
newChallenges := findNewChallenges(state.LastChallenges, currentChallenges)
+
for _, challenge := range newChallenges {
+
msg := ntfy.NewMessage(fmt.Sprintf("🎯 New challenge released: %s (%s) - %d points", challenge.Name, challenge.Category, challenge.Value))
+
msg.Title = "New CTFd Challenge"
+
msg.Tags = []string{"challenge", "new"}
+
msg.Priority = 3
+
+
if err := ntfy.SendMessage(msg); err != nil {
+
log.Printf("Failed to send new challenge alert: %v", err)
+
} else {
+
log.Printf("Sent new challenge alert: %s", challenge.Name)
+
}
+
}
+
}
+
+
// Update state
+
state.LastScoreboard = currentScoreboard
+
state.LastChallenges = currentChallenges
+
+
return nil
+
}
+
+
func findUserPosition(scoreboard *clients.ScoreboardResponse, username string) int {
+
for _, team := range scoreboard.Data {
+
if team.Name == username {
+
return team.Position
+
}
+
// Also check team members
+
for _, member := range team.Members {
+
if member.Name == username {
+
return team.Position
+
}
+
}
+
}
+
return 0 // User not found
+
}
+
+
func findNewChallenges(oldChallenges, newChallenges *clients.ChallengeListResponse) []clients.Challenge {
+
oldMap := make(map[int]bool)
+
for _, challenge := range oldChallenges.Data {
+
oldMap[challenge.ID] = true
+
}
+
+
var newOnes []clients.Challenge
+
for _, challenge := range newChallenges.Data {
+
if !oldMap[challenge.ID] {
+
newOnes = append(newOnes, challenge)
+
}
+
}
+
+
return newOnes
+
}
-250
cmd/server/server.go
···
-
package server
-
-
import (
-
"encoding/json"
-
"fmt"
-
"log"
-
"os"
-
"os/signal"
-
"path/filepath"
-
"reflect"
-
"syscall"
-
"time"
-
-
"github.com/spf13/cobra"
-
"github.com/taciturnaxolotl/ctfd-alerts/clients"
-
)
-
-
type MonitorState struct {
-
LastScoreboard *clients.ScoreboardResponse `json:"last_scoreboard"`
-
LastChallenges *clients.ChallengeListResponse `json:"last_challenges"`
-
UserPosition int `json:"user_position"`
-
}
-
-
func getCacheFilePath() string {
-
return filepath.Join(".", "cache.json")
-
}
-
-
func loadStateFromCache() *MonitorState {
-
cachePath := getCacheFilePath()
-
data, err := os.ReadFile(cachePath)
-
if err != nil {
-
log.Printf("No cache file found or error reading cache: %v", err)
-
return &MonitorState{}
-
}
-
-
var state MonitorState
-
if err := json.Unmarshal(data, &state); err != nil {
-
log.Printf("Error parsing cache file: %v", err)
-
return &MonitorState{}
-
}
-
-
log.Printf("Loaded state from cache: %s", cachePath)
-
return &state
-
}
-
-
func saveStateToCache(state *MonitorState) error {
-
cachePath := getCacheFilePath()
-
data, err := json.MarshalIndent(state, "", " ")
-
if err != nil {
-
return fmt.Errorf("error marshaling state: %v", err)
-
}
-
-
if err := os.WriteFile(cachePath, data, 0644); err != nil {
-
return fmt.Errorf("error writing cache file: %v", err)
-
}
-
-
return nil
-
}
-
-
// ServerCmd represents the server command
-
var ServerCmd = &cobra.Command{
-
Use: "server",
-
Short: "Run monitoring server",
-
Long: "Continuously monitors CTFd for leaderboard changes and new challenges, sending alerts when events occur",
-
Run: runServer,
-
}
-
-
func runServer(cmd *cobra.Command, args []string) {
-
ctx := cmd.Context()
-
-
// Get CTFd client from context
-
ctfdClient, ok := ctx.Value("ctfd_client").(clients.CTFdClient)
-
if !ok {
-
log.Fatal("CTFd client not found in context")
-
}
-
-
// Get config from context
-
config := ctx.Value("config")
-
-
// Use reflection to access config fields
-
configValue := reflect.ValueOf(config).Elem()
-
userField := configValue.FieldByName("User").String()
-
intervalField := int(configValue.FieldByName("MonitorInterval").Int())
-
-
ntfyConfigField := configValue.FieldByName("NtfyConfig")
-
ntfyTopic := ntfyConfigField.FieldByName("Topic").String()
-
ntfyApiBase := ntfyConfigField.FieldByName("ApiBase").String()
-
ntfyAccessToken := ntfyConfigField.FieldByName("AccessToken").String()
-
-
// Create ntfy client
-
ntfyClient := clients.NewNtfyClient(ntfyTopic, ntfyApiBase, ntfyAccessToken)
-
-
// Initialize monitoring state - try to load from cache first
-
state := loadStateFromCache()
-
-
// If cache is empty or we want fresh data, get initial state from API
-
if state.LastScoreboard == nil || state.LastChallenges == nil {
-
log.Println("No cached state found, fetching initial state from API...")
-
if err := updateState(ctfdClient, state, userField); err != nil {
-
log.Printf("Error getting initial state: %v", err)
-
}
-
} else {
-
log.Println("Using cached state")
-
// Still update user position in case it changed
-
if state.LastScoreboard != nil {
-
state.UserPosition = findUserPosition(state.LastScoreboard, userField)
-
}
-
}
-
-
log.Printf("Starting monitoring server (interval: %d seconds)", intervalField)
-
log.Printf("Monitoring user: %s", userField)
-
-
// Set up signal handling for graceful shutdown
-
sigChan := make(chan os.Signal, 1)
-
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
-
-
// Main monitoring loop
-
ticker := time.NewTicker(time.Duration(intervalField) * time.Second)
-
defer ticker.Stop()
-
-
for {
-
select {
-
case <-ticker.C:
-
if err := monitorAndAlert(ctfdClient, ntfyClient, state, userField); err != nil {
-
log.Printf("Error during monitoring: %v", err)
-
} else {
-
// Save state to cache after successful monitoring
-
if err := saveStateToCache(state); err != nil {
-
log.Printf("Error saving state to cache: %v", err)
-
}
-
}
-
case <-sigChan:
-
log.Println("Received shutdown signal, saving state and stopping server...")
-
if err := saveStateToCache(state); err != nil {
-
log.Printf("Error saving final state to cache: %v", err)
-
} else {
-
log.Printf("State saved to cache: %s", getCacheFilePath())
-
}
-
return
-
}
-
}
-
}
-
-
func updateState(client clients.CTFdClient, state *MonitorState, username string) error {
-
// Get scoreboard
-
scoreboard, err := client.GetScoreboard()
-
if err != nil {
-
return fmt.Errorf("failed to get scoreboard: %v", err)
-
}
-
state.LastScoreboard = scoreboard
-
-
// Find user position
-
state.UserPosition = findUserPosition(scoreboard, username)
-
-
// Get challenges
-
challenges, err := client.GetChallengeList()
-
if err != nil {
-
return fmt.Errorf("failed to get challenges: %v", err)
-
}
-
state.LastChallenges = challenges
-
-
return nil
-
}
-
-
func monitorAndAlert(client clients.CTFdClient, ntfy *clients.NtfyClient, state *MonitorState, username string) error {
-
// Get current scoreboard
-
currentScoreboard, err := client.GetScoreboard()
-
if err != nil {
-
return fmt.Errorf("failed to get scoreboard: %v", err)
-
}
-
-
// Get current challenges
-
currentChallenges, err := client.GetChallengeList()
-
if err != nil {
-
return fmt.Errorf("failed to get challenges: %v", err)
-
}
-
-
// Check for leaderboard bypass
-
if state.LastScoreboard != nil {
-
currentPosition := findUserPosition(currentScoreboard, username)
-
if currentPosition > state.UserPosition && state.UserPosition > 0 {
-
// User was bypassed
-
msg := ntfy.NewMessage(fmt.Sprintf("🏆 You've been bypassed on the leaderboard! New position: #%d (was #%d)", currentPosition, state.UserPosition))
-
msg.Title = "CTFd Leaderboard Alert"
-
msg.Tags = []string{"warning", "leaderboard"}
-
msg.Priority = 4
-
-
if err := ntfy.SendMessage(msg); err != nil {
-
log.Printf("Failed to send bypass alert: %v", err)
-
} else {
-
log.Printf("Sent bypass alert: %s -> %d", username, currentPosition)
-
}
-
}
-
state.UserPosition = currentPosition
-
}
-
-
// Check for new challenges
-
if state.LastChallenges != nil {
-
newChallenges := findNewChallenges(state.LastChallenges, currentChallenges)
-
for _, challenge := range newChallenges {
-
msg := ntfy.NewMessage(fmt.Sprintf("🎯 New challenge released: %s (%s) - %d points", challenge.Name, challenge.Category, challenge.Value))
-
msg.Title = "New CTFd Challenge"
-
msg.Tags = []string{"challenge", "new"}
-
msg.Priority = 3
-
-
if err := ntfy.SendMessage(msg); err != nil {
-
log.Printf("Failed to send new challenge alert: %v", err)
-
} else {
-
log.Printf("Sent new challenge alert: %s", challenge.Name)
-
}
-
}
-
}
-
-
// Update state
-
state.LastScoreboard = currentScoreboard
-
state.LastChallenges = currentChallenges
-
-
return nil
-
}
-
-
func findUserPosition(scoreboard *clients.ScoreboardResponse, username string) int {
-
for _, team := range scoreboard.Data {
-
if team.Name == username {
-
return team.Position
-
}
-
// Also check team members
-
for _, member := range team.Members {
-
if member.Name == username {
-
return team.Position
-
}
-
}
-
}
-
return 0 // User not found
-
}
-
-
func findNewChallenges(oldChallenges, newChallenges *clients.ChallengeListResponse) []clients.Challenge {
-
oldMap := make(map[int]bool)
-
for _, challenge := range oldChallenges.Data {
-
oldMap[challenge.ID] = true
-
}
-
-
var newOnes []clients.Challenge
-
for _, challenge := range newChallenges.Data {
-
if !oldMap[challenge.ID] {
-
newOnes = append(newOnes, challenge)
-
}
-
}
-
-
return newOnes
-
}
-12
config.toml
···
-
debug = true
-
interval = 100
-
user = "echo_kieran"
-
-
[ctfd]
-
api_base = "http://163.11.237.79/api/v1"
-
api_key = "ctfd_10698fd44950bf7556bc3f5e1012832dae5bddcffb1fe82191d8dd3be3641393"
-
-
[ntfy]
-
api_base = "https://ntfy.sh/"
-
acess_token = ""
-
topic = "cugencyber_ctfd"
+16
ctfd-alerts.service
···
+
[Unit]
+
Description=ctfd alerts server
+
After=network.target
+
+
+
[Service]
+
ExecStart=/home/pi/go/bin/ctfd-alerts serve
+
Restart=always
+
User=pi
+
WorkingDirectory=/home/pi/ctfd-alerts
+
StandardOutput=journal
+
StandardError=journal
+
LimitNOFILE=65536
+
+
[Install]
+
WantedBy=multi-user.target
+1 -1
flake.nix
···
packages = forAllSystems ({ pkgs }: {
default = pkgs.buildGoModule {
pname = "ctfd-alerts";
-
version = "0.0.1";
+
version = "0.1.0";
subPackages = [ "." ]; # Build from root directory
src = self;
vendorHash = null;
+2 -7
go.mod
···
require (
github.com/charmbracelet/fang v0.2.0
+
github.com/charmbracelet/lipgloss v1.1.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/spf13/cobra v1.9.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
-
github.com/charmbracelet/bubbles v0.21.0 // indirect
-
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250623112707-45752038d08d // indirect
+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
-
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-cobra v1.2.0 // indirect
···
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
-
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
)
+1 -15
go.sum
···
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
-
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
-
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
-
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
-
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/fang v0.2.0 h1:F2sK2Zjy9kRYz/xUSF1o89DNj2BHKpxVKT7TA21KZi0=
···
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250623112707-45752038d08d h1:Y34SmGfNOuc7NxbbSXJDvIDv3BFNhj4LGWPxqk+YoNo=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250623112707-45752038d08d/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
-
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
-
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
-
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
···
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
-
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
-
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+11 -4
main.go
···
"github.com/charmbracelet/fang"
"github.com/spf13/cobra"
"github.com/taciturnaxolotl/ctfd-alerts/clients"
-
"github.com/taciturnaxolotl/ctfd-alerts/cmd/server"
+
"github.com/taciturnaxolotl/ctfd-alerts/cmd/serve"
"github.com/taciturnaxolotl/ctfd-alerts/cmd/status"
)
···
var cmd = &cobra.Command{
Use: "ctfd-alerts",
Short: "A tool for monitoring CTFd competitions",
-
Long: `ctfd-alerts is a command-line tool that helps you monitor CTFd-based
-
competitions by providing real-time updates, notifications, and status information.`,
+
Long: ` _ __ _ _ _
+
___| |_ / _| __| | __ _| | ___ _ __| |_ ___
+
/ __| __| |_ / _, |_____ / _ | |/ _ \ '__| __/ __|
+
| (__| |_| _| (_| |_____| (_| | | __/ | | |_\__ \
+
\___|\__|_| \__,_| \__,_|_|\___|_| \__|___/
+
+
ctfd-alerts is a command-line tool that helps you monitor CTFd-based
+
competitions by sending you ntfy notifications when someone bypasses
+
you or a new challenge is announced. You can also use the fancy status command :)`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
configFile, _ := cmd.Flags().GetString("config")
var err error
···
// Add commands
cmd.AddCommand(status.StatusCmd)
-
cmd.AddCommand(server.ServerCmd)
+
cmd.AddCommand(serve.ServeCmd)
}
func main() {