general cleanup - removed old comments, enforced naming conventions etc
Some checks are pending
check / check (push) Waiting to run

This commit is contained in:
KS Jannette
2026-03-04 23:29:58 -05:00
parent 44dad43f1d
commit 2c86bba235
55 changed files with 68 additions and 101 deletions

View File

@@ -0,0 +1,146 @@
package notifications
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
const (
// discordHTTPTimeoutSeconds is the timeout for Discord webhook requests.
discordHTTPTimeoutSeconds = 10
// Alert color codes for Discord embeds.
colorGreen = 0x00ff00
colorOrange = 0xff9900
colorRed = 0xff0000
colorBlue = 0x0099ff
)
var discordHTTPClient = &http.Client{ //nolint:gochecknoglobals
Timeout: discordHTTPTimeoutSeconds * time.Second,
}
// sends alert notifications via a Discord webhook.
type DiscordNotifier struct {
WebhookURL string
}
func (d *DiscordNotifier) Send(_ context.Context, message string, meta AlertMetadata) error {
_, err := SendDiscordNotification(d.WebhookURL, message, meta)
return err
}
type discordEmbed struct {
Title string `json:"title"`
Description string `json:"description"`
Color int `json:"color"`
Fields []discordField `json:"fields"`
Timestamp string `json:"timestamp"`
Footer discordFooter `json:"footer"`
}
type discordField struct {
Name string `json:"name"`
Value string `json:"value"`
Inline bool `json:"inline"`
}
type discordFooter struct {
Text string `json:"text"`
}
type discordPayload struct {
Content *string `json:"content"`
Embeds []discordEmbed `json:"embeds,omitempty"`
}
func SendDiscordNotification(webhookURL, message string, meta AlertMetadata) (bool, error) {
fields := []discordField{
{Name: "Address", Value: meta.AddressLabel, Inline: true},
{Name: "Blockchain Address", Value: fmt.Sprintf("`%s`", meta.Address), Inline: false},
}
if meta.TxHash != "" {
fields = append(fields, discordField{
Name: "Transaction",
Value: fmt.Sprintf("[View on Etherscan](https://etherscan.io/tx/%s)", meta.TxHash),
Inline: false,
})
}
payload := discordPayload{
Content: nil,
Embeds: []discordEmbed{
{
Title: "Koin Ping Alert",
Description: message,
Color: colorForAlertType(meta.AlertType),
Fields: fields,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Footer: discordFooter{Text: "Koin Ping"},
},
},
}
body, err := json.Marshal(payload)
if err != nil {
return false, fmt.Errorf("marshal discord payload: %w", err)
}
resp, err := discordHTTPClient.Post(
webhookURL, "application/json", bytes.NewReader(body),
)
if err != nil {
log.Printf("Failed to send Discord notification: %v", err)
return false, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Printf("Discord webhook failed: HTTP %d", resp.StatusCode)
return false, fmt.Errorf("discord webhook failed: HTTP %d", resp.StatusCode)
}
return true, nil
}
func TestDiscordWebhook(webhookURL string) (bool, error) {
payload := map[string]string{
"content": "Koin Ping test notification - Your Discord webhook is configured correctly!",
}
body, err := json.Marshal(payload)
if err != nil {
return false, err
}
resp, err := discordHTTPClient.Post(
webhookURL, "application/json", bytes.NewReader(body),
)
if err != nil {
log.Printf("Discord webhook test failed: %v", err)
return false, err
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300, nil
}
func colorForAlertType(alertType string) int {
switch alertType {
case "incoming_tx":
return colorGreen
case "outgoing_tx":
return colorOrange
case "large_transfer":
return colorRed
case "balance_below":
return colorRed
default:
return colorBlue
}
}

View File

@@ -0,0 +1,154 @@
package notifications
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
type EmailNotifier struct {
APIKey string
From string
To string
}
func (e *EmailNotifier) Send(_ context.Context, message string, meta AlertMetadata) error {
_, err := SendEmailNotification(e.APIKey, e.From, e.To, message, meta)
return err
}
const emailHTTPTimeoutSeconds = 10
var emailHTTPClient = &http.Client{ //nolint:gochecknoglobals
Timeout: emailHTTPTimeoutSeconds * time.Second,
}
type resendPayload struct {
From string `json:"from"`
To string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html"`
}
func SendEmailNotification(apiKey, fromAddress, toAddress, message string, meta AlertMetadata) (bool, error) {
if apiKey == "" {
log.Printf("Skipping email notification: RESEND_API_KEY not configured")
return false, nil
}
subject := fmt.Sprintf("Koin Ping Alert: %s", alertTypeLabel(meta.AlertType))
txLink := ""
if meta.TxHash != "" {
txLink = fmt.Sprintf(
`<p><a href="https://etherscan.io/tx/%s">View on Etherscan</a></p>`,
meta.TxHash,
)
}
html := fmt.Sprintf(`
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">Koin Ping Alert</h2>
<p style="font-size: 16px;">%s</p>
<table style="margin: 16px 0; border-collapse: collapse;">
<tr>
<td style="padding: 4px 12px 4px 0; color: #666;">Address</td>
<td style="padding: 4px 0;">%s</td>
</tr>
<tr>
<td style="padding: 4px 12px 4px 0; color: #666;">Blockchain</td>
<td style="padding: 4px 0; font-family: monospace; font-size: 13px;">%s</td>
</tr>
</table>
%s
<hr style="border: none; border-top: 1px solid #eee; margin: 24px 0;" />
<p style="font-size: 12px; color: #999;">Sent by Koin Ping</p>
</div>`,
message, meta.AddressLabel, meta.Address, txLink)
payload := resendPayload{
From: fromAddress,
To: toAddress,
Subject: subject,
HTML: html,
}
body, err := json.Marshal(payload)
if err != nil {
return false, fmt.Errorf("marshal email payload: %w", err)
}
req, err := http.NewRequest(http.MethodPost, "https://api.resend.com/emails", bytes.NewReader(body))
if err != nil {
return false, fmt.Errorf("create email request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := emailHTTPClient.Do(req)
if err != nil {
log.Printf("Failed to send email notification: %v", err)
return false, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Printf("Resend API failed: HTTP %d", resp.StatusCode)
return false, fmt.Errorf("resend API failed: HTTP %d", resp.StatusCode)
}
return true, nil
}
func TestEmailNotification(apiKey, fromAddress, toAddress string) (bool, error) {
if apiKey == "" {
return false, fmt.Errorf("email not configured: RESEND_API_KEY not set") //nolint:err113
}
payload := resendPayload{
From: fromAddress,
To: toAddress,
Subject: "Koin Ping — Test Notification",
HTML: `<p>Your email alerts are configured correctly!</p><p style="font-size:12px;color:#999;">Sent by Koin Ping</p>`,
}
body, err := json.Marshal(payload)
if err != nil {
return false, err
}
req, err := http.NewRequest(http.MethodPost, "https://api.resend.com/emails", bytes.NewReader(body))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := emailHTTPClient.Do(req)
if err != nil {
log.Printf("Email test failed: %v", err)
return false, err
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300, nil
}
func alertTypeLabel(alertType string) string {
switch alertType {
case "incoming_tx":
return "Incoming Transaction"
case "outgoing_tx":
return "Outgoing Transaction"
case "large_transfer":
return "Large Transfer"
case "balance_below":
return "Balance Below Threshold"
default:
return "Alert"
}
}

View File

@@ -0,0 +1,15 @@
package notifications
import "context"
// AlertMetadata holds context about the alert being sent.
type AlertMetadata struct {
TxHash string
AddressLabel string
AlertType string
Address string
}
type Notifier interface {
Send(ctx context.Context, message string, meta AlertMetadata) error
}

View File

@@ -0,0 +1,128 @@
package notifications
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// SlackNotifier sends alert notifications via a Slack webhook.
type SlackNotifier struct {
WebhookURL string
}
// Send implements Notifier for Slack.
func (s *SlackNotifier) Send(_ context.Context, message string, meta AlertMetadata) error {
_, err := SendSlackNotification(s.WebhookURL, message, meta)
return err
}
const slackHTTPTimeoutSeconds = 10
var slackHTTPClient = &http.Client{ //nolint:gochecknoglobals
Timeout: slackHTTPTimeoutSeconds * time.Second,
}
type slackAttachment struct {
Color string `json:"color"`
Title string `json:"title"`
Text string `json:"text"`
Fields []slackField `json:"fields"`
Footer string `json:"footer"`
Ts int64 `json:"ts"`
}
type slackField struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}
type slackPayload struct {
Text string `json:"text,omitempty"`
Attachments []slackAttachment `json:"attachments,omitempty"`
}
func SendSlackNotification(webhookURL, message string, meta AlertMetadata) (bool, error) {
fields := []slackField{
{Title: "Address", Value: meta.AddressLabel, Short: true},
{Title: "Blockchain Address", Value: fmt.Sprintf("`%s`", meta.Address), Short: false},
}
if meta.TxHash != "" {
fields = append(fields, slackField{
Title: "Transaction",
Value: fmt.Sprintf("<https://etherscan.io/tx/%s|View on Etherscan>", meta.TxHash),
Short: false,
})
}
payload := slackPayload{
Attachments: []slackAttachment{
{
Color: slackColorForAlertType(meta.AlertType),
Title: "Koin Ping Alert",
Text: message,
Fields: fields,
Footer: "Koin Ping",
Ts: time.Now().Unix(),
},
},
}
body, err := json.Marshal(payload)
if err != nil {
return false, fmt.Errorf("marshal slack payload: %w", err)
}
resp, err := slackHTTPClient.Post(webhookURL, "application/json", bytes.NewReader(body))
if err != nil {
log.Printf("Failed to send Slack notification: %v", err)
return false, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Printf("Slack webhook failed: HTTP %d", resp.StatusCode)
return false, fmt.Errorf("slack webhook failed: HTTP %d", resp.StatusCode)
}
return true, nil
}
func TestSlackWebhook(webhookURL string) (bool, error) {
payload := slackPayload{
Text: "Koin Ping test notification — Your Slack alerts are configured correctly!",
}
body, err := json.Marshal(payload)
if err != nil {
return false, err
}
resp, err := slackHTTPClient.Post(webhookURL, "application/json", bytes.NewReader(body))
if err != nil {
log.Printf("Slack webhook test failed: %v", err)
return false, err
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300, nil
}
func slackColorForAlertType(alertType string) string {
switch alertType {
case "incoming_tx":
return "#00ff00"
case "outgoing_tx":
return "#ff9900"
case "large_transfer", "balance_below":
return "#ff0000"
default:
return "#0099ff"
}
}

View File

@@ -0,0 +1,122 @@
package notifications
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// TelegramNotifier sends alert notifications via Telegram.
type TelegramNotifier struct {
BotToken string
ChatID string
}
// Send implements Notifier for Telegram.
func (t *TelegramNotifier) Send(_ context.Context, message string, meta AlertMetadata) error {
_, err := SendTelegramNotification(t.BotToken, t.ChatID, message, meta)
return err
}
const telegramHTTPTimeoutSeconds = 10
var telegramHTTPClient = &http.Client{ //nolint:gochecknoglobals
Timeout: telegramHTTPTimeoutSeconds * time.Second,
}
type telegramPayload struct {
ChatID string `json:"chat_id"`
Text string `json:"text"`
ParseMode string `json:"parse_mode"`
}
func SendTelegramNotification(botToken, chatID, message string, meta AlertMetadata) (bool, error) {
text := fmt.Sprintf("*Koin Ping Alert*\n\n%s\n\n*Address:* %s\n`%s`",
escapeMarkdown(message), escapeMarkdown(meta.AddressLabel), meta.Address)
if meta.TxHash != "" {
text += fmt.Sprintf("\n\n[View on Etherscan](https://etherscan.io/tx/%s)", meta.TxHash)
}
payload := telegramPayload{
ChatID: chatID,
Text: text,
ParseMode: "Markdown",
}
body, err := json.Marshal(payload)
if err != nil {
return false, fmt.Errorf("marshal telegram payload: %w", err)
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
resp, err := telegramHTTPClient.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
log.Printf("Failed to send Telegram notification: %v", err)
return false, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Printf("Telegram API failed: HTTP %d", resp.StatusCode)
return false, fmt.Errorf("telegram API failed: HTTP %d", resp.StatusCode)
}
return true, nil
}
func TestTelegramWebhook(botToken, chatID string) (bool, error) {
payload := telegramPayload{
ChatID: chatID,
Text: "Koin Ping test notification — Your Telegram alerts are configured correctly!",
ParseMode: "Markdown",
}
body, err := json.Marshal(payload)
if err != nil {
return false, err
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
resp, err := telegramHTTPClient.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
log.Printf("Telegram test failed: %v", err)
return false, err
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300, nil
}
func escapeMarkdown(s string) string {
replacer := []struct{ old, new string }{
{"_", "\\_"}, {"*", "\\*"}, {"[", "\\["}, {"]", "\\]"},
{"(", "\\("}, {")", "\\)"}, {"~", "\\~"}, {"`", "\\`"},
{">", "\\>"}, {"#", "\\#"}, {"+", "\\+"}, {"-", "\\-"},
{"=", "\\="}, {"|", "\\|"}, {"{", "\\{"}, {"}", "\\}"},
{".", "\\."}, {"!", "\\!"},
}
result := s
for _, r := range replacer {
result = replaceAll(result, r.old, r.new)
}
return result
}
func replaceAll(s, old, new string) string {
out := ""
for i := 0; i < len(s); i++ {
if string(s[i]) == old {
out += new
} else {
out += string(s[i])
}
}
return out
}