feat(notification/telegram): take back telegram notification after being reverted in #19088
This reverts commit f67c3a14e0
.
pull/19135/head
parent
d0a42098a0
commit
6d086f7495
|
@ -11169,6 +11169,7 @@ components:
|
|||
- $ref: "#/components/schemas/SMTPNotificationRule"
|
||||
- $ref: "#/components/schemas/PagerDutyNotificationRule"
|
||||
- $ref: "#/components/schemas/HTTPNotificationRule"
|
||||
- $ref: "#/components/schemas/TelegramNotificationRule"
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
|
@ -11176,6 +11177,7 @@ components:
|
|||
smtp: "#/components/schemas/SMTPNotificationRule"
|
||||
pagerduty: "#/components/schemas/PagerDutyNotificationRule"
|
||||
http: "#/components/schemas/HTTPNotificationRule"
|
||||
telegram: "#/components/schemas/TelegramNotificationRule"
|
||||
NotificationRule:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationRuleDiscriminator"
|
||||
|
@ -11377,6 +11379,31 @@ components:
|
|||
enum: [pagerduty]
|
||||
messageTemplate:
|
||||
type: string
|
||||
TelegramNotificationRule:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationRuleBase"
|
||||
- $ref: "#/components/schemas/TelegramNotificationRuleBase"
|
||||
TelegramNotificationRuleBase:
|
||||
type: object
|
||||
required: [type, messageTemplate, channel]
|
||||
properties:
|
||||
type:
|
||||
description: The discriminator between other types of notification rules is "telegram".
|
||||
type: string
|
||||
enum: [telegram]
|
||||
messageTemplate:
|
||||
description: The message template as a flux interpolated string.
|
||||
type: string
|
||||
parseMode:
|
||||
description: Parse mode of the message text per https://core.telegram.org/bots/api#formatting-options . Defaults to "MarkdownV2" .
|
||||
type: string
|
||||
enum:
|
||||
- MarkdownV2
|
||||
- HTML
|
||||
- Markdown
|
||||
disableWebPagePreview:
|
||||
description: Disables preview of web links in the sent messages when "true". Defaults to "false" .
|
||||
type: boolean
|
||||
NotificationEndpointUpdate:
|
||||
type: object
|
||||
|
||||
|
@ -11395,12 +11422,14 @@ components:
|
|||
- $ref: "#/components/schemas/SlackNotificationEndpoint"
|
||||
- $ref: "#/components/schemas/PagerDutyNotificationEndpoint"
|
||||
- $ref: "#/components/schemas/HTTPNotificationEndpoint"
|
||||
- $ref: "#/components/schemas/TelegramNotificationEndpoint"
|
||||
discriminator:
|
||||
propertyName: type
|
||||
mapping:
|
||||
slack: "#/components/schemas/SlackNotificationEndpoint"
|
||||
pagerduty: "#/components/schemas/PagerDutyNotificationEndpoint"
|
||||
http: "#/components/schemas/HTTPNotificationEndpoint"
|
||||
telegram: "#/components/schemas/TelegramNotificationEndpoint"
|
||||
NotificationEndpoint:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationEndpointDiscrimator"
|
||||
|
@ -11519,9 +11548,22 @@ components:
|
|||
description: Customized headers.
|
||||
additionalProperties:
|
||||
type: string
|
||||
TelegramNotificationEndpoint:
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NotificationEndpointBase"
|
||||
- type: object
|
||||
required: [token, channel]
|
||||
properties:
|
||||
token:
|
||||
description: Specifies the Telegram bot token. See https://core.telegram.org/bots#creating-a-new-bot .
|
||||
type: string
|
||||
channel:
|
||||
description: ID of the telegram channel, a chat_id in https://core.telegram.org/bots/api#sendmessage .
|
||||
type: string
|
||||
NotificationEndpointType:
|
||||
type: string
|
||||
enum: ["slack", "pagerduty", "http"]
|
||||
enum: ["slack", "pagerduty", "http", "telegram"]
|
||||
DBRP:
|
||||
required:
|
||||
- orgID
|
||||
|
|
|
@ -12,12 +12,14 @@ const (
|
|||
SlackType = "slack"
|
||||
PagerDutyType = "pagerduty"
|
||||
HTTPType = "http"
|
||||
TelegramType = "telegram"
|
||||
)
|
||||
|
||||
var typeToEndpoint = map[string]func() influxdb.NotificationEndpoint{
|
||||
SlackType: func() influxdb.NotificationEndpoint { return &Slack{} },
|
||||
PagerDutyType: func() influxdb.NotificationEndpoint { return &PagerDuty{} },
|
||||
HTTPType: func() influxdb.NotificationEndpoint { return &HTTP{} },
|
||||
TelegramType: func() influxdb.NotificationEndpoint { return &Telegram{} },
|
||||
}
|
||||
|
||||
// UnmarshalJSON will convert the bytes to notification endpoint.
|
||||
|
|
|
@ -55,7 +55,7 @@ func TestValidEndpoint(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
name: "empty name PagerDuty",
|
||||
src: &endpoint.PagerDuty{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
|
@ -70,6 +70,22 @@ func TestValidEndpoint(t *testing.T) {
|
|||
Msg: "Notification Endpoint Name can't be empty",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty name Telegram",
|
||||
src: &endpoint.Telegram{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
},
|
||||
Token: influxdb.SecretField{Key: id1 + "-token"},
|
||||
Channel: "-1001406363649",
|
||||
},
|
||||
err: &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "Notification Endpoint Name can't be empty",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty slack url",
|
||||
src: &endpoint.Slack{
|
||||
|
@ -136,6 +152,36 @@ func TestValidEndpoint(t *testing.T) {
|
|||
Msg: "invalid http username/password for basic auth",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty telegram token",
|
||||
src: &endpoint.Telegram{
|
||||
Base: goodBase,
|
||||
},
|
||||
err: &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "empty telegram bot token",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty telegram channel",
|
||||
src: &endpoint.Telegram{
|
||||
Base: goodBase,
|
||||
Token: influxdb.SecretField{Key: id1 + "-token"},
|
||||
},
|
||||
err: &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "empty telegram channel",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid telegram token",
|
||||
src: &endpoint.Telegram{
|
||||
Base: goodBase,
|
||||
Token: influxdb.SecretField{Key: id1 + "-token"},
|
||||
Channel: "-1001406363649",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
@ -226,6 +272,22 @@ func TestJSON(t *testing.T) {
|
|||
Password: influxdb.SecretField{Key: "password-key"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple Telegram",
|
||||
src: &endpoint.Telegram{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
Name: "nameTelegram",
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
Token: influxdb.SecretField{Key: "token-key-1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
b, err := json.Marshal(c.src)
|
||||
|
@ -365,6 +427,40 @@ func TestBackFill(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple Telegram",
|
||||
src: &endpoint.Telegram{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
Name: "name1",
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
Token: influxdb.SecretField{
|
||||
Value: strPtr("token-value"),
|
||||
},
|
||||
},
|
||||
target: &endpoint.Telegram{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
Name: "name1",
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
Token: influxdb.SecretField{
|
||||
Key: id1 + "-token",
|
||||
Value: strPtr("token-value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c.src.BackfillSecretKeys()
|
||||
|
@ -374,6 +470,133 @@ func TestBackFill(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSecretFields(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
src influxdb.NotificationEndpoint
|
||||
secrets []influxdb.SecretField
|
||||
}{
|
||||
{
|
||||
name: "simple Slack",
|
||||
src: &endpoint.Slack{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
Name: "name1",
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
URL: "https://slack.com/api/chat.postMessage",
|
||||
Token: influxdb.SecretField{
|
||||
Key: id1 + "-token",
|
||||
Value: strPtr("token-value"),
|
||||
},
|
||||
},
|
||||
secrets: []influxdb.SecretField{
|
||||
{
|
||||
Key: id1 + "-token",
|
||||
Value: strPtr("token-value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple pagerduty",
|
||||
src: &endpoint.PagerDuty{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
Name: "name1",
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
ClientURL: "https://events.pagerduty.com/v2/enqueue",
|
||||
RoutingKey: influxdb.SecretField{
|
||||
Key: id1 + "-routing-key",
|
||||
Value: strPtr("routing-key-value"),
|
||||
},
|
||||
},
|
||||
secrets: []influxdb.SecretField{
|
||||
{
|
||||
Key: id1 + "-routing-key",
|
||||
Value: strPtr("routing-key-value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http with user and password",
|
||||
src: &endpoint.HTTP{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
Name: "name1",
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
AuthMethod: "basic",
|
||||
URL: "http://example.com",
|
||||
Username: influxdb.SecretField{
|
||||
Key: id1 + "-username",
|
||||
Value: strPtr("user1"),
|
||||
},
|
||||
Password: influxdb.SecretField{
|
||||
Key: id1 + "-password",
|
||||
Value: strPtr("password1"),
|
||||
},
|
||||
},
|
||||
secrets: []influxdb.SecretField{
|
||||
{
|
||||
Key: id1 + "-username",
|
||||
Value: strPtr("user1"),
|
||||
},
|
||||
{
|
||||
Key: id1 + "-password",
|
||||
Value: strPtr("password1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple Telegram",
|
||||
src: &endpoint.Telegram{
|
||||
Base: endpoint.Base{
|
||||
ID: influxTesting.MustIDBase16Ptr(id1),
|
||||
Name: "name1",
|
||||
OrgID: influxTesting.MustIDBase16Ptr(id3),
|
||||
Status: influxdb.Active,
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
Token: influxdb.SecretField{
|
||||
Key: id1 + "-token",
|
||||
Value: strPtr("token-value"),
|
||||
},
|
||||
},
|
||||
secrets: []influxdb.SecretField{
|
||||
{
|
||||
Key: id1 + "-token",
|
||||
Value: strPtr("token-value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
secretFields := c.src.SecretFields()
|
||||
if diff := cmp.Diff(c.secrets, secretFields); diff != "" {
|
||||
t.Errorf("failed %s, NotificationEndpoint are different -got/+want\ndiff %s", c.name, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
ss := new(string)
|
||||
*ss = s
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package endpoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
)
|
||||
|
||||
var _ influxdb.NotificationEndpoint = &Telegram{}
|
||||
|
||||
const telegramTokenSuffix = "-token"
|
||||
|
||||
// Telegram is the notification endpoint config of telegram.
|
||||
type Telegram struct {
|
||||
Base
|
||||
// Token is the telegram bot token, see https://core.telegram.org/bots#creating-a-new-bot
|
||||
Token influxdb.SecretField `json:"token"`
|
||||
// Channel is an ID of the telegram channel, see https://core.telegram.org/bots/api#sendmessage
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
|
||||
// BackfillSecretKeys fill back the secret field key during the unmarshalling
|
||||
// if value of that secret field is not nil.
|
||||
func (s *Telegram) BackfillSecretKeys() {
|
||||
if s.Token.Key == "" && s.Token.Value != nil {
|
||||
s.Token.Key = s.idStr() + telegramTokenSuffix
|
||||
}
|
||||
}
|
||||
|
||||
// SecretFields return available secret fields.
|
||||
func (s Telegram) SecretFields() []influxdb.SecretField {
|
||||
arr := []influxdb.SecretField{}
|
||||
if s.Token.Key != "" {
|
||||
arr = append(arr, s.Token)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// Valid returns error if some configuration is invalid
|
||||
func (s Telegram) Valid() error {
|
||||
if err := s.Base.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Token.Key == "" {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "empty telegram bot token",
|
||||
}
|
||||
}
|
||||
if s.Channel == "" {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "empty telegram channel",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implement json.Marshaler interface.
|
||||
func (s Telegram) MarshalJSON() ([]byte, error) {
|
||||
type telegramAlias Telegram
|
||||
return json.Marshal(
|
||||
struct {
|
||||
telegramAlias
|
||||
Type string `json:"type"`
|
||||
}{
|
||||
telegramAlias: telegramAlias(s),
|
||||
Type: s.Type(),
|
||||
})
|
||||
}
|
||||
|
||||
// Type returns the type.
|
||||
func (s Telegram) Type() string {
|
||||
return TelegramType
|
||||
}
|
|
@ -16,6 +16,7 @@ var typeToRule = map[string](func() influxdb.NotificationRule){
|
|||
"slack": func() influxdb.NotificationRule { return &Slack{} },
|
||||
"pagerduty": func() influxdb.NotificationRule { return &PagerDuty{} },
|
||||
"http": func() influxdb.NotificationRule { return &HTTP{} },
|
||||
"telegram": func() influxdb.NotificationRule { return &Telegram{} },
|
||||
}
|
||||
|
||||
// UnmarshalJSON will convert
|
||||
|
|
|
@ -321,6 +321,41 @@ func TestJSON(t *testing.T) {
|
|||
MessageTemplate: "msg1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple telegram",
|
||||
src: &rule.Telegram{
|
||||
Base: rule.Base{
|
||||
ID: influxTesting.MustIDBase16(id1),
|
||||
OwnerID: influxTesting.MustIDBase16(id2),
|
||||
Name: "name1",
|
||||
OrgID: influxTesting.MustIDBase16(id3),
|
||||
RunbookLink: "runbooklink1",
|
||||
SleepUntil: &time3,
|
||||
Every: mustDuration("1h"),
|
||||
TagRules: []notification.TagRule{
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "k1",
|
||||
Value: "v1",
|
||||
},
|
||||
Operator: influxdb.NotEqual,
|
||||
},
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "k2",
|
||||
Value: "v2",
|
||||
},
|
||||
Operator: influxdb.RegexEqual,
|
||||
},
|
||||
},
|
||||
CRUDLog: influxdb.CRUDLog{
|
||||
CreatedAt: timeGen1.Now(),
|
||||
UpdatedAt: timeGen2.Now(),
|
||||
},
|
||||
},
|
||||
MessageTemplate: "blah",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
b, err := json.Marshal(c.src)
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
package rule
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdata/flux/ast"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/notification/endpoint"
|
||||
"github.com/influxdata/influxdb/v2/notification/flux"
|
||||
)
|
||||
|
||||
// Telegram is the notification rule config of telegram.
|
||||
type Telegram struct {
|
||||
Base
|
||||
MessageTemplate string `json:"messageTemplate"`
|
||||
ParseMode string `json:"parseMode"`
|
||||
DisableWebPagePreview bool `json:"disableWebPagePreview"`
|
||||
}
|
||||
|
||||
// GenerateFlux generates a flux script for the telegram notification rule.
|
||||
func (s *Telegram) GenerateFlux(e influxdb.NotificationEndpoint) (string, error) {
|
||||
telegramEndpoint, ok := e.(*endpoint.Telegram)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("endpoint provided is a %s, not a Telegram endpoint", e.Type())
|
||||
}
|
||||
p, err := s.GenerateFluxAST(telegramEndpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ast.Format(p), nil
|
||||
}
|
||||
|
||||
// GenerateFluxAST generates a flux AST for the telegram notification rule.
|
||||
func (s *Telegram) GenerateFluxAST(e *endpoint.Telegram) (*ast.Package, error) {
|
||||
f := flux.File(
|
||||
s.Name,
|
||||
flux.Imports("influxdata/influxdb/monitor", "contrib/sranka/telegram", "influxdata/influxdb/secrets", "experimental"),
|
||||
s.generateFluxASTBody(e),
|
||||
)
|
||||
return &ast.Package{Package: "main", Files: []*ast.File{f}}, nil
|
||||
}
|
||||
|
||||
func (s *Telegram) generateFluxASTBody(e *endpoint.Telegram) []ast.Statement {
|
||||
var statements []ast.Statement
|
||||
statements = append(statements, s.generateTaskOption())
|
||||
if e.Token.Key != "" {
|
||||
statements = append(statements, s.generateFluxASTSecrets(e))
|
||||
}
|
||||
statements = append(statements, s.generateFluxASTEndpoint(e))
|
||||
statements = append(statements, s.generateFluxASTNotificationDefinition(e))
|
||||
statements = append(statements, s.generateFluxASTStatuses())
|
||||
statements = append(statements, s.generateLevelChecks()...)
|
||||
statements = append(statements, s.generateFluxASTNotifyPipe(e))
|
||||
|
||||
return statements
|
||||
}
|
||||
|
||||
func (s *Telegram) generateFluxASTSecrets(e *endpoint.Telegram) ast.Statement {
|
||||
call := flux.Call(flux.Member("secrets", "get"), flux.Object(flux.Property("key", flux.String(e.Token.Key))))
|
||||
|
||||
return flux.DefineVariable("telegram_secret", call)
|
||||
}
|
||||
|
||||
func (s *Telegram) generateFluxASTEndpoint(e *endpoint.Telegram) ast.Statement {
|
||||
props := []*ast.Property{}
|
||||
if e.Token.Key != "" {
|
||||
props = append(props, flux.Property("token", flux.Identifier("telegram_secret")))
|
||||
}
|
||||
if s.ParseMode != "" {
|
||||
props = append(props, flux.Property("parseMode", flux.String(s.ParseMode)))
|
||||
}
|
||||
props = append(props, flux.Property("disableWebPagePreview", flux.Bool(s.DisableWebPagePreview)))
|
||||
call := flux.Call(flux.Member("telegram", "endpoint"), flux.Object(props...))
|
||||
|
||||
return flux.DefineVariable("telegram_endpoint", call)
|
||||
}
|
||||
|
||||
func (s *Telegram) generateFluxASTNotifyPipe(e *endpoint.Telegram) ast.Statement {
|
||||
endpointProps := []*ast.Property{}
|
||||
endpointProps = append(endpointProps, flux.Property("channel", flux.String(e.Channel)))
|
||||
endpointProps = append(endpointProps, flux.Property("text", flux.String(s.MessageTemplate)))
|
||||
endpointProps = append(endpointProps, flux.Property("silent", s.generateSilent()))
|
||||
endpointFn := flux.Function(flux.FunctionParams("r"), flux.Object(endpointProps...))
|
||||
|
||||
props := []*ast.Property{}
|
||||
props = append(props, flux.Property("data", flux.Identifier("notification")))
|
||||
props = append(props, flux.Property("endpoint",
|
||||
flux.Call(flux.Identifier("telegram_endpoint"), flux.Object(flux.Property("mapFn", endpointFn)))))
|
||||
|
||||
call := flux.Call(flux.Member("monitor", "notify"), flux.Object(props...))
|
||||
|
||||
return flux.ExpressionStatement(flux.Pipe(flux.Identifier("all_statuses"), call))
|
||||
}
|
||||
|
||||
func (s *Telegram) generateSilent() ast.Expression {
|
||||
level := flux.Member("r", "_level")
|
||||
return flux.If(
|
||||
flux.Equal(level, flux.String("crit")),
|
||||
flux.Bool(true),
|
||||
flux.If(
|
||||
flux.Equal(level, flux.String("warn")),
|
||||
flux.Bool(true),
|
||||
flux.Bool(false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type telegramAlias Telegram
|
||||
|
||||
// MarshalJSON implement json.Marshaler interface.
|
||||
func (s Telegram) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(
|
||||
struct {
|
||||
telegramAlias
|
||||
Type string `json:"type"`
|
||||
}{
|
||||
telegramAlias: telegramAlias(s),
|
||||
Type: s.Type(),
|
||||
})
|
||||
}
|
||||
|
||||
// Valid returns where the config is valid.
|
||||
func (s Telegram) Valid() error {
|
||||
if err := s.Base.valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.MessageTemplate == "" {
|
||||
return &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "Telegram MessageTemplate is invalid",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type returns the type of the rule config.
|
||||
func (s Telegram) Type() string {
|
||||
return "telegram"
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
package rule_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/andreyvit/diff"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/notification"
|
||||
"github.com/influxdata/influxdb/v2/notification/endpoint"
|
||||
"github.com/influxdata/influxdb/v2/notification/rule"
|
||||
influxTesting "github.com/influxdata/influxdb/v2/testing"
|
||||
)
|
||||
|
||||
var _ influxdb.NotificationRule = &rule.Telegram{}
|
||||
|
||||
func TestTelegram_GenerateFlux(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rule *rule.Telegram
|
||||
endpoint influxdb.NotificationEndpoint
|
||||
script string
|
||||
}{
|
||||
{
|
||||
name: "incompatible with endpoint",
|
||||
endpoint: &endpoint.Slack{
|
||||
Base: endpoint.Base{
|
||||
ID: idPtr(3),
|
||||
Name: "foo",
|
||||
},
|
||||
URL: "http://whatever",
|
||||
},
|
||||
rule: &rule.Telegram{
|
||||
MessageTemplate: "blah",
|
||||
Base: rule.Base{
|
||||
ID: 1,
|
||||
EndpointID: 3,
|
||||
Name: "foo",
|
||||
Every: mustDuration("1h"),
|
||||
StatusRules: []notification.StatusRule{
|
||||
{
|
||||
CurrentLevel: notification.Critical,
|
||||
},
|
||||
},
|
||||
TagRules: []notification.TagRule{
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
Operator: influxdb.Equal,
|
||||
},
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "baz",
|
||||
Value: "bang",
|
||||
},
|
||||
Operator: influxdb.Equal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
script: "", //no script generater, because of incompatible endpoint
|
||||
},
|
||||
{
|
||||
name: "notify on crit",
|
||||
endpoint: &endpoint.Telegram{
|
||||
Base: endpoint.Base{
|
||||
ID: idPtr(3),
|
||||
Name: "foo",
|
||||
},
|
||||
Token: influxdb.SecretField{Key: "3-key"},
|
||||
Channel: "-12345",
|
||||
},
|
||||
rule: &rule.Telegram{
|
||||
MessageTemplate: "blah",
|
||||
Base: rule.Base{
|
||||
ID: 1,
|
||||
EndpointID: 3,
|
||||
Name: "foo",
|
||||
Every: mustDuration("1h"),
|
||||
StatusRules: []notification.StatusRule{
|
||||
{
|
||||
CurrentLevel: notification.Critical,
|
||||
},
|
||||
},
|
||||
TagRules: []notification.TagRule{
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
Operator: influxdb.Equal,
|
||||
},
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "baz",
|
||||
Value: "bang",
|
||||
},
|
||||
Operator: influxdb.Equal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
script: `package main
|
||||
// foo
|
||||
import "influxdata/influxdb/monitor"
|
||||
import "contrib/sranka/telegram"
|
||||
import "influxdata/influxdb/secrets"
|
||||
import "experimental"
|
||||
|
||||
option task = {name: "foo", every: 1h}
|
||||
|
||||
telegram_secret = secrets["get"](key: "3-key")
|
||||
telegram_endpoint = telegram["endpoint"](token: telegram_secret, disableWebPagePreview: false)
|
||||
notification = {
|
||||
_notification_rule_id: "0000000000000001",
|
||||
_notification_rule_name: "foo",
|
||||
_notification_endpoint_id: "0000000000000003",
|
||||
_notification_endpoint_name: "foo",
|
||||
}
|
||||
statuses = monitor["from"](start: -2h, fn: (r) =>
|
||||
(r["foo"] == "bar" and r["baz"] == "bang"))
|
||||
crit = statuses
|
||||
|> filter(fn: (r) =>
|
||||
(r["_level"] == "crit"))
|
||||
all_statuses = crit
|
||||
|> filter(fn: (r) =>
|
||||
(r["_time"] > experimental["subDuration"](from: now(), d: 1h)))
|
||||
|
||||
all_statuses
|
||||
|> monitor["notify"](data: notification, endpoint: telegram_endpoint(mapFn: (r) =>
|
||||
({channel: "-12345", text: "blah", silent: if r["_level"] == "crit" then true else if r["_level"] == "warn" then true else false})))`,
|
||||
},
|
||||
{
|
||||
name: "with DisableWebPagePreview and ParseMode",
|
||||
endpoint: &endpoint.Telegram{
|
||||
Base: endpoint.Base{
|
||||
ID: idPtr(3),
|
||||
Name: "foo",
|
||||
},
|
||||
Token: influxdb.SecretField{Key: "3-key"},
|
||||
Channel: "-12345",
|
||||
},
|
||||
rule: &rule.Telegram{
|
||||
MessageTemplate: "blah",
|
||||
DisableWebPagePreview: true,
|
||||
ParseMode: "HTML",
|
||||
Base: rule.Base{
|
||||
ID: 1,
|
||||
EndpointID: 3,
|
||||
Name: "foo",
|
||||
Every: mustDuration("1h"),
|
||||
StatusRules: []notification.StatusRule{
|
||||
{
|
||||
CurrentLevel: notification.Any,
|
||||
},
|
||||
},
|
||||
TagRules: []notification.TagRule{
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "foo",
|
||||
Value: "bar",
|
||||
},
|
||||
Operator: influxdb.Equal,
|
||||
},
|
||||
{
|
||||
Tag: influxdb.Tag{
|
||||
Key: "baz",
|
||||
Value: "bang",
|
||||
},
|
||||
Operator: influxdb.Equal,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
script: `package main
|
||||
// foo
|
||||
import "influxdata/influxdb/monitor"
|
||||
import "contrib/sranka/telegram"
|
||||
import "influxdata/influxdb/secrets"
|
||||
import "experimental"
|
||||
|
||||
option task = {name: "foo", every: 1h}
|
||||
|
||||
telegram_secret = secrets["get"](key: "3-key")
|
||||
telegram_endpoint = telegram["endpoint"](token: telegram_secret, parseMode: "HTML", disableWebPagePreview: true)
|
||||
notification = {
|
||||
_notification_rule_id: "0000000000000001",
|
||||
_notification_rule_name: "foo",
|
||||
_notification_endpoint_id: "0000000000000003",
|
||||
_notification_endpoint_name: "foo",
|
||||
}
|
||||
statuses = monitor["from"](start: -2h, fn: (r) =>
|
||||
(r["foo"] == "bar" and r["baz"] == "bang"))
|
||||
any = statuses
|
||||
|> filter(fn: (r) =>
|
||||
(true))
|
||||
all_statuses = any
|
||||
|> filter(fn: (r) =>
|
||||
(r["_time"] > experimental["subDuration"](from: now(), d: 1h)))
|
||||
|
||||
all_statuses
|
||||
|> monitor["notify"](data: notification, endpoint: telegram_endpoint(mapFn: (r) =>
|
||||
({channel: "-12345", text: "blah", silent: if r["_level"] == "crit" then true else if r["_level"] == "warn" then true else false})))`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
script, err := tt.rule.GenerateFlux(tt.endpoint)
|
||||
if err != nil {
|
||||
if script != "" {
|
||||
t.Errorf("Failed to generate flux: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if got, want := script, tt.script; got != want {
|
||||
t.Errorf("\n\nStrings do not match:\n\n%s", diff.LineDiff(got, want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegram_Valid(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
rule *rule.Telegram
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "valid template",
|
||||
rule: &rule.Telegram{
|
||||
MessageTemplate: "blah",
|
||||
Base: rule.Base{
|
||||
ID: 1,
|
||||
EndpointID: 3,
|
||||
OwnerID: 4,
|
||||
OrgID: 5,
|
||||
Name: "foo",
|
||||
Every: mustDuration("1h"),
|
||||
StatusRules: []notification.StatusRule{
|
||||
{
|
||||
CurrentLevel: notification.Critical,
|
||||
},
|
||||
},
|
||||
TagRules: []notification.TagRule{},
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "missing MessageTemplate",
|
||||
rule: &rule.Telegram{
|
||||
MessageTemplate: "",
|
||||
Base: rule.Base{
|
||||
ID: 1,
|
||||
EndpointID: 3,
|
||||
OwnerID: 4,
|
||||
OrgID: 5,
|
||||
Name: "foo",
|
||||
Every: mustDuration("1h"),
|
||||
StatusRules: []notification.StatusRule{
|
||||
{
|
||||
CurrentLevel: notification.Critical,
|
||||
},
|
||||
},
|
||||
TagRules: []notification.TagRule{},
|
||||
},
|
||||
},
|
||||
err: &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "Telegram MessageTemplate is invalid",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing EndpointID",
|
||||
rule: &rule.Telegram{
|
||||
MessageTemplate: "",
|
||||
Base: rule.Base{
|
||||
ID: 1,
|
||||
// EndpointID: 3,
|
||||
OwnerID: 4,
|
||||
OrgID: 5,
|
||||
Name: "foo",
|
||||
Every: mustDuration("1h"),
|
||||
StatusRules: []notification.StatusRule{
|
||||
{
|
||||
CurrentLevel: notification.Critical,
|
||||
},
|
||||
},
|
||||
TagRules: []notification.TagRule{},
|
||||
},
|
||||
},
|
||||
err: &influxdb.Error{
|
||||
Code: influxdb.EInvalid,
|
||||
Msg: "Notification Rule EndpointID is invalid",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := c.rule.Valid()
|
||||
influxTesting.ErrorsEqual(t, got, c.err)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -57,7 +57,7 @@ const EmptyEndpointList: FC<{searchTerm: string}> = ({searchTerm}) => {
|
|||
<EmptyState.Text>
|
||||
Want to send notifications to Slack,
|
||||
<br />
|
||||
PagerDuty or an HTTP server?
|
||||
PagerDuty, Telegram or an HTTP server?
|
||||
<br />
|
||||
<br />
|
||||
Try creating a <b>Notification Endpoint</b>
|
||||
|
|
|
@ -5,6 +5,7 @@ import React, {FC, ChangeEvent} from 'react'
|
|||
import EndpointOptionsSlack from './EndpointOptionsSlack'
|
||||
import EndpointOptionsPagerDuty from './EndpointOptionsPagerDuty'
|
||||
import EndpointOptionsHTTP from './EndpointOptionsHTTP'
|
||||
import EndpointOptionsTelegram from './EndpointOptionsTelegram'
|
||||
|
||||
// Types
|
||||
import {
|
||||
|
@ -12,6 +13,7 @@ import {
|
|||
SlackNotificationEndpoint,
|
||||
PagerDutyNotificationEndpoint,
|
||||
HTTPNotificationEndpoint,
|
||||
TelegramNotificationEndpoint,
|
||||
} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
|
@ -40,6 +42,16 @@ const EndpointOptions: FC<Props> = ({
|
|||
/>
|
||||
)
|
||||
}
|
||||
case 'telegram': {
|
||||
const {token, channel} = endpoint as TelegramNotificationEndpoint
|
||||
return (
|
||||
<EndpointOptionsTelegram
|
||||
token={token}
|
||||
channel={channel}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'http': {
|
||||
const {
|
||||
url,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Input,
|
||||
InputType,
|
||||
FormElement,
|
||||
Panel,
|
||||
Grid,
|
||||
Columns,
|
||||
} from '@influxdata/clockface'
|
||||
|
||||
interface Props {
|
||||
token: string
|
||||
channel: string
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const EndpointOptionsTelegram: FC<Props> = ({token, channel, onChange}) => {
|
||||
return (
|
||||
<Panel>
|
||||
<Panel.Header>
|
||||
<h4>Telegram Options</h4>
|
||||
</Panel.Header>
|
||||
<Panel.Body>
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column widthXS={Columns.Twelve}>
|
||||
<FormElement label="Bot Token">
|
||||
<Input
|
||||
name="token"
|
||||
value={token}
|
||||
testID="token"
|
||||
onChange={onChange}
|
||||
type={InputType.Password}
|
||||
/>
|
||||
</FormElement>
|
||||
<FormElement label="Chat ID">
|
||||
<Input
|
||||
name="channel"
|
||||
value={channel}
|
||||
testID="channel"
|
||||
onChange={onChange}
|
||||
type={InputType.Text}
|
||||
/>
|
||||
</FormElement>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Panel.Body>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
export default EndpointOptionsTelegram
|
|
@ -59,6 +59,13 @@ export const reducer = (
|
|||
url: DEFAULT_ENDPOINT_URLS.slack,
|
||||
token: '',
|
||||
}
|
||||
case 'telegram':
|
||||
return {
|
||||
...baseProps,
|
||||
type: 'telegram',
|
||||
token: '',
|
||||
channel: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
|
|
|
@ -32,6 +32,7 @@ const types: EndpointType[] = [
|
|||
{name: 'HTTP', type: 'http', id: 'http'},
|
||||
{name: 'Slack', type: 'slack', id: 'slack'},
|
||||
{name: 'Pagerduty', type: 'pagerduty', id: 'pagerduty'},
|
||||
{name: 'Telegram', type: 'telegram', id: 'telegram'},
|
||||
]
|
||||
|
||||
const EndpointTypeDropdown: FC<Props> = ({
|
||||
|
|
|
@ -33,7 +33,7 @@ const EndpointsColumn: FC<Props> = ({history, match, endpoints, tabIndex}) => {
|
|||
<br />
|
||||
to a third party service that can receive notifications
|
||||
<br />
|
||||
like Slack, PagerDuty, or an HTTP server
|
||||
like Slack, PagerDuty, Telegram, or an HTTP server
|
||||
<br />
|
||||
<br />
|
||||
<a
|
||||
|
|
|
@ -5,6 +5,7 @@ import React, {FC} from 'react'
|
|||
import SlackMessage from './SlackMessage'
|
||||
import SMTPMessage from './SMTPMessage'
|
||||
import PagerDutyMessage from './PagerDutyMessage'
|
||||
import TelegramMessage from './TelegramMessage'
|
||||
|
||||
// Utils
|
||||
import {useRuleDispatch} from './RuleOverlayProvider'
|
||||
|
@ -61,6 +62,15 @@ const RuleMessageContents: FC<Props> = ({rule}) => {
|
|||
/>
|
||||
)
|
||||
}
|
||||
case 'telegram': {
|
||||
const {messageTemplate} = rule
|
||||
return (
|
||||
<TelegramMessage
|
||||
messageTemplate={messageTemplate}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'http': {
|
||||
return <></>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Form, TextArea} from '@influxdata/clockface'
|
||||
import {TelegramNotificationRuleBase} from 'src/types/alerting'
|
||||
|
||||
interface EventHandlers {
|
||||
onChange: (e: ChangeEvent) => void
|
||||
}
|
||||
type Props = Omit<TelegramNotificationRuleBase, 'type'> & EventHandlers
|
||||
|
||||
const TelegramMessage: FC<Props> = ({messageTemplate, onChange}) => {
|
||||
return (
|
||||
<Form.Element label="Message Template">
|
||||
<TextArea
|
||||
name="messageTemplate"
|
||||
testID="slack-message-template--textarea"
|
||||
value={messageTemplate}
|
||||
onChange={onChange}
|
||||
rows={3}
|
||||
/>
|
||||
</Form.Element>
|
||||
/*
|
||||
// keep it simple, the following elements are possible, but too advanced
|
||||
<Form.Element label="Parse Mode">
|
||||
<Input value={parseMode} name="parseMode" onChange={onChange} />
|
||||
</Form.Element>
|
||||
<Form.Element label="">
|
||||
<Input
|
||||
value={String(!disableWebPagePreview)}
|
||||
name="disableWebPagePreview"
|
||||
onChange={onChange}
|
||||
type={InputType.Checkbox}
|
||||
/>
|
||||
</Form.Element>
|
||||
*/
|
||||
)
|
||||
}
|
||||
|
||||
export default TelegramMessage
|
|
@ -8,6 +8,7 @@ import {
|
|||
SlackNotificationRuleBase,
|
||||
SMTPNotificationRuleBase,
|
||||
PagerDutyNotificationRuleBase,
|
||||
TelegramNotificationRuleBase,
|
||||
NotificationEndpoint,
|
||||
NotificationRuleDraft,
|
||||
HTTPNotificationRuleBase,
|
||||
|
@ -22,6 +23,7 @@ type RuleVariantFields =
|
|||
| SMTPNotificationRuleBase
|
||||
| PagerDutyNotificationRuleBase
|
||||
| HTTPNotificationRuleBase
|
||||
| TelegramNotificationRuleBase
|
||||
|
||||
const defaultMessage =
|
||||
'Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }'
|
||||
|
@ -45,6 +47,20 @@ export const getRuleVariantDefaults = (
|
|||
return {type: 'http', url: ''}
|
||||
}
|
||||
|
||||
case 'telegram': {
|
||||
// wrap all variable values into `` to prevent telegram's markdown errors
|
||||
const messageTemplate = defaultMessage.replace(
|
||||
/\$\{[^\}]+\}/g,
|
||||
x => `\`${x}\``
|
||||
)
|
||||
return {
|
||||
messageTemplate: messageTemplate,
|
||||
parseMode: 'MarkdownV2',
|
||||
disableWebPagePreview: false,
|
||||
type: 'telegram',
|
||||
}
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Could not find NotificationEndpoint with id "${id}"`)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
Threshold,
|
||||
CheckBase as GenCheckBase,
|
||||
NotificationEndpointBase as GenEndpointBase,
|
||||
TelegramNotificationRuleBase,
|
||||
TelegramNotificationEndpoint,
|
||||
} from 'src/client'
|
||||
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
@ -50,6 +52,8 @@ export type NotificationEndpoint =
|
|||
| (Omit<PagerDutyNotificationEndpoint, 'status' | 'labels'> &
|
||||
EndpointOverrides)
|
||||
| (Omit<HTTPNotificationEndpoint, 'status' | 'labels'> & EndpointOverrides)
|
||||
| (Omit<TelegramNotificationEndpoint, 'status' | 'labels'> &
|
||||
EndpointOverrides)
|
||||
export type NotificationEndpointBase = Omit<GenEndpointBase, 'labels'> &
|
||||
EndpointOverrides
|
||||
|
||||
|
@ -76,7 +80,7 @@ export type NotificationRuleBaseDraft = Overwrite<
|
|||
}
|
||||
>
|
||||
|
||||
type RuleDraft = SlackRule | SMTPRule | PagerDutyRule | HTTPRule
|
||||
type RuleDraft = SlackRule | SMTPRule | PagerDutyRule | HTTPRule | TelegramRule
|
||||
|
||||
export type NotificationRuleDraft = RuleDraft
|
||||
|
||||
|
@ -96,6 +100,10 @@ type HTTPRule = NotificationRuleBaseDraft &
|
|||
HTTPNotificationRuleBase &
|
||||
RuleOverrides
|
||||
|
||||
type TelegramRule = NotificationRuleBaseDraft &
|
||||
TelegramNotificationRuleBase &
|
||||
RuleOverrides
|
||||
|
||||
export type LowercaseCheckStatusLevel =
|
||||
| 'crit'
|
||||
| 'warn'
|
||||
|
@ -189,4 +197,7 @@ export {
|
|||
PostNotificationRule,
|
||||
CheckPatch,
|
||||
TaskStatusType,
|
||||
TelegramNotificationEndpoint,
|
||||
TelegramNotificationRuleBase,
|
||||
TelegramNotificationRule,
|
||||
} from '../client'
|
||||
|
|
Loading…
Reference in New Issue