2022-07-16 06:43:48 +02:00
package matrix
import (
"bytes"
2022-10-20 21:48:51 +02:00
"encoding/json"
2022-07-16 06:43:48 +02:00
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"time"
2022-12-06 07:41:09 +01:00
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
2024-05-10 04:56:16 +02:00
"github.com/TwiN/gatus/v5/config/endpoint"
2022-07-16 06:43:48 +02:00
)
// AlertProvider is the configuration necessary for sending an alert using Matrix
type AlertProvider struct {
2024-04-11 02:46:17 +02:00
ProviderConfig ` yaml:",inline" `
2022-07-16 06:43:48 +02:00
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert * alert . Alert ` yaml:"default-alert,omitempty" `
// Overrides is a list of Override that may be prioritized over the default configuration
Overrides [ ] Override ` yaml:"overrides,omitempty" `
}
// Override is a case under which the default integration is overridden
type Override struct {
Group string ` yaml:"group" `
2024-04-11 02:46:17 +02:00
ProviderConfig ` yaml:",inline" `
2022-07-16 06:43:48 +02:00
}
2024-04-11 02:46:17 +02:00
const defaultServerURL = "https://matrix-client.matrix.org"
2022-07-16 06:43:48 +02:00
2024-04-11 02:46:17 +02:00
type ProviderConfig struct {
2022-07-19 19:15:41 +02:00
// ServerURL is the custom homeserver to use (optional)
ServerURL string ` yaml:"server-url" `
// AccessToken is the bot user's access token to send messages
AccessToken string ` yaml:"access-token" `
// InternalRoomID is the room that the bot user has permissions to send messages to
2022-07-16 06:43:48 +02:00
InternalRoomID string ` yaml:"internal-room-id" `
}
// IsValid returns whether the provider's configuration is valid
func ( provider * AlertProvider ) IsValid ( ) bool {
registeredGroups := make ( map [ string ] bool )
if provider . Overrides != nil {
for _ , override := range provider . Overrides {
if isAlreadyRegistered := registeredGroups [ override . Group ] ; isAlreadyRegistered || override . Group == "" || len ( override . AccessToken ) == 0 || len ( override . InternalRoomID ) == 0 {
return false
}
registeredGroups [ override . Group ] = true
}
}
return len ( provider . AccessToken ) > 0 && len ( provider . InternalRoomID ) > 0
}
// Send an alert using the provider
2024-05-10 04:56:16 +02:00
func ( provider * AlertProvider ) Send ( ep * endpoint . Endpoint , alert * alert . Alert , result * endpoint . Result , resolved bool ) error {
buffer := bytes . NewBuffer ( provider . buildRequestBody ( ep , alert , result , resolved ) )
config := provider . getConfigForGroup ( ep . Group )
2022-07-19 19:15:41 +02:00
if config . ServerURL == "" {
2024-04-11 02:46:17 +02:00
config . ServerURL = defaultServerURL
2022-07-16 06:43:48 +02:00
}
2022-07-19 19:42:28 +02:00
// The Matrix endpoint requires a unique transaction ID for each event sent
2022-07-16 06:43:48 +02:00
txnId := randStringBytes ( 24 )
request , err := http . NewRequest (
http . MethodPut ,
2022-07-19 19:40:07 +02:00
fmt . Sprintf ( "%s/_matrix/client/v3/rooms/%s/send/m.room.message/%s?access_token=%s" ,
2022-07-19 19:15:41 +02:00
config . ServerURL ,
2022-07-16 06:43:48 +02:00
url . PathEscape ( config . InternalRoomID ) ,
txnId ,
url . QueryEscape ( config . AccessToken ) ,
) ,
buffer ,
)
if err != nil {
return err
}
request . Header . Set ( "Content-Type" , "application/json" )
response , err := client . GetHTTPClient ( nil ) . Do ( request )
if err != nil {
return err
}
2022-10-20 21:16:27 +02:00
defer response . Body . Close ( )
2022-07-16 06:43:48 +02:00
if response . StatusCode > 399 {
body , _ := io . ReadAll ( response . Body )
return fmt . Errorf ( "call to provider alert returned status code %d: %s" , response . StatusCode , string ( body ) )
}
return err
}
2022-10-20 21:48:51 +02:00
type Body struct {
MsgType string ` json:"msgtype" `
Format string ` json:"format" `
Body string ` json:"body" `
FormattedBody string ` json:"formatted_body" `
}
2022-07-16 06:43:48 +02:00
// buildRequestBody builds the request body for the provider
2024-05-10 04:56:16 +02:00
func ( provider * AlertProvider ) buildRequestBody ( ep * endpoint . Endpoint , alert * alert . Alert , result * endpoint . Result , resolved bool ) [ ] byte {
2022-10-20 21:48:51 +02:00
body , _ := json . Marshal ( Body {
MsgType : "m.text" ,
Format : "org.matrix.custom.html" ,
2024-05-10 04:56:16 +02:00
Body : buildPlaintextMessageBody ( ep , alert , result , resolved ) ,
FormattedBody : buildHTMLMessageBody ( ep , alert , result , resolved ) ,
2022-10-20 21:48:51 +02:00
} )
return body
2022-07-16 06:43:48 +02:00
}
// buildPlaintextMessageBody builds the message body in plaintext to include in request
2024-05-10 04:56:16 +02:00
func buildPlaintextMessageBody ( ep * endpoint . Endpoint , alert * alert . Alert , result * endpoint . Result , resolved bool ) string {
2024-04-11 02:46:17 +02:00
var message string
2022-07-16 06:43:48 +02:00
if resolved {
2024-05-10 04:56:16 +02:00
message = fmt . Sprintf ( "An alert for `%s` has been resolved after passing successfully %d time(s) in a row" , ep . DisplayName ( ) , alert . SuccessThreshold )
2022-07-16 06:43:48 +02:00
} else {
2024-05-10 04:56:16 +02:00
message = fmt . Sprintf ( "An alert for `%s` has been triggered due to having failed %d time(s) in a row" , ep . DisplayName ( ) , alert . FailureThreshold )
2022-07-16 06:43:48 +02:00
}
2024-04-11 02:46:17 +02:00
var formattedConditionResults string
2022-07-16 06:43:48 +02:00
for _ , conditionResult := range result . ConditionResults {
var prefix string
if conditionResult . Success {
prefix = "✓"
} else {
prefix = "✕"
}
2024-04-11 02:46:17 +02:00
formattedConditionResults += fmt . Sprintf ( "\n%s - %s" , prefix , conditionResult . Condition )
2022-07-16 06:43:48 +02:00
}
var description string
if alertDescription := alert . GetDescription ( ) ; len ( alertDescription ) > 0 {
2022-10-20 21:48:51 +02:00
description = "\n" + alertDescription
2022-07-16 06:43:48 +02:00
}
2024-04-11 02:46:17 +02:00
return fmt . Sprintf ( "%s%s\n%s" , message , description , formattedConditionResults )
2022-07-16 06:43:48 +02:00
}
// buildHTMLMessageBody builds the message body in HTML to include in request
2024-05-10 04:56:16 +02:00
func buildHTMLMessageBody ( ep * endpoint . Endpoint , alert * alert . Alert , result * endpoint . Result , resolved bool ) string {
2024-04-11 02:46:17 +02:00
var message string
2022-07-16 06:43:48 +02:00
if resolved {
2024-05-10 04:56:16 +02:00
message = fmt . Sprintf ( "An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row" , ep . DisplayName ( ) , alert . SuccessThreshold )
2022-07-16 06:43:48 +02:00
} else {
2024-05-10 04:56:16 +02:00
message = fmt . Sprintf ( "An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row" , ep . DisplayName ( ) , alert . FailureThreshold )
2022-07-16 06:43:48 +02:00
}
2024-04-11 02:46:17 +02:00
var formattedConditionResults string
if len ( result . ConditionResults ) > 0 {
formattedConditionResults = "\n<h5>Condition results</h5><ul>"
for _ , conditionResult := range result . ConditionResults {
var prefix string
if conditionResult . Success {
prefix = "✅"
} else {
prefix = "❌"
}
formattedConditionResults += fmt . Sprintf ( "<li>%s - <code>%s</code></li>" , prefix , conditionResult . Condition )
2022-07-16 06:43:48 +02:00
}
2024-04-11 02:46:17 +02:00
formattedConditionResults += "</ul>"
2022-07-16 06:43:48 +02:00
}
var description string
if alertDescription := alert . GetDescription ( ) ; len ( alertDescription ) > 0 {
2022-10-20 21:48:51 +02:00
description = fmt . Sprintf ( "\n<blockquote>%s</blockquote>" , alertDescription )
2022-07-16 06:43:48 +02:00
}
2024-04-11 02:46:17 +02:00
return fmt . Sprintf ( "<h3>%s</h3>%s%s" , message , description , formattedConditionResults )
2022-07-16 06:43:48 +02:00
}
// getConfigForGroup returns the appropriate configuration for a given group
2024-04-11 02:46:17 +02:00
func ( provider * AlertProvider ) getConfigForGroup ( group string ) ProviderConfig {
2022-07-16 06:43:48 +02:00
if provider . Overrides != nil {
for _ , override := range provider . Overrides {
if group == override . Group {
2024-04-11 02:46:17 +02:00
return override . ProviderConfig
2022-07-16 06:43:48 +02:00
}
}
}
2024-04-11 02:46:17 +02:00
return provider . ProviderConfig
2022-07-16 06:43:48 +02:00
}
func randStringBytes ( n int ) string {
2022-07-19 19:42:28 +02:00
// All the compatible characters to use in a transaction ID
const availableCharacterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
2022-07-16 06:43:48 +02:00
b := make ( [ ] byte , n )
rand . Seed ( time . Now ( ) . UnixNano ( ) )
for i := range b {
2022-07-19 19:42:28 +02:00
b [ i ] = availableCharacterBytes [ rand . Intn ( len ( availableCharacterBytes ) ) ]
2022-07-16 06:43:48 +02:00
}
return string ( b )
}
// GetDefaultAlert returns the provider's default alert configuration
2024-02-08 02:09:45 +01:00
func ( provider * AlertProvider ) GetDefaultAlert ( ) * alert . Alert {
2022-07-16 06:43:48 +02:00
return provider . DefaultAlert
}