Skip to content

Commit 0aa60ca

Browse files
SerhiiZahubaRostislavDugin
authored andcommitted
FEATURE (notifiers): Add MS Teams notifier
1 parent dee330e commit 0aa60ca

File tree

19 files changed

+356
-18
lines changed

19 files changed

+356
-18
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ postgresus-data/
33
.env
44
pgdata/
55
docker-compose.yml
6-
node_modules/
6+
node_modules/
7+
.idea

backend/internal/features/notifiers/enums.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ const (
88
NotifierTypeWebhook NotifierType = "WEBHOOK"
99
NotifierTypeSlack NotifierType = "SLACK"
1010
NotifierTypeDiscord NotifierType = "DISCORD"
11+
NotifierTypeTeams NotifierType = "TEAMS"
1112
)

backend/internal/features/notifiers/model.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
discord_notifier "postgresus-backend/internal/features/notifiers/models/discord"
77
"postgresus-backend/internal/features/notifiers/models/email_notifier"
88
slack_notifier "postgresus-backend/internal/features/notifiers/models/slack"
9+
teams_notifier "postgresus-backend/internal/features/notifiers/models/teams"
910
telegram_notifier "postgresus-backend/internal/features/notifiers/models/telegram"
1011
webhook_notifier "postgresus-backend/internal/features/notifiers/models/webhook"
1112

@@ -20,11 +21,12 @@ type Notifier struct {
2021
LastSendError *string `json:"lastSendError" gorm:"column:last_send_error;type:text"`
2122

2223
// specific notifier
23-
TelegramNotifier *telegram_notifier.TelegramNotifier `json:"telegramNotifier" gorm:"foreignKey:NotifierID"`
24-
EmailNotifier *email_notifier.EmailNotifier `json:"emailNotifier" gorm:"foreignKey:NotifierID"`
25-
WebhookNotifier *webhook_notifier.WebhookNotifier `json:"webhookNotifier" gorm:"foreignKey:NotifierID"`
26-
SlackNotifier *slack_notifier.SlackNotifier `json:"slackNotifier" gorm:"foreignKey:NotifierID"`
27-
DiscordNotifier *discord_notifier.DiscordNotifier `json:"discordNotifier" gorm:"foreignKey:NotifierID"`
24+
TelegramNotifier *telegram_notifier.TelegramNotifier `json:"telegramNotifier" gorm:"foreignKey:NotifierID"`
25+
EmailNotifier *email_notifier.EmailNotifier `json:"emailNotifier" gorm:"foreignKey:NotifierID"`
26+
WebhookNotifier *webhook_notifier.WebhookNotifier `json:"webhookNotifier" gorm:"foreignKey:NotifierID"`
27+
SlackNotifier *slack_notifier.SlackNotifier `json:"slackNotifier" gorm:"foreignKey:NotifierID"`
28+
DiscordNotifier *discord_notifier.DiscordNotifier `json:"discordNotifier" gorm:"foreignKey:NotifierID"`
29+
TeamsNotifier *teams_notifier.TeamsNotifier `json:"teamsNotifier,omitempty" gorm:"foreignKey:NotifierID;constraint:OnDelete:CASCADE"`
2830
}
2931

3032
func (n *Notifier) TableName() string {
@@ -64,6 +66,8 @@ func (n *Notifier) getSpecificNotifier() NotificationSender {
6466
return n.SlackNotifier
6567
case NotifierTypeDiscord:
6668
return n.DiscordNotifier
69+
case NotifierTypeTeams:
70+
return n.TeamsNotifier
6771
default:
6872
panic("unknown notifier type: " + string(n.NotifierType))
6973
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package teams_notifier
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"log/slog"
9+
"net/http"
10+
"net/url"
11+
12+
"github.com/google/uuid"
13+
)
14+
15+
type TeamsNotifier struct {
16+
NotifierID uuid.UUID `gorm:"type:uuid;primaryKey;column:notifier_id" json:"notifierId"`
17+
WebhookURL string `gorm:"type:text;not null;column:power_automate_url" json:"powerAutomateUrl"`
18+
}
19+
20+
func (TeamsNotifier) TableName() string {
21+
return "teams_notifiers"
22+
}
23+
24+
func (n *TeamsNotifier) Validate() error {
25+
if n.WebhookURL == "" {
26+
return errors.New("webhook_url is required")
27+
}
28+
u, err := url.Parse(n.WebhookURL)
29+
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
30+
return errors.New("invalid webhook_url")
31+
}
32+
return nil
33+
}
34+
35+
type cardAttachment struct {
36+
ContentType string `json:"contentType"`
37+
Content interface{} `json:"content"`
38+
}
39+
40+
type payload struct {
41+
Title string `json:"title"`
42+
Text string `json:"text"`
43+
Attachments []cardAttachment `json:"attachments,omitempty"`
44+
}
45+
46+
func (n *TeamsNotifier) Send(logger *slog.Logger, heading, message string) error {
47+
if err := n.Validate(); err != nil {
48+
return err
49+
}
50+
51+
card := map[string]any{
52+
"type": "AdaptiveCard",
53+
"version": "1.4",
54+
"body": []any{
55+
map[string]any{
56+
"type": "TextBlock",
57+
"size": "Medium",
58+
"weight": "Bolder",
59+
"text": heading,
60+
},
61+
map[string]any{"type": "TextBlock", "wrap": true, "text": message},
62+
},
63+
}
64+
65+
p := payload{
66+
Title: heading,
67+
Text: message,
68+
Attachments: []cardAttachment{
69+
{ContentType: "application/vnd.microsoft.card.adaptive", Content: card},
70+
},
71+
}
72+
73+
body, _ := json.Marshal(p)
74+
req, err := http.NewRequest(http.MethodPost, n.WebhookURL, bytes.NewReader(body))
75+
if err != nil {
76+
return err
77+
}
78+
req.Header.Set("Content-Type", "application/json")
79+
80+
resp, err := http.DefaultClient.Do(req)
81+
if err != nil {
82+
return err
83+
}
84+
85+
defer func() {
86+
if closeErr := resp.Body.Close(); closeErr != nil {
87+
logger.Error("failed to close response body", "error", closeErr)
88+
}
89+
}()
90+
91+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
92+
return fmt.Errorf("teams webhook returned status %d", resp.StatusCode)
93+
}
94+
95+
return nil
96+
}

