Skip to content

Commit 6ac4f53

Browse files
FEATURE (notifiers): Add Discord notifier
1 parent 72c62c8 commit 6ac4f53

File tree

18 files changed

+268
-12
lines changed

18 files changed

+268
-12
lines changed

backend/internal/features/notifiers/enums.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ const (
77
NotifierTypeTelegram NotifierType = "TELEGRAM"
88
NotifierTypeWebhook NotifierType = "WEBHOOK"
99
NotifierTypeSlack NotifierType = "SLACK"
10+
NotifierTypeDiscord NotifierType = "DISCORD"
1011
)

backend/internal/features/notifiers/model.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package notifiers
33
import (
44
"errors"
55
"log/slog"
6+
discord_notifier "postgresus-backend/internal/features/notifiers/models/discord"
67
"postgresus-backend/internal/features/notifiers/models/email_notifier"
78
slack_notifier "postgresus-backend/internal/features/notifiers/models/slack"
89
telegram_notifier "postgresus-backend/internal/features/notifiers/models/telegram"
@@ -23,6 +24,7 @@ type Notifier struct {
2324
EmailNotifier *email_notifier.EmailNotifier `json:"emailNotifier" gorm:"foreignKey:NotifierID"`
2425
WebhookNotifier *webhook_notifier.WebhookNotifier `json:"webhookNotifier" gorm:"foreignKey:NotifierID"`
2526
SlackNotifier *slack_notifier.SlackNotifier `json:"slackNotifier" gorm:"foreignKey:NotifierID"`
27+
DiscordNotifier *discord_notifier.DiscordNotifier `json:"discordNotifier" gorm:"foreignKey:NotifierID"`
2628
}
2729

2830
func (n *Notifier) TableName() string {
@@ -60,6 +62,8 @@ func (n *Notifier) getSpecificNotifier() NotificationSender {
6062
return n.WebhookNotifier
6163
case NotifierTypeSlack:
6264
return n.SlackNotifier
65+
case NotifierTypeDiscord:
66+
return n.DiscordNotifier
6367
default:
6468
panic("unknown notifier type: " + string(n.NotifierType))
6569
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package discord_notifier
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"log/slog"
10+
"net/http"
11+
12+
"github.com/google/uuid"
13+
)
14+
15+
type DiscordNotifier struct {
16+
NotifierID uuid.UUID `json:"notifierId" gorm:"primaryKey;column:notifier_id"`
17+
ChannelWebhookURL string `json:"channelWebhookUrl" gorm:"not null;column:channel_webhook_url"`
18+
}
19+
20+
func (d *DiscordNotifier) TableName() string {
21+
return "discord_notifiers"
22+
}
23+
24+
func (d *DiscordNotifier) Validate() error {
25+
if d.ChannelWebhookURL == "" {
26+
return errors.New("webhook URL is required")
27+
}
28+
29+
return nil
30+
}
31+
32+
func (d *DiscordNotifier) Send(logger *slog.Logger, heading string, message string) error {
33+
fullMessage := heading
34+
if message != "" {
35+
fullMessage = fmt.Sprintf("%s\n\n%s", heading, message)
36+
}
37+
38+
payload := map[string]interface{}{
39+
"content": fullMessage,
40+
}
41+
42+
jsonPayload, err := json.Marshal(payload)
43+
if err != nil {
44+
return fmt.Errorf("failed to marshal Discord payload: %w", err)
45+
}
46+
47+
req, err := http.NewRequest("POST", d.ChannelWebhookURL, bytes.NewReader(jsonPayload))
48+
if err != nil {
49+
return fmt.Errorf("failed to create request: %w", err)
50+
}
51+
52+
req.Header.Set("Content-Type", "application/json")
53+
54+
client := &http.Client{}
55+
resp, err := client.Do(req)
56+
if err != nil {
57+
return fmt.Errorf("failed to send Discord message: %w", err)
58+
}
59+
defer func() {
60+
_ = resp.Body.Close()
61+
}()
62+
63+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
64+
bodyBytes, _ := io.ReadAll(resp.Body)
65+
return fmt.Errorf(
66+
"discord API returned non-OK status: %s. Error: %s",
67+
resp.Status,
68+
string(bodyBytes),
69+
)
70+
}
71+
72+
return nil
73+
}

backend/internal/features/notifiers/models/email_notifier/model.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (e *EmailNotifier) Send(logger *slog.Logger, heading string, message string
8080
timeout := DefaultTimeout
8181

8282
// Determine if authentication is required
83-
authRequired := e.SMTPUser != "" && e.SMTPPassword != ""
83+
isAuthRequired := e.SMTPUser != "" && e.SMTPPassword != ""
8484

8585
// Handle different port scenarios
8686
if e.SMTPPort == ImplicitTLSPort {
@@ -110,7 +110,7 @@ func (e *EmailNotifier) Send(logger *slog.Logger, heading string, message string
110110
}()
111111

112112
// Set up authentication only if credentials are provided
113-
if authRequired {
113+
if isAuthRequired {
114114
auth := smtp.PlainAuth("", e.SMTPUser, e.SMTPPassword, e.SMTPHost)
115115
if err := client.Auth(auth); err != nil {
116116
return fmt.Errorf("SMTP authentication failed: %w", err)
@@ -173,7 +173,7 @@ func (e *EmailNotifier) Send(logger *slog.Logger, heading string, message string
173173
}
174174

175175
// Authenticate only if credentials are provided
176-
if authRequired {
176+
if isAuthRequired {
177177
auth := smtp.PlainAuth("", e.SMTPUser, e.SMTPPassword, e.SMTPHost)
178178
if err := client.Auth(auth); err != nil {
179179
return fmt.Errorf("SMTP authentication failed: %w", err)

backend/internal/features/notifiers/repository.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,33 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) {
3030
if notifier.SlackNotifier != nil {
3131
notifier.SlackNotifier.NotifierID = notifier.ID
3232
}
33+
case NotifierTypeDiscord:
34+
if notifier.DiscordNotifier != nil {
35+
notifier.DiscordNotifier.NotifierID = notifier.ID
36+
}
3337
}
3438

3539
if notifier.ID == uuid.Nil {
3640
if err := tx.Create(notifier).
37-
Omit("TelegramNotifier", "EmailNotifier", "WebhookNotifier", "SlackNotifier").
41+
Omit(
42+
"TelegramNotifier",
43+
"EmailNotifier",
44+
"WebhookNotifier",
45+
"SlackNotifier",
46+
"DiscordNotifier",
47+
).
3848
Error; err != nil {
3949
return err
4050
}
4151
} else {
4252
if err := tx.Save(notifier).
43-
Omit("TelegramNotifier", "EmailNotifier", "WebhookNotifier", "SlackNotifier").
53+
Omit(
54+
"TelegramNotifier",
55+
"EmailNotifier",
56+
"WebhookNotifier",
57+
"SlackNotifier",
58+
"DiscordNotifier",
59+
).
4460
Error; err != nil {
4561
return err
4662
}
@@ -75,6 +91,13 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) {
7591
return err
7692
}
7793
}
94+
case NotifierTypeDiscord:
95+
if notifier.DiscordNotifier != nil {
96+
notifier.DiscordNotifier.NotifierID = notifier.ID // Ensure ID is set
97+
if err := tx.Save(notifier.DiscordNotifier).Error; err != nil {
98+
return err
99+
}
100+
}
78101
}
79102

80103
return nil
@@ -96,6 +119,7 @@ func (r *NotifierRepository) FindByID(id uuid.UUID) (*Notifier, error) {
96119
Preload("EmailNotifier").
97120
Preload("WebhookNotifier").
98121
Preload("SlackNotifier").
122+
Preload("DiscordNotifier").
99123
Where("id = ?", id).
100124
First(&notifier).Error; err != nil {
101125
return nil, err
@@ -113,7 +137,9 @@ func (r *NotifierRepository) FindByUserID(userID uuid.UUID) ([]*Notifier, error)
113137
Preload("EmailNotifier").
114138
Preload("WebhookNotifier").
115139
Preload("SlackNotifier").
140+
Preload("DiscordNotifier").
116141
Where("user_id = ?", userID).
142+
Order("name ASC").
117143
Find(&notifiers).Error; err != nil {
118144
return nil, err
119145
}
@@ -149,6 +175,12 @@ func (r *NotifierRepository) Delete(notifier *Notifier) error {
149175
return err
150176
}
151177
}
178+
case NotifierTypeDiscord:
179+
if notifier.DiscordNotifier != nil {
180+
if err := tx.Delete(notifier.DiscordNotifier).Error; err != nil {
181+
return err
182+
}
183+
}
152184
}
153185

154186
// Delete the main notifier
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
4+
-- Create discord notifiers table
5+
CREATE TABLE discord_notifiers (
6+
notifier_id UUID PRIMARY KEY,
7+
channel_webhook_url TEXT NOT NULL
8+
);
9+
10+
ALTER TABLE discord_notifiers
11+
ADD CONSTRAINT fk_discord_notifiers_notifier
12+
FOREIGN KEY (notifier_id)
13+
REFERENCES notifiers (id)
14+
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
15+
16+
-- +goose StatementEnd
17+
18+
-- +goose Down
19+
-- +goose StatementBegin
20+
21+
DROP TABLE IF EXISTS discord_notifiers;
22+
23+
-- +goose StatementEnd

frontend/public/icons/notifiers/discord.svg

Lines changed: 8 additions & 0 deletions
Loading

frontend/src/entity/notifiers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ export { WebhookMethod } from './models/webhook/WebhookMethod';
1414

1515
export type { SlackNotifier } from './models/slack/SlackNotifier';
1616
export { validateSlackNotifier } from './models/slack/validateSlackNotifier';
17+
18+
export type { DiscordNotifier } from './models/discord/DiscordNotifier';
19+
export { validateDiscordNotifier } from './models/discord/validateDiscordNotifier';

frontend/src/entity/notifiers/models/Notifier.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { NotifierType } from './NotifierType';
2+
import type { DiscordNotifier } from './discord/DiscordNotifier';
23
import type { EmailNotifier } from './email/EmailNotifier';
34
import type { SlackNotifier } from './slack/SlackNotifier';
45
import type { TelegramNotifier } from './telegram/TelegramNotifier';
@@ -15,4 +16,5 @@ export interface Notifier {
1516
emailNotifier?: EmailNotifier;
1617
webhookNotifier?: WebhookNotifier;
1718
slackNotifier?: SlackNotifier;
19+
discordNotifier?: DiscordNotifier;
1820
}

frontend/src/entity/notifiers/models/NotifierType.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export enum NotifierType {
33
TELEGRAM = 'TELEGRAM',
44
WEBHOOK = 'WEBHOOK',
55
SLACK = 'SLACK',
6+
DISCORD = 'DISCORD',
67
}

0 commit comments

Comments
 (0)