⛳ alerts for any ctfd instance via ntfy
1package status
2
3import (
4 "fmt"
5 "log"
6 "strings"
7
8 "github.com/charmbracelet/lipgloss"
9 "github.com/charmbracelet/lipgloss/table"
10 "github.com/spf13/cobra"
11 "github.com/taciturnaxolotl/ctfd-alerts/clients"
12)
13
14var (
15 // Colors
16 purple = lipgloss.AdaptiveColor{Light: "#9D8EFF", Dark: "#7D56F4"}
17 gray = lipgloss.AdaptiveColor{Light: "#BEBEBE", Dark: "#4A4A4A"}
18 lightGray = lipgloss.AdaptiveColor{Light: "#CCCCCC", Dark: "#3A3A3A"}
19 green = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}
20 red = lipgloss.AdaptiveColor{Light: "#F52D4F", Dark: "#FF5F7A"}
21
22 // Styles
23 titleStyle = lipgloss.NewStyle().
24 Foreground(purple).
25 Bold(true)
26
27 containerStyle = lipgloss.NewStyle().
28 BorderStyle(lipgloss.RoundedBorder()).
29 BorderForeground(purple).
30 Padding(1)
31
32 headerStyle = lipgloss.NewStyle().
33 Foreground(purple).
34 Bold(true).
35 Align(lipgloss.Center)
36
37 oddRowStyle = lipgloss.NewStyle()
38
39 evenRowStyle = lipgloss.NewStyle()
40 // Status indicators
41 solvedStyle = lipgloss.NewStyle().Foreground(green).SetString("✓")
42 unsolvedStyle = lipgloss.NewStyle().Foreground(red).SetString("✗")
43)
44
45func truncateString(s string, maxLen int) string {
46 if len(s) <= maxLen {
47 return s
48 }
49 return s[:maxLen-3] + "..."
50}
51
52func createTable(headers []string, rows [][]string) string {
53
54 t := table.New().
55 Border(lipgloss.ASCIIBorder()).
56 BorderStyle(lipgloss.NewStyle().Foreground(lightGray)).
57 StyleFunc(func(row, col int) lipgloss.Style {
58 switch {
59 case row == table.HeaderRow:
60 return headerStyle
61 case row%2 == 0:
62 return evenRowStyle
63 default:
64 return oddRowStyle
65 }
66 }).
67 Headers(headers...).
68 Rows(rows...)
69
70 return t.Render()
71}
72
73func runDashboard(cmd *cobra.Command, args []string) {
74 // Get CTFd client from root command context
75 ctfdClient := cmd.Context().Value("ctfd_client").(CTFdClient)
76
77 // Get scoreboard data
78 scoreboard, err := ctfdClient.GetScoreboard()
79 if err != nil {
80 log.Fatalf("Error fetching scoreboard: %v", err)
81 }
82
83 // Prepare scoreboard data
84 scoreboardHeaders := []string{"Pos", "Team", "Score", "Members"}
85 scoreboardRows := make([][]string, len(scoreboard.Data))
86
87 for i, team := range scoreboard.Data {
88 memberNames := make([]string, 0, len(team.Members))
89 for _, member := range team.Members {
90 memberNames = append(memberNames, member.Name)
91 }
92 scoreboardRows[i] = []string{
93 fmt.Sprintf("%d", team.Position),
94 truncateString(team.Name, 24),
95 fmt.Sprintf("%d", team.Score),
96 truncateString(strings.Join(memberNames, ", "), 39),
97 }
98 }
99
100 // Get challenge list
101 challenges, err := ctfdClient.GetChallengeList()
102 if err != nil {
103 log.Fatalf("Error fetching challenges: %v", err)
104 }
105
106 // Prepare challenge data
107 challengeHeaders := []string{"ID", "Name", "Category", "Value", "Solves", "Solved"}
108 challengeRows := make([][]string, len(challenges.Data))
109
110 for i, challenge := range challenges.Data {
111 solvedStatus := unsolvedStyle.String()
112 if challenge.SolvedByMe {
113 solvedStatus = solvedStyle.String()
114 }
115 challengeRows[i] = []string{
116 fmt.Sprintf("%d", challenge.ID),
117 truncateString(challenge.Name, 24),
118 truncateString(challenge.Category, 14),
119 fmt.Sprintf("%d", challenge.Value),
120 fmt.Sprintf("%d", challenge.Solves),
121 solvedStatus,
122 }
123 }
124
125 // Build and render the complete dashboard
126 var dashboard strings.Builder
127
128 // Scoreboard section
129 dashboard.WriteString(titleStyle.Render(fmt.Sprintf("CTFd Scoreboard [%d]", len(scoreboard.Data))))
130 dashboard.WriteString("\n")
131 dashboard.WriteString(createTable(scoreboardHeaders, scoreboardRows))
132
133 // Challenges section
134 dashboard.WriteString("\n\n")
135 dashboard.WriteString(titleStyle.Render(fmt.Sprintf("CTFd Challenges [%d]", len(challenges.Data))))
136 dashboard.WriteString("\n")
137 dashboard.WriteString(createTable(challengeHeaders, challengeRows))
138
139 // Render the final output
140 fmt.Print("\n")
141 fmt.Print(dashboard.String())
142 fmt.Print("\n")
143}
144
145// CTFdClient alias for the client interface from the clients package
146type CTFdClient = clients.CTFdClient