backend/internal/features/notifiers/repository.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) {
1313
db := storage.GetDb()
1414

1515
err := db.Transaction(func(tx *gorm.DB) error {
16+
1617
switch notifier.NotifierType {
1718
case NotifierTypeTelegram:
1819
if notifier.TelegramNotifier != nil {
@@ -34,70 +35,83 @@ func (r *NotifierRepository) Save(notifier *Notifier) (*Notifier, error) {
3435
if notifier.DiscordNotifier != nil {
3536
notifier.DiscordNotifier.NotifierID = notifier.ID
3637
}
38+
case NotifierTypeTeams:
39+
if notifier.TeamsNotifier != nil {
40+
notifier.TeamsNotifier.NotifierID = notifier.ID
41+
}
3742
}
3843

3944
if notifier.ID == uuid.Nil {
40-
if err := tx.Create(notifier).
45+
if err := tx.
4146
Omit(
4247
"TelegramNotifier",
4348
"EmailNotifier",
4449
"WebhookNotifier",
4550
"SlackNotifier",
4651
"DiscordNotifier",
52+
"TeamsNotifier",
4753
).
48-
Error; err != nil {
54+
Create(notifier).Error; err != nil {
4955
return err
5056
}
5157
} else {
52-
if err := tx.Save(notifier).
58+
if err := tx.
5359
Omit(
5460
"TelegramNotifier",
5561
"EmailNotifier",
5662
"WebhookNotifier",
5763
"SlackNotifier",
5864
"DiscordNotifier",
65+
"TeamsNotifier",
5966
).
60-
Error; err != nil {
67+
Save(notifier).Error; err != nil {
6168
return err
6269
}
6370
}
6471

6572
switch notifier.NotifierType {
6673
case NotifierTypeTelegram:
6774
if notifier.TelegramNotifier != nil {
68-
notifier.TelegramNotifier.NotifierID = notifier.ID // Ensure ID is set
75+
notifier.TelegramNotifier.NotifierID = notifier.ID
6976
if err := tx.Save(notifier.TelegramNotifier).Error; err != nil {
7077
return err
7178
}
7279
}
7380
case NotifierTypeEmail:
7481
if notifier.EmailNotifier != nil {
75-
notifier.EmailNotifier.NotifierID = notifier.ID // Ensure ID is set
82+
notifier.EmailNotifier.NotifierID = notifier.ID
7683
if err := tx.Save(notifier.EmailNotifier).Error; err != nil {
7784
return err
7885
}
7986
}
8087
case NotifierTypeWebhook:
8188
if notifier.WebhookNotifier != nil {
82-
notifier.WebhookNotifier.NotifierID = notifier.ID // Ensure ID is set
89+
notifier.WebhookNotifier.NotifierID = notifier.ID
8390
if err := tx.Save(notifier.WebhookNotifier).Error; err != nil {
8491
return err
8592
}
8693
}
8794
case NotifierTypeSlack:
8895
if notifier.SlackNotifier != nil {
89-
notifier.SlackNotifier.NotifierID = notifier.ID // Ensure ID is set
96+
notifier.SlackNotifier.NotifierID = notifier.ID
9097
if err := tx.Save(notifier.SlackNotifier).Error; err != nil {
9198
return err
9299
}
93100
}
94101
case NotifierTypeDiscord:
95102
if notifier.DiscordNotifier != nil {
96-
notifier.DiscordNotifier.NotifierID = notifier.ID // Ensure ID is set
103+
notifier.DiscordNotifier.NotifierID = notifier.ID
97104
if err := tx.Save(notifier.DiscordNotifier).Error; err != nil {
98105
return err
99106
}
100107
}
108+
case NotifierTypeTeams:
109+
if notifier.TeamsNotifier != nil {
110+
notifier.TeamsNotifier.NotifierID = notifier.ID
111+
if err := tx.Save(notifier.TeamsNotifier).Error; err != nil {
112+
return err
113+
}
114+
}
101115
}
102116

103117
return nil
@@ -120,6 +134,7 @@ func (r *NotifierRepository) FindByID(id uuid.UUID) (*Notifier, error) {
120134
Preload("WebhookNotifier").
121135
Preload("SlackNotifier").
122136
Preload("DiscordNotifier").
137+
Preload("TeamsNotifier").
123138
Where("id = ?", id).
124139
First(&notifier).Error; err != nil {
125140
return nil, err
@@ -138,6 +153,7 @@ func (r *NotifierRepository) FindByUserID(userID uuid.UUID) ([]*Notifier, error)
138153
Preload("WebhookNotifier").
139154
Preload("SlackNotifier").
140155
Preload("DiscordNotifier").
156+
Preload("TeamsNotifier").
141157
Where("user_id = ?", userID).
142158
Order("name ASC").
143159
Find(&notifiers).Error; err != nil {
@@ -149,7 +165,7 @@ func (r *NotifierRepository) FindByUserID(userID uuid.UUID) ([]*Notifier, error)
149165

150166
func (r *NotifierRepository) Delete(notifier *Notifier) error {
151167
return storage.GetDb().Transaction(func(tx *gorm.DB) error {
152-
// Delete specific notifier based on type
168+
153169
switch notifier.NotifierType {
154170
case NotifierTypeTelegram:
155171
if notifier.TelegramNotifier != nil {
@@ -181,9 +197,14 @@ func (r *NotifierRepository) Delete(notifier *Notifier) error {
181197
return err
182198
}
183199
}
200+
case NotifierTypeTeams:
201+
if notifier.TeamsNotifier != nil {
202+
if err := tx.Delete(notifier.TeamsNotifier).Error; err != nil {
203+
return err
204+
}
205+
}
184206
}
185207

186-
// Delete the main notifier
187208
return tx.Delete(notifier).Error
188209
})
189210
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
4+
CREATE TABLE teams_notifiers (
5+
notifier_id UUID PRIMARY KEY,
6+
power_automate_url TEXT NOT NULL
7+
);
8+
9+
ALTER TABLE teams_notifiers
10+
ADD CONSTRAINT fk_teams_notifiers_notifier
11+
FOREIGN KEY (notifier_id)
12+
REFERENCES notifiers (id)
13+
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
14+
15+
-- +goose StatementEnd
16+
17+
-- +goose Down
18+
-- +goose StatementBegin
19+
DROP TABLE IF EXISTS teams_notifiers;
20+
-- +goose StatementEnd
Lines changed: 2 additions & 0 deletions
Loading

frontend/src/entity/notifiers/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ export { validateSlackNotifier } from './models/slack/validateSlackNotifier';
1717

1818
export type { DiscordNotifier } from './models/discord/DiscordNotifier';
1919
export { validateDiscordNotifier } from './models/discord/validateDiscordNotifier';
20+
21+
export type { TeamsNotifier } from './models/teams/TeamsNotifier';
22+
export { validateTeamsNotifier } from './models/teams/validateTeamsNotifier';

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NotifierType } from './NotifierType';
22
import type { DiscordNotifier } from './discord/DiscordNotifier';
33
import type { EmailNotifier } from './email/EmailNotifier';
44
import type { SlackNotifier } from './slack/SlackNotifier';
5+
import type { TeamsNotifier } from './teams/TeamsNotifier';
56
import type { TelegramNotifier } from './telegram/TelegramNotifier';
67
import type { WebhookNotifier } from './webhook/WebhookNotifier';
78

@@ -17,4 +18,5 @@ export interface Notifier {
1718
webhookNotifier?: WebhookNotifier;
1819
slackNotifier?: SlackNotifier;
1920
discordNotifier?: DiscordNotifier;
21+
teamsNotifier?: TeamsNotifier;
2022
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export enum NotifierType {
44
WEBHOOK = 'WEBHOOK',
55
SLACK = 'SLACK',
66
DISCORD = 'DISCORD',
7+
TEAMS = 'TEAMS',
78
}

0 commit comments

Comments
 (0)