feat(notifications): add telegram support (#299)

* feat(notifications): add telegram support

* feat(notifications): change list view

* refactor(notifications): overall setup

* feat(notifications): forms add telegram
This commit is contained in:
Ludvig Lundgren 2022-06-13 17:01:36 +02:00 committed by GitHub
parent 2ab7133dd0
commit 38addb99e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 630 additions and 457 deletions

View file

@ -5,16 +5,18 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/logger"
)
type DiscordMessage struct {
Content interface{} `json:"content"`
Embeds []DiscordEmbeds `json:"embeds"`
Embeds []DiscordEmbeds `json:"embeds,omitempty"`
Username string `json:"username"`
}
@ -22,7 +24,7 @@ type DiscordEmbeds struct {
Title string `json:"title"`
Description string `json:"description"`
Color int `json:"color"`
Fields []DiscordEmbedsFields `json:"fields"`
Fields []DiscordEmbedsFields `json:"fields,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
type DiscordEmbedsFields struct {
@ -31,7 +33,46 @@ type DiscordEmbedsFields struct {
Inline bool `json:"inline,omitempty"`
}
func (s *service) discordNotification(event domain.EventsReleasePushed, webhookURL string) {
type EmbedColors int
const (
LIGHT_BLUE EmbedColors = 5814783 // 58b9ff
RED EmbedColors = 15548997 // ed4245
GREEN EmbedColors = 5763719 // 57f287
GRAY EmbedColors = 10070709 // 99aab5
)
type discordSender struct {
log logger.Logger
Settings domain.Notification
}
func NewDiscordSender(log logger.Logger, settings domain.Notification) domain.NotificationSender {
return &discordSender{log: log, Settings: settings}
}
func (a *discordSender) Send(event domain.NotificationEvent, payload domain.NotificationPayload) error {
m := DiscordMessage{
Content: nil,
Embeds: []DiscordEmbeds{a.buildEmbed(event, payload)},
Username: "brr",
}
jsonData, err := json.Marshal(m)
if err != nil {
a.log.Error().Err(err).Msgf("discord client could not marshal data: %v", m)
return err
}
req, err := http.NewRequest(http.MethodPost, a.Settings.Webhook, bytes.NewBuffer(jsonData))
if err != nil {
a.log.Error().Err(err).Msgf("discord client request error: %v", event)
return err
}
req.Header.Set("Content-Type", "application/json")
//req.Header.Set("User-Agent", "autobrr")
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
@ -39,107 +80,141 @@ func (s *service) discordNotification(event domain.EventsReleasePushed, webhookU
}
client := http.Client{Transport: t, Timeout: 30 * time.Second}
color := map[domain.ReleasePushStatus]int{
domain.ReleasePushStatusApproved: 5814783,
domain.ReleasePushStatusRejected: 5814783,
domain.ReleasePushStatusErr: 14026000,
}
m := DiscordMessage{
Content: nil,
Embeds: []DiscordEmbeds{
{
Title: event.ReleaseName,
Description: "New release!",
Color: color[event.Status],
Fields: []DiscordEmbedsFields{
{
Name: "Status",
Value: event.Status.String(),
Inline: true,
},
{
Name: "Indexer",
Value: event.Indexer,
Inline: true,
},
{
Name: "Filter",
Value: event.Filter,
Inline: true,
},
{
Name: "Action",
Value: event.Action,
Inline: true,
},
{
Name: "Action type",
Value: string(event.ActionType),
Inline: true,
},
//{
// Name: "Action client",
// Value: event.ActionClient,
// Inline: true,
//},
},
Timestamp: time.Now(),
},
},
Username: "brr",
}
if event.ActionClient == "" {
rej := DiscordEmbedsFields{
Name: "Action client",
Value: "n/a",
Inline: true,
}
m.Embeds[0].Fields = append(m.Embeds[0].Fields, rej)
} else {
rej := DiscordEmbedsFields{
Name: "Action client",
Value: event.ActionClient,
Inline: true,
}
m.Embeds[0].Fields = append(m.Embeds[0].Fields, rej)
}
if len(event.Rejections) > 0 {
rej := DiscordEmbedsFields{
Name: "Reasons",
Value: fmt.Sprintf("```\n%v\n```", strings.Join(event.Rejections, " ,")),
Inline: false,
}
m.Embeds[0].Fields = append(m.Embeds[0].Fields, rej)
}
jsonData, err := json.Marshal(m)
if err != nil {
s.log.Error().Err(err).Msgf("discord client could not marshal data: %v", m)
return
}
req, err := http.NewRequest(http.MethodPost, webhookURL, bytes.NewBuffer(jsonData))
if err != nil {
s.log.Error().Err(err).Msgf("discord client request error: %v", event.ReleaseName)
return
}
req.Header.Set("Content-Type", "application/json")
//req.Header.Set("User-Agent", "autobrr")
res, err := client.Do(req)
if err != nil {
s.log.Error().Err(err).Msgf("discord client request error: %v", event.ReleaseName)
return
a.log.Error().Err(err).Msgf("discord client request error: %v", event)
return err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
a.log.Error().Err(err).Msgf("discord client request error: %v", event)
return err
}
defer res.Body.Close()
s.log.Debug().Msg("notification successfully sent to discord")
a.log.Trace().Msgf("discord status: %v response: %v", res.StatusCode, string(body))
return
if res.StatusCode != http.StatusNoContent {
a.log.Error().Err(err).Msgf("discord client request error: %v", string(body))
return fmt.Errorf("err: %v", string(body))
}
a.log.Debug().Msg("notification successfully sent to discord")
return nil
}
func (a *discordSender) CanSend(event domain.NotificationEvent) bool {
if a.isEnabled() && a.isEnabledEvent(event) {
return true
}
return false
}
func (a *discordSender) isEnabled() bool {
if a.Settings.Enabled && a.Settings.Webhook != "" {
return true
}
return false
}
func (a *discordSender) isEnabledEvent(event domain.NotificationEvent) bool {
for _, e := range a.Settings.Events {
if e == string(event) {
return true
}
}
return false
}
func (a *discordSender) buildEmbed(event domain.NotificationEvent, payload domain.NotificationPayload) DiscordEmbeds {
color := LIGHT_BLUE
switch event {
case domain.NotificationEventPushApproved:
color = GREEN
case domain.NotificationEventPushRejected:
color = GRAY
case domain.NotificationEventPushError:
color = RED
case domain.NotificationEventTest:
color = LIGHT_BLUE
}
var fields []DiscordEmbedsFields
if payload.Status != "" {
f := DiscordEmbedsFields{
Name: "Status",
Value: payload.Status.String(),
Inline: true,
}
fields = append(fields, f)
}
if payload.Indexer != "" {
f := DiscordEmbedsFields{
Name: "Indexer",
Value: payload.Indexer,
Inline: true,
}
fields = append(fields, f)
}
if payload.Filter != "" {
f := DiscordEmbedsFields{
Name: "Filter",
Value: payload.Filter,
Inline: true,
}
fields = append(fields, f)
}
if payload.Action != "" {
f := DiscordEmbedsFields{
Name: "Action",
Value: payload.Action,
Inline: true,
}
fields = append(fields, f)
}
if payload.ActionType != "" {
f := DiscordEmbedsFields{
Name: "Action type",
Value: string(payload.ActionType),
Inline: true,
}
fields = append(fields, f)
}
if payload.ActionClient != "" {
f := DiscordEmbedsFields{
Name: "Action client",
Value: payload.ActionClient,
Inline: true,
}
fields = append(fields, f)
}
if len(payload.Rejections) > 0 {
f := DiscordEmbedsFields{
Name: "Reasons",
Value: fmt.Sprintf("```\n%v\n```", strings.Join(payload.Rejections, ", ")),
Inline: false,
}
fields = append(fields, f)
}
embed := DiscordEmbeds{
Title: payload.ReleaseName,
Description: "New release!",
Color: int(color),
Fields: fields,
Timestamp: time.Now(),
}
if payload.Subject != "" && payload.Message != "" {
embed.Title = payload.Subject
embed.Description = payload.Message
}
return embed
}

View file

@ -2,13 +2,9 @@ package notification
import (
"context"
"fmt"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/logger"
"github.com/containrrr/shoutrrr"
t "github.com/containrrr/shoutrrr/pkg/types"
)
type Service interface {
@ -17,20 +13,26 @@ type Service interface {
Store(ctx context.Context, n domain.Notification) (*domain.Notification, error)
Update(ctx context.Context, n domain.Notification) (*domain.Notification, error)
Delete(ctx context.Context, id int) error
Send(event domain.NotificationEvent, msg string) error
SendEvent(event domain.EventsReleasePushed) error
Send(event domain.NotificationEvent, payload domain.NotificationPayload) error
Test(ctx context.Context, notification domain.Notification) error
}
type service struct {
log logger.Logger
repo domain.NotificationRepo
log logger.Logger
repo domain.NotificationRepo
senders []domain.NotificationSender
}
func NewService(log logger.Logger, repo domain.NotificationRepo) Service {
return &service{
log: log,
repo: repo,
s := &service{
log: log,
repo: repo,
senders: []domain.NotificationSender{},
}
s.registerSenders()
return s
}
func (s *service) Find(ctx context.Context, params domain.NotificationQueryParams) ([]domain.Notification, int, error) {
@ -42,109 +44,96 @@ func (s *service) FindByID(ctx context.Context, id int) (*domain.Notification, e
}
func (s *service) Store(ctx context.Context, n domain.Notification) (*domain.Notification, error) {
return s.repo.Store(ctx, n)
_, err := s.repo.Store(ctx, n)
if err != nil {
return nil, err
}
// reset senders
s.senders = []domain.NotificationSender{}
// re register senders
s.registerSenders()
return nil, nil
}
func (s *service) Update(ctx context.Context, n domain.Notification) (*domain.Notification, error) {
return s.repo.Update(ctx, n)
_, err := s.repo.Update(ctx, n)
if err != nil {
return nil, err
}
// reset senders
s.senders = []domain.NotificationSender{}
// re register senders
s.registerSenders()
return nil, nil
}
func (s *service) Delete(ctx context.Context, id int) error {
return s.repo.Delete(ctx, id)
err := s.repo.Delete(ctx, id)
if err != nil {
return err
}
// reset senders
s.senders = []domain.NotificationSender{}
// re register senders
s.registerSenders()
return nil
}
func (s *service) registerSenders() {
senders, err := s.repo.List(context.Background())
if err != nil {
return
}
for _, n := range senders {
if n.Enabled {
switch n.Type {
case domain.NotificationTypeDiscord:
s.senders = append(s.senders, NewDiscordSender(s.log, n))
case domain.NotificationTypeTelegram:
s.senders = append(s.senders, NewTelegramSender(s.log, n))
}
}
}
return
}
// Send notifications
func (s *service) Send(event domain.NotificationEvent, msg string) error {
// find notifications for type X
func (s *service) Send(event domain.NotificationEvent, payload domain.NotificationPayload) error {
s.log.Debug().Msgf("sending notification for %v", string(event))
notifications, err := s.repo.List(context.Background())
if err != nil {
return err
}
var urls []string
for _, n := range notifications {
if !n.Enabled {
continue
}
switch n.Type {
case domain.NotificationTypeDiscord:
urls = append(urls, fmt.Sprintf("discord://%v@%v", n.Token, n.Webhook))
default:
return nil
for _, sender := range s.senders {
// check if sender is active and have notification types
if sender.CanSend(event) {
sender.Send(event, payload)
}
}
if len(urls) == 0 {
return nil
}
sender, err := shoutrrr.CreateSender(urls...)
if err != nil {
return err
}
p := t.Params{"title": "TEST"}
items := []t.MessageItem{
{
Text: "text hello",
Fields: []t.Field{
{
Key: "eventt",
Value: "push?",
},
},
},
}
//items = append(items, t.MessageItem{
// Text: "text hello",
// Fields: []t.Field{
// {
// Key: "eventt",
// Value: "push?",
// },
// },
//})
sender.SendItems(items, p)
return nil
}
func (s *service) SendEvent(event domain.EventsReleasePushed) error {
notifications, err := s.repo.List(context.Background())
if err != nil {
return err
func (s *service) Test(ctx context.Context, notification domain.Notification) error {
var agent domain.NotificationSender
switch notification.Type {
case domain.NotificationTypeDiscord:
agent = NewDiscordSender(s.log, notification)
case domain.NotificationTypeTelegram:
agent = NewTelegramSender(s.log, notification)
}
return s.send(notifications, event)
}
func (s *service) send(notifications []domain.Notification, event domain.EventsReleasePushed) error {
// find notifications for type X
for _, n := range notifications {
if !n.Enabled {
continue
}
if n.Events == nil {
continue
}
for _, evt := range n.Events {
if evt == string(event.Status) {
switch n.Type {
case domain.NotificationTypeDiscord:
go s.discordNotification(event, n.Webhook)
default:
return nil
}
}
}
}
return nil
return agent.Send(domain.NotificationEventTest, domain.NotificationPayload{
Subject: "Test Notification",
Message: "autobrr goes brr!!",
})
}

View file

@ -0,0 +1,147 @@
package notification
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"html"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/internal/logger"
)
type TelegramMessage struct {
ChatID string `json:"chat_id"`
Text string `json:"text"`
ParseMode string `json:"parse_mode"`
}
type telegramSender struct {
log logger.Logger
Settings domain.Notification
}
func NewTelegramSender(log logger.Logger, settings domain.Notification) domain.NotificationSender {
return &telegramSender{
log: log,
Settings: settings,
}
}
func (s *telegramSender) Send(event domain.NotificationEvent, payload domain.NotificationPayload) error {
m := TelegramMessage{
ChatID: s.Settings.Channel,
Text: s.buildMessage(event, payload),
ParseMode: "HTML",
//ParseMode: "MarkdownV2",
}
jsonData, err := json.Marshal(m)
if err != nil {
s.log.Error().Err(err).Msgf("telegram client could not marshal data: %v", m)
return err
}
url := fmt.Sprintf("https://api.telegram.org/bot%v/sendMessage", s.Settings.Token)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData))
if err != nil {
s.log.Error().Err(err).Msgf("telegram client request error: %v", event)
return err
}
req.Header.Set("Content-Type", "application/json")
//req.Header.Set("User-Agent", "autobrr")
t := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := http.Client{Transport: t, Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil {
s.log.Error().Err(err).Msgf("telegram client request error: %v", event)
return err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
s.log.Error().Err(err).Msgf("telegram client request error: %v", event)
return err
}
defer res.Body.Close()
s.log.Trace().Msgf("telegram status: %v response: %v", res.StatusCode, string(body))
if res.StatusCode != http.StatusOK {
s.log.Error().Err(err).Msgf("telegram client request error: %v", string(body))
return fmt.Errorf("err: %v", string(body))
}
s.log.Debug().Msg("notification successfully sent to telegram")
return nil
}
func (s *telegramSender) CanSend(event domain.NotificationEvent) bool {
if s.isEnabled() && s.isEnabledEvent(event) {
return true
}
return false
}
func (s *telegramSender) isEnabled() bool {
if s.Settings.Enabled && s.Settings.Token != "" && s.Settings.Channel != "" {
return true
}
return false
}
func (s *telegramSender) isEnabledEvent(event domain.NotificationEvent) bool {
for _, e := range s.Settings.Events {
if e == string(event) {
return true
}
}
return false
}
func (s *telegramSender) buildMessage(event domain.NotificationEvent, payload domain.NotificationPayload) string {
msg := ""
if payload.Subject != "" && payload.Message != "" {
msg += fmt.Sprintf("%v\n<b>%v</b>", payload.Subject, html.EscapeString(payload.Message))
}
if payload.ReleaseName != "" {
msg += fmt.Sprintf("\n<b>New release:</b> %v", html.EscapeString(payload.ReleaseName))
}
if payload.Status != "" {
msg += fmt.Sprintf("\n<b>Status:</b> %v", payload.Status.String())
}
if payload.Indexer != "" {
msg += fmt.Sprintf("\n<b>Indexer:</b> %v", payload.Indexer)
}
if payload.Filter != "" {
msg += fmt.Sprintf("\n<b>Filter:</b> %v", html.EscapeString(payload.Filter))
}
if payload.Action != "" {
action := fmt.Sprintf("\n<b>Action:</b> %v <b>Type:</b> %v", html.EscapeString(payload.Action), payload.ActionType)
if payload.ActionClient != "" {
action += fmt.Sprintf(" <b>Client:</b> %v", html.EscapeString(payload.ActionClient))
}
msg += action
}
if len(payload.Rejections) > 0 {
msg += fmt.Sprintf("\nRejections: %v", strings.Join(payload.Rejections, ", "))
}
return msg
